///|
/// A MoonBit package discovered from a moon.pkg file.
pub(all) struct MoonPackage {
path : String
import_path : String
manifest : MoonManifest
source_files : Array[ProjectFile]
test_files : Array[ProjectFile]
} derive(Debug, Eq)
///|
pub fn MoonPackage::new(
path : String,
import_path : String,
manifest : MoonManifest,
source_files : Array[ProjectFile],
test_files : Array[ProjectFile],
) -> MoonPackage {
{ path, import_path, manifest, source_files, test_files }
}
///|
pub fn MoonPackage::has_tests(self : MoonPackage) -> Bool {
self.test_files.length() > 0
}
///|
pub fn MoonPackage::has_source(self : MoonPackage) -> Bool {
self.source_files.length() > 0
}
///|
/// Directed graph of local package imports in one MoonBit module.
pub(all) struct PackageGraph {
module_name : String
packages : Array[MoonPackage]
local_edges : Map[String, Array[String]]
missing_local_imports : Array[(String, String)]
cycles : Array[Array[String]]
} derive(Debug, Eq)
///|
pub fn PackageGraph::new(
module_name : String,
packages : Array[MoonPackage],
local_edges : Map[String, Array[String]],
missing_local_imports : Array[(String, String)],
cycles : Array[Array[String]],
) -> PackageGraph {
{ module_name, packages, local_edges, missing_local_imports, cycles }
}
///|
pub fn PackageGraph::find_package(
self : PackageGraph,
import_path : String,
) -> MoonPackage? {
for node in self.packages {
if node.import_path == import_path {
return Some(node)
}
}
None
}
///|
pub fn PackageGraph::package_count(self : PackageGraph) -> Int {
self.packages.length()
}
///|
pub fn PackageGraph::edge_count(self : PackageGraph) -> Int {
let mut count = 0
for _, targets in self.local_edges {
count += targets.length()
}
count
}
///|
fn package_directory(manifest_path : String) -> String {
let pieces = manifest_path.split("/").to_array()
if pieces.length() <= 1 {
""
} else {
pieces[:pieces.length() - 1].join("/")
}
}
///|
fn package_import_path(module_name : String, directory : String) -> String {
if directory.length() == 0 {
module_name
} else {
module_name + "/" + directory
}
}
///|
fn package_files(
inventory : ProjectInventory,
directory : String,
) -> Array[ProjectFile] {
let prefix = if directory.length() == 0 { "" } else { directory + "/" }
let files : Array[ProjectFile] = []
for file in inventory.files {
if file.path.has_prefix(prefix) {
let nested_path = file.path[prefix.length():]
if !nested_path.contains("/") && file.path.has_suffix(".mbt") {
files.push(file)
}
}
}
files
}
///|
fn split_test_files(
files : Array[ProjectFile],
) -> (Array[ProjectFile], Array[ProjectFile]) {
let source_files : Array[ProjectFile] = []
let test_files : Array[ProjectFile] = []
for file in files {
if file.path.has_suffix("_test.mbt") || file.path.has_suffix("_wbtest.mbt") {
test_files.push(file)
} else {
source_files.push(file)
}
}
(source_files, test_files)
}
///|
fn discover_packages(
inventory : ProjectInventory,
module_name : String,
) -> Array[MoonPackage] {
let packages : Array[MoonPackage] = []
for manifest_file in inventory.files_with_suffix("moon.pkg") {
let directory = package_directory(manifest_file.path)
let all_files = package_files(inventory, directory)
let (source_files, test_files) = split_test_files(all_files)
packages.push(
MoonPackage::new(
directory,
package_import_path(module_name, directory),
parse_manifest_file(manifest_file),
source_files,
test_files,
),
)
}
packages
}
///|
fn local_imports(
module_name : String,
imports : Array[String],
) -> Array[String] {
let local_imports : Array[String] = []
for import_name in imports {
if import_name == module_name || import_name.has_prefix(module_name + "/") {
local_imports.push(import_name)
}
}
local_imports
}
///|
fn package_paths(packages : Array[MoonPackage]) -> Map[String, Unit] {
let paths : Map[String, Unit] = {}
for node in packages {
paths[node.import_path] = ()
}
paths
}
///|
fn graph_edges(
module_name : String,
packages : Array[MoonPackage],
) -> (Map[String, Array[String]], Array[(String, String)]) {
let edges : Map[String, Array[String]] = {}
let missing : Array[(String, String)] = []
let known = package_paths(packages)
for node in packages {
let imports = local_imports(module_name, node.manifest.imports)
edges[node.import_path] = imports
for target in imports {
if !known.contains(target) {
missing.push((node.import_path, target))
}
}
}
(edges, missing)
}
///|
fn find_cycle_from(
current : String,
edges : Map[String, Array[String]],
visiting : Map[String, Unit],
visited : Map[String, Unit],
path : Array[String],
cycles : Array[Array[String]],
) -> Unit {
if visited.contains(current) {
return
}
if visiting.contains(current) {
let cycle : Array[String] = []
let mut collecting = false
for item in path {
if item == current {
collecting = true
}
if collecting {
cycle.push(item)
}
}
cycle.push(current)
if cycle.length() > 1 && !cycles.contains(cycle) {
cycles.push(cycle)
}
return
}
visiting[current] = ()
path.push(current)
match edges.get(current) {
Some(targets) =>
for target in targets {
find_cycle_from(target, edges, visiting, visited, path, cycles)
}
None => ()
}
ignore(path.pop())
visiting.remove(current)
visited[current] = ()
}
///|
fn find_cycles(edges : Map[String, Array[String]]) -> Array[Array[String]] {
let cycles : Array[Array[String]] = []
let visited : Map[String, Unit] = {}
for start in edges.keys() {
let visiting : Map[String, Unit] = {}
find_cycle_from(start, edges, visiting, visited, [], cycles)
}
cycles
}
///|
/// Build the local package dependency graph from a recursive inventory.
pub fn build_package_graph(inventory : ProjectInventory) -> PackageGraph {
let module_name = match parse_root_manifest(inventory) {
Some(manifest) =>
match manifest.text("name") {
Some(name) => name
None => ""
}
None => ""
}
let packages = discover_packages(inventory, module_name)
let (edges, missing) = graph_edges(module_name, packages)
PackageGraph::new(module_name, packages, edges, missing, find_cycles(edges))
}