///|
pub(all) enum WorkspaceFileCategory {
SourceFileCategory
TestFileCategory
GeneratedFileCategory
ExcludedFileCategory
} derive(Debug, Eq, ToJson)
///|
pub(all) struct WorkspaceFileSpec {
include_suffixes : Array[String]
exclude_segments : Array[String]
exclude_suffixes : Array[String]
include_tests : Bool
include_generated : Bool
} derive(Debug, Eq, ToJson)
///|
pub(all) struct WorkspaceFileDecision {
path : String
included : Bool
category : WorkspaceFileCategory
reason : String
} derive(Debug, Eq, ToJson)
///|
pub(all) struct WorkspaceSelectionReport {
total_files : Int
included_files : Int
excluded_files : Int
source_files : Int
test_files : Int
generated_files : Int
decisions : Array[WorkspaceFileDecision]
} derive(Debug, Eq, ToJson)
///|
pub fn WorkspaceFileSpec::moonbit_default() -> WorkspaceFileSpec {
{
include_suffixes: [".mbt"],
exclude_segments: ["_build", ".moon", ".git", ".repos"],
exclude_suffixes: [],
include_tests: false,
include_generated: false,
}
}
///|
pub fn WorkspaceFileSpec::with_tests() -> WorkspaceFileSpec {
{
include_suffixes: [".mbt"],
exclude_segments: ["_build", ".moon", ".git", ".repos"],
exclude_suffixes: [],
include_tests: true,
include_generated: false,
}
}
///|
pub fn classify_workspace_file(
path : String,
spec : WorkspaceFileSpec,
) -> WorkspaceFileDecision {
if !has_any_suffix(path, spec.include_suffixes) {
{
path,
included: false,
category: ExcludedFileCategory,
reason: "path does not match included suffixes",
}
} else if has_any_segment(path, spec.exclude_segments) {
{
path,
included: false,
category: ExcludedFileCategory,
reason: "path is under an excluded directory",
}
} else if is_generated_moonbit_path(path) {
{
path,
included: spec.include_generated,
category: GeneratedFileCategory,
reason: if spec.include_generated {
"generated MoonBit file included by configuration"
} else {
"generated MoonBit file excluded by default"
},
}
} else if has_any_suffix(path, spec.exclude_suffixes) {
{
path,
included: false,
category: ExcludedFileCategory,
reason: "path matches an excluded suffix",
}
} else if is_test_moonbit_path(path) {
{
path,
included: spec.include_tests,
category: TestFileCategory,
reason: if spec.include_tests {
"test file included by configuration"
} else {
"test file excluded by default"
},
}
} else {
{
path,
included: true,
category: SourceFileCategory,
reason: "MoonBit source file included",
}
}
}
///|
pub fn select_workspace_files(
files : ArrayView[SourceFile],
spec : WorkspaceFileSpec,
) -> Array[SourceFile] {
let selected : Array[SourceFile] = []
for file in files {
let decision = classify_workspace_file(file.path, spec)
if decision.included {
selected.push(file)
}
}
selected
}
///|
/// Keep only already-selected workspace files whose root-relative paths are
/// present in a Git changed-file list.
pub fn select_changed_workspace_files(
files : ArrayView[SourceFile],
changed_paths : ArrayView[String],
) -> Array[SourceFile] {
let selected : Array[SourceFile] = []
for file in files {
if is_changed_workspace_path(file.path, changed_paths) {
selected.push(file)
}
}
selected
}
///|
pub fn summarize_workspace_selection(
files : ArrayView[SourceFile],
spec : WorkspaceFileSpec,
) -> WorkspaceSelectionReport {
let decisions : Array[WorkspaceFileDecision] = []
let mut included_files = 0
let mut source_files = 0
let mut test_files = 0
let mut generated_files = 0
for file in files {
let decision = classify_workspace_file(file.path, spec)
if decision.included {
included_files += 1
match decision.category {
SourceFileCategory => source_files += 1
TestFileCategory => test_files += 1
GeneratedFileCategory => generated_files += 1
ExcludedFileCategory => ()
}
}
decisions.push(decision)
}
{
total_files: files.length(),
included_files,
excluded_files: files.length() - included_files,
source_files,
test_files,
generated_files,
decisions,
}
}
///|
pub fn format_workspace_file_category(
category : WorkspaceFileCategory,
) -> String {
match category {
SourceFileCategory => "source"
TestFileCategory => "test"
GeneratedFileCategory => "generated"
ExcludedFileCategory => "excluded"
}
}
///|
pub fn format_workspace_selection_report(
report : WorkspaceSelectionReport,
) -> String {
let lines : Array[String] = [
"Workspace selection",
"total-files: \{report.total_files}",
"included-files: \{report.included_files}",
"excluded-files: \{report.excluded_files}",
"source-files: \{report.source_files}",
"test-files: \{report.test_files}",
"generated-files: \{report.generated_files}",
]
for decision in report.decisions {
let action = if decision.included { "include" } else { "exclude" }
lines.push(
"- \{decision.path}: " +
"\{action} " +
"(\{format_workspace_file_category(decision.category)}) " +
"\{decision.reason}",
)
}
lines.join("\n")
}
///|
fn has_any_suffix(path : String, suffixes : ArrayView[String]) -> Bool {
for suffix in suffixes {
if path.has_suffix(suffix) {
break true
}
} nobreak {
false
}
}
///|
fn has_any_segment(path : String, segments : ArrayView[String]) -> Bool {
let normalized = normalize_path(path)
for segment in segments {
if normalized == segment ||
normalized.has_prefix(segment + "/") ||
normalized.has_suffix("/" + segment) ||
normalized.contains("/" + segment + "/") {
break true
}
} nobreak {
false
}
}
///|
fn is_test_moonbit_path(path : String) -> Bool {
path.has_suffix("_test.mbt") ||
path.has_suffix("_wbtest.mbt") ||
normalize_path(path).contains("/test/")
}
///|
fn is_generated_moonbit_path(path : String) -> Bool {
path.has_suffix(".generated.mbt") ||
path.has_suffix(".generated.mbti") ||
path.has_suffix("pkg.generated.mbti")
}
///|
fn normalize_path(path : String) -> String {
path.replace(old="\\", new="/")
}
///|
fn is_changed_workspace_path(
file_path : String,
changed_paths : ArrayView[String],
) -> Bool {
let normalized_file = normalize_path(file_path)
for changed_path in changed_paths {
if normalized_file == normalize_path(changed_path) {
break true
}
} nobreak {
false
}
}