@lamentis/naome 1.3.15 → 1.3.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +2 -2
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/architecture_commands.rs +5 -3
- package/crates/naome-cli/src/architecture_init/infer.rs +131 -0
- package/crates/naome-cli/src/architecture_init/render.rs +56 -0
- package/crates/naome-cli/src/architecture_init/repository.rs +59 -0
- package/crates/naome-cli/src/architecture_init.rs +17 -0
- package/crates/naome-cli/src/main.rs +1 -0
- package/crates/naome-cli/tests/architecture_cli.rs +75 -0
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/architecture/config_findings/configuration/coverage.rs +81 -0
- package/crates/naome-core/src/architecture/config_findings/configuration/overlap.rs +117 -0
- package/crates/naome-core/src/architecture/config_findings/configuration.rs +12 -0
- package/crates/naome-core/src/architecture/config_findings/imports.rs +30 -0
- package/crates/naome-core/src/architecture/config_findings.rs +50 -0
- package/crates/naome-core/src/architecture/explain.rs +45 -0
- package/crates/naome-core/src/architecture/output.rs +59 -36
- package/crates/naome-core/src/architecture/rules.rs +5 -1
- package/crates/naome-core/src/architecture/scan/cache.rs +1 -1
- package/crates/naome-core/src/architecture/scan/imports/resolver/candidates.rs +71 -0
- package/crates/naome-core/src/architecture/scan/imports/resolver/js_ts_alias.rs +241 -0
- package/crates/naome-core/src/architecture/scan/imports/resolver.rs +162 -91
- package/crates/naome-core/src/architecture/scan.rs +20 -6
- package/crates/naome-core/src/architecture.rs +6 -2
- package/crates/naome-core/src/lib.rs +8 -7
- package/crates/naome-core/tests/architecture.rs +30 -0
- package/crates/naome-core/tests/architecture_acceptance.rs +304 -0
- package/crates/naome-core/tests/architecture_aliases.rs +101 -0
- package/crates/naome-core/tests/architecture_cache.rs +57 -0
- package/crates/naome-core/tests/architecture_config.rs +154 -0
- package/crates/naome-core/tests/architecture_rules.rs +32 -0
- package/crates/naome-core/tests/architecture_unresolved.rs +36 -0
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- package/package.json +1 -1
- package/templates/naome-root/.naome/bin/check-harness-health.js +1 -0
- package/templates/naome-root/.naome/bin/check-task-state.js +1 -0
- package/templates/naome-root/.naome/manifest.json +1 -1
- package/templates/naome-root/.naome/verification.json +6 -1
- package/templates/naome-root/docs/naome/architecture-fitness.md +68 -51
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
mod configuration;
|
|
2
|
+
mod imports;
|
|
3
|
+
|
|
4
|
+
use super::output::{ArchitectureAgentFeedback, ArchitectureConfigFinding};
|
|
5
|
+
use super::scan::ArchitectureScanReport;
|
|
6
|
+
|
|
7
|
+
pub fn architecture_config_agent_feedback(
|
|
8
|
+
findings: &[ArchitectureConfigFinding],
|
|
9
|
+
) -> Vec<ArchitectureAgentFeedback> {
|
|
10
|
+
findings
|
|
11
|
+
.iter()
|
|
12
|
+
.filter(|finding| finding.id == "arch.import.unresolved")
|
|
13
|
+
.map(|finding| ArchitectureAgentFeedback {
|
|
14
|
+
problem: finding.message.clone(),
|
|
15
|
+
repair: finding.suggestion.clone(),
|
|
16
|
+
files: finding
|
|
17
|
+
.subject
|
|
18
|
+
.strip_prefix("file:")
|
|
19
|
+
.map(|path| vec![path.to_string()])
|
|
20
|
+
.unwrap_or_default(),
|
|
21
|
+
must_not_do: vec![
|
|
22
|
+
"Do not leave unresolved imports hidden from architecture validation.".to_string(),
|
|
23
|
+
"Do not suppress architecture findings without an explicit ignore reason."
|
|
24
|
+
.to_string(),
|
|
25
|
+
],
|
|
26
|
+
})
|
|
27
|
+
.collect()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
pub fn config_findings_for(scan: &ArchitectureScanReport) -> Vec<ArchitectureConfigFinding> {
|
|
31
|
+
let mut findings = Vec::new();
|
|
32
|
+
findings.extend(configuration::findings(scan));
|
|
33
|
+
findings.extend(imports::findings(scan));
|
|
34
|
+
findings.sort_by(|left, right| {
|
|
35
|
+
(
|
|
36
|
+
left.id.as_str(),
|
|
37
|
+
left.subject.as_str(),
|
|
38
|
+
left.message.as_str(),
|
|
39
|
+
)
|
|
40
|
+
.cmp(&(
|
|
41
|
+
right.id.as_str(),
|
|
42
|
+
right.subject.as_str(),
|
|
43
|
+
right.message.as_str(),
|
|
44
|
+
))
|
|
45
|
+
});
|
|
46
|
+
findings.dedup_by(|left, right| {
|
|
47
|
+
left.id == right.id && left.subject == right.subject && left.message == right.message
|
|
48
|
+
});
|
|
49
|
+
findings
|
|
50
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
use super::config_findings::config_findings_for;
|
|
2
|
+
use super::output::ARCHITECTURE_RULE_IDS;
|
|
3
|
+
use super::scan::ArchitectureScanReport;
|
|
4
|
+
|
|
5
|
+
pub fn format_architecture_explain(scan: &ArchitectureScanReport) -> String {
|
|
6
|
+
let findings = config_findings_for(scan);
|
|
7
|
+
let layers = scan
|
|
8
|
+
.config
|
|
9
|
+
.layers
|
|
10
|
+
.keys()
|
|
11
|
+
.cloned()
|
|
12
|
+
.collect::<Vec<_>>()
|
|
13
|
+
.join(", ");
|
|
14
|
+
let contexts = scan
|
|
15
|
+
.config
|
|
16
|
+
.contexts
|
|
17
|
+
.keys()
|
|
18
|
+
.cloned()
|
|
19
|
+
.collect::<Vec<_>>()
|
|
20
|
+
.join(", ");
|
|
21
|
+
let mut output = format!(
|
|
22
|
+
"NAOME Architecture Fitness\nrules: {}\nlayers: {}\ncontexts: {}\npath extractor: enabled for every repository\nimport extractors: typescript, javascript, rust, python, go, swift\n",
|
|
23
|
+
ARCHITECTURE_RULE_IDS.join(", "),
|
|
24
|
+
empty_label(&layers),
|
|
25
|
+
empty_label(&contexts)
|
|
26
|
+
);
|
|
27
|
+
if !findings.is_empty() {
|
|
28
|
+
output.push_str(&format!("configuration findings: {}\n", findings.len()));
|
|
29
|
+
for finding in findings.iter().take(5) {
|
|
30
|
+
output.push_str(&format!(
|
|
31
|
+
"- {} {}: {}\n",
|
|
32
|
+
finding.severity, finding.subject, finding.message
|
|
33
|
+
));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
output
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fn empty_label(value: &str) -> &str {
|
|
40
|
+
if value.is_empty() {
|
|
41
|
+
"<none>"
|
|
42
|
+
} else {
|
|
43
|
+
value
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -34,6 +34,17 @@ pub struct ArchitectureAgentFeedback {
|
|
|
34
34
|
pub must_not_do: Vec<String>,
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
38
|
+
#[serde(rename_all = "camelCase")]
|
|
39
|
+
pub struct ArchitectureConfigFinding {
|
|
40
|
+
pub id: String,
|
|
41
|
+
pub severity: String,
|
|
42
|
+
pub subject: String,
|
|
43
|
+
pub message: String,
|
|
44
|
+
pub suggestion: String,
|
|
45
|
+
pub agent_instruction: String,
|
|
46
|
+
}
|
|
47
|
+
|
|
37
48
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
38
49
|
#[serde(rename_all = "camelCase")]
|
|
39
50
|
pub struct ArchitectureValidation {
|
|
@@ -48,6 +59,7 @@ pub struct ArchitectureValidation {
|
|
|
48
59
|
pub changed_only_degraded_to_full_scan: bool,
|
|
49
60
|
pub changed_only_mode: String,
|
|
50
61
|
pub changed_only_degradation_reason: Option<String>,
|
|
62
|
+
pub config_findings: Vec<ArchitectureConfigFinding>,
|
|
51
63
|
pub violations: Vec<ArchitectureViolation>,
|
|
52
64
|
#[serde(rename = "agent_feedback")]
|
|
53
65
|
pub agent_feedback: Vec<ArchitectureAgentFeedback>,
|
|
@@ -74,15 +86,42 @@ pub fn architecture_agent_feedback(
|
|
|
74
86
|
.map(|violation| ArchitectureAgentFeedback {
|
|
75
87
|
problem: violation.message.clone(),
|
|
76
88
|
repair: violation.suggestion.clone(),
|
|
77
|
-
files: violation
|
|
78
|
-
must_not_do:
|
|
79
|
-
"Do not suppress architecture rules without an explicit ignore reason.".to_string(),
|
|
80
|
-
"Do not edit generated files unless the architecture config allows it.".to_string(),
|
|
81
|
-
],
|
|
89
|
+
files: feedback_files(violation),
|
|
90
|
+
must_not_do: feedback_must_not_do(violation),
|
|
82
91
|
})
|
|
83
92
|
.collect()
|
|
84
93
|
}
|
|
85
94
|
|
|
95
|
+
fn feedback_files(violation: &ArchitectureViolation) -> Vec<String> {
|
|
96
|
+
let mut files = Vec::new();
|
|
97
|
+
if let Some(path) = &violation.path {
|
|
98
|
+
files.push(path.clone());
|
|
99
|
+
}
|
|
100
|
+
for endpoint in [&violation.from, &violation.to] {
|
|
101
|
+
let Some(path) = endpoint
|
|
102
|
+
.as_deref()
|
|
103
|
+
.and_then(|value| value.strip_prefix("file:"))
|
|
104
|
+
else {
|
|
105
|
+
continue;
|
|
106
|
+
};
|
|
107
|
+
if !files.iter().any(|existing| existing == path) {
|
|
108
|
+
files.push(path.to_string());
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
files
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fn feedback_must_not_do(violation: &ArchitectureViolation) -> Vec<String> {
|
|
115
|
+
let mut items = vec![
|
|
116
|
+
"Do not suppress architecture rules without an explicit ignore reason.".to_string(),
|
|
117
|
+
"Do not edit generated files unless the architecture config allows it.".to_string(),
|
|
118
|
+
];
|
|
119
|
+
if violation.id == "arch.no_forbidden_layer_dependencies" {
|
|
120
|
+
items.push(violation.agent_instruction.clone());
|
|
121
|
+
}
|
|
122
|
+
items
|
|
123
|
+
}
|
|
124
|
+
|
|
86
125
|
pub fn summary_for(violations: &[ArchitectureViolation]) -> ViolationSummary {
|
|
87
126
|
ViolationSummary {
|
|
88
127
|
errors: violations
|
|
@@ -128,6 +167,21 @@ pub fn format_architecture_validation(report: &ArchitectureValidation) -> String
|
|
|
128
167
|
lines.push("changed-only requested: using incremental architecture cache".to_string());
|
|
129
168
|
}
|
|
130
169
|
|
|
170
|
+
if !report.config_findings.is_empty() {
|
|
171
|
+
lines.push(format!(
|
|
172
|
+
"configuration findings: {}",
|
|
173
|
+
report.config_findings.len()
|
|
174
|
+
));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for finding in report.config_findings.iter().take(5) {
|
|
178
|
+
lines.push(format!(
|
|
179
|
+
"- Config {} {} {}",
|
|
180
|
+
finding.severity, finding.subject, finding.message
|
|
181
|
+
));
|
|
182
|
+
lines.push(format!(" fix: {}", finding.suggestion));
|
|
183
|
+
}
|
|
184
|
+
|
|
131
185
|
for violation in report.violations.iter().take(10) {
|
|
132
186
|
lines.push(format!(
|
|
133
187
|
"- {:?} {} {}",
|
|
@@ -162,34 +216,3 @@ pub fn format_architecture_scan(report: &ArchitectureScanReport) -> String {
|
|
|
162
216
|
}
|
|
163
217
|
output
|
|
164
218
|
}
|
|
165
|
-
|
|
166
|
-
pub fn format_architecture_explain(scan: &ArchitectureScanReport) -> String {
|
|
167
|
-
let layers = scan
|
|
168
|
-
.config
|
|
169
|
-
.layers
|
|
170
|
-
.keys()
|
|
171
|
-
.cloned()
|
|
172
|
-
.collect::<Vec<_>>()
|
|
173
|
-
.join(", ");
|
|
174
|
-
let contexts = scan
|
|
175
|
-
.config
|
|
176
|
-
.contexts
|
|
177
|
-
.keys()
|
|
178
|
-
.cloned()
|
|
179
|
-
.collect::<Vec<_>>()
|
|
180
|
-
.join(", ");
|
|
181
|
-
format!(
|
|
182
|
-
"NAOME Architecture Fitness\nrules: {}\nlayers: {}\ncontexts: {}\npath extractor: enabled for every repository\nimport extractors: typescript, javascript, rust, python, go, swift\n",
|
|
183
|
-
ARCHITECTURE_RULE_IDS.join(", "),
|
|
184
|
-
empty_label(&layers),
|
|
185
|
-
empty_label(&contexts)
|
|
186
|
-
)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
fn empty_label(value: &str) -> &str {
|
|
190
|
-
if value.is_empty() {
|
|
191
|
-
"<none>"
|
|
192
|
-
} else {
|
|
193
|
-
value
|
|
194
|
-
}
|
|
195
|
-
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
use crate::paths;
|
|
2
2
|
|
|
3
|
+
use super::config_findings::{architecture_config_agent_feedback, config_findings_for};
|
|
3
4
|
use super::model::Severity;
|
|
4
5
|
use super::output::{
|
|
5
6
|
architecture_agent_feedback, summary_for, ArchitectureValidation, ArchitectureViolation,
|
|
@@ -43,7 +44,9 @@ pub fn validate_scan(scan: ArchitectureScanReport) -> ArchitectureValidation {
|
|
|
43
44
|
});
|
|
44
45
|
let summary = summary_for(&violations);
|
|
45
46
|
let status = if summary.errors > 0 { "fail" } else { "pass" }.to_string();
|
|
46
|
-
let
|
|
47
|
+
let config_findings = config_findings_for(&scan);
|
|
48
|
+
let mut agent_feedback = architecture_agent_feedback(&violations);
|
|
49
|
+
agent_feedback.extend(architecture_config_agent_feedback(&config_findings));
|
|
47
50
|
|
|
48
51
|
ArchitectureValidation {
|
|
49
52
|
schema: "naome.arch.validation.v1".to_string(),
|
|
@@ -57,6 +60,7 @@ pub fn validate_scan(scan: ArchitectureScanReport) -> ArchitectureValidation {
|
|
|
57
60
|
changed_only_degraded_to_full_scan: scan.changed_only_degraded_to_full_scan,
|
|
58
61
|
changed_only_mode: scan.changed_only_mode,
|
|
59
62
|
changed_only_degradation_reason: scan.changed_only_degradation_reason,
|
|
63
|
+
config_findings,
|
|
60
64
|
violations,
|
|
61
65
|
agent_feedback,
|
|
62
66
|
}
|
|
@@ -9,7 +9,7 @@ use super::FileFact;
|
|
|
9
9
|
use crate::models::NaomeError;
|
|
10
10
|
|
|
11
11
|
const CACHE_SCHEMA: &str = "naome.architecture-cache.v1";
|
|
12
|
-
const EXTRACTOR_VERSION: &str = "architecture-cache-v1.3.
|
|
12
|
+
const EXTRACTOR_VERSION: &str = "architecture-cache-v1.3.17";
|
|
13
13
|
const CACHE_PATH: &str = ".naome/cache/architecture/cache.json";
|
|
14
14
|
|
|
15
15
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
use std::collections::BTreeSet;
|
|
2
|
+
|
|
3
|
+
pub(super) fn resolve_candidates(
|
|
4
|
+
candidate: &str,
|
|
5
|
+
repository_files: &BTreeSet<String>,
|
|
6
|
+
language: Option<&str>,
|
|
7
|
+
) -> Option<String> {
|
|
8
|
+
candidate_paths(candidate, language)
|
|
9
|
+
.into_iter()
|
|
10
|
+
.find(|path| repository_files.contains(path))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
pub(super) fn resolve_progressively(
|
|
14
|
+
candidate: &str,
|
|
15
|
+
repository_files: &BTreeSet<String>,
|
|
16
|
+
language: Option<&str>,
|
|
17
|
+
separator: char,
|
|
18
|
+
) -> Option<String> {
|
|
19
|
+
let mut current = candidate.to_string();
|
|
20
|
+
loop {
|
|
21
|
+
if let Some(path) = resolve_candidates(¤t, repository_files, language) {
|
|
22
|
+
return Some(path);
|
|
23
|
+
}
|
|
24
|
+
let Some((parent, _)) = current.rsplit_once(separator) else {
|
|
25
|
+
return None;
|
|
26
|
+
};
|
|
27
|
+
current = parent.to_string();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fn candidate_paths(candidate: &str, language: Option<&str>) -> Vec<String> {
|
|
32
|
+
let extensions = extensions_for_language(language);
|
|
33
|
+
let mut candidates = extensions
|
|
34
|
+
.iter()
|
|
35
|
+
.map(|extension| format!("{candidate}{extension}"))
|
|
36
|
+
.collect::<Vec<_>>();
|
|
37
|
+
match language {
|
|
38
|
+
Some("rust") => candidates.push(format!("{candidate}/mod.rs")),
|
|
39
|
+
Some("python") => candidates.push(format!("{candidate}/__init__.py")),
|
|
40
|
+
Some("go") => {}
|
|
41
|
+
Some("typescript") | Some("javascript") => candidates.extend([
|
|
42
|
+
format!("{candidate}/index.ts"),
|
|
43
|
+
format!("{candidate}/index.tsx"),
|
|
44
|
+
format!("{candidate}/index.js"),
|
|
45
|
+
format!("{candidate}/index.jsx"),
|
|
46
|
+
]),
|
|
47
|
+
_ => candidates.extend([
|
|
48
|
+
format!("{candidate}/index.ts"),
|
|
49
|
+
format!("{candidate}/index.tsx"),
|
|
50
|
+
format!("{candidate}/index.js"),
|
|
51
|
+
format!("{candidate}/index.jsx"),
|
|
52
|
+
format!("{candidate}/mod.rs"),
|
|
53
|
+
format!("{candidate}/__init__.py"),
|
|
54
|
+
]),
|
|
55
|
+
}
|
|
56
|
+
candidates
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fn extensions_for_language(language: Option<&str>) -> &'static [&'static str] {
|
|
60
|
+
match language {
|
|
61
|
+
Some("rust") => &["", ".rs"],
|
|
62
|
+
Some("python") => &["", ".py"],
|
|
63
|
+
Some("go") => &["", ".go"],
|
|
64
|
+
Some("swift") => &["", ".swift"],
|
|
65
|
+
Some("typescript") => &["", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
|
|
66
|
+
Some("javascript") => &["", ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"],
|
|
67
|
+
_ => &[
|
|
68
|
+
"", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".rs", ".py", ".go", ".swift",
|
|
69
|
+
],
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
use std::collections::BTreeSet;
|
|
2
|
+
use std::collections::HashSet;
|
|
3
|
+
use std::fs;
|
|
4
|
+
use std::path::{Path, PathBuf};
|
|
5
|
+
|
|
6
|
+
use serde_json::Value;
|
|
7
|
+
|
|
8
|
+
pub(super) enum AliasResolution {
|
|
9
|
+
Resolved(String),
|
|
10
|
+
Unresolved,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
pub(super) fn resolve(
|
|
14
|
+
root: &Path,
|
|
15
|
+
from_path: &str,
|
|
16
|
+
specifier: &str,
|
|
17
|
+
repository_files: &BTreeSet<String>,
|
|
18
|
+
language: Option<&str>,
|
|
19
|
+
) -> Option<AliasResolution> {
|
|
20
|
+
for config_path in nearest_configs(from_path, repository_files) {
|
|
21
|
+
let mut seen = HashSet::new();
|
|
22
|
+
for config_path in config_with_extends(root, &config_path, &mut seen) {
|
|
23
|
+
let Some(resolution) =
|
|
24
|
+
resolve_from_config(root, &config_path, specifier, repository_files, language)
|
|
25
|
+
else {
|
|
26
|
+
continue;
|
|
27
|
+
};
|
|
28
|
+
match resolution {
|
|
29
|
+
AliasResolution::Resolved(_) => return Some(resolution),
|
|
30
|
+
AliasResolution::Unresolved => return Some(AliasResolution::Unresolved),
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
None
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fn resolve_from_config(
|
|
38
|
+
root: &Path,
|
|
39
|
+
config_path: &str,
|
|
40
|
+
specifier: &str,
|
|
41
|
+
repository_files: &BTreeSet<String>,
|
|
42
|
+
language: Option<&str>,
|
|
43
|
+
) -> Option<AliasResolution> {
|
|
44
|
+
let config_dir = Path::new(config_path)
|
|
45
|
+
.parent()
|
|
46
|
+
.unwrap_or_else(|| Path::new(""));
|
|
47
|
+
let raw = read_jsonc(root, config_path)?;
|
|
48
|
+
let compiler_options = raw.get("compilerOptions")?;
|
|
49
|
+
let base_url = compiler_options
|
|
50
|
+
.get("baseUrl")
|
|
51
|
+
.and_then(Value::as_str)
|
|
52
|
+
.unwrap_or(".");
|
|
53
|
+
let paths = compiler_options.get("paths")?.as_object()?;
|
|
54
|
+
let mut matches = paths
|
|
55
|
+
.iter()
|
|
56
|
+
.filter_map(|(alias_pattern, targets)| {
|
|
57
|
+
let capture = alias_capture(alias_pattern, specifier)?;
|
|
58
|
+
Some((
|
|
59
|
+
alias_specificity(alias_pattern),
|
|
60
|
+
alias_pattern,
|
|
61
|
+
capture,
|
|
62
|
+
targets,
|
|
63
|
+
))
|
|
64
|
+
})
|
|
65
|
+
.collect::<Vec<_>>();
|
|
66
|
+
matches.sort_by(|left, right| {
|
|
67
|
+
right
|
|
68
|
+
.0
|
|
69
|
+
.cmp(&left.0)
|
|
70
|
+
.then_with(|| right.1.len().cmp(&left.1.len()))
|
|
71
|
+
.then_with(|| left.1.cmp(right.1))
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
let (_, _, capture, targets) = matches.into_iter().next()?;
|
|
75
|
+
let Some(targets) = targets.as_array() else {
|
|
76
|
+
return Some(AliasResolution::Unresolved);
|
|
77
|
+
};
|
|
78
|
+
for target in targets.iter().filter_map(Value::as_str) {
|
|
79
|
+
let expanded = target.replace('*', &capture);
|
|
80
|
+
let candidate = super::normalize(config_dir.join(base_url).join(expanded));
|
|
81
|
+
if let Some(path) = super::resolve_candidates(&candidate, repository_files, language) {
|
|
82
|
+
return Some(AliasResolution::Resolved(path));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
Some(AliasResolution::Unresolved)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fn read_jsonc(root: &Path, config_path: &str) -> Option<Value> {
|
|
89
|
+
let content = fs::read_to_string(root.join(config_path)).ok()?;
|
|
90
|
+
serde_json::from_str::<Value>(&strip_jsonc(&content)).ok()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fn config_with_extends(root: &Path, config_path: &str, seen: &mut HashSet<String>) -> Vec<String> {
|
|
94
|
+
if !seen.insert(config_path.to_string()) {
|
|
95
|
+
return Vec::new();
|
|
96
|
+
}
|
|
97
|
+
let mut configs = vec![config_path.to_string()];
|
|
98
|
+
let Some(raw) = read_jsonc(root, config_path) else {
|
|
99
|
+
return configs;
|
|
100
|
+
};
|
|
101
|
+
let Some(extends) = raw.get("extends").and_then(Value::as_str) else {
|
|
102
|
+
return configs;
|
|
103
|
+
};
|
|
104
|
+
let Some(extended_path) = resolve_extends_path(config_path, extends) else {
|
|
105
|
+
return configs;
|
|
106
|
+
};
|
|
107
|
+
configs.extend(config_with_extends(root, &extended_path, seen));
|
|
108
|
+
configs
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
fn resolve_extends_path(config_path: &str, extends: &str) -> Option<String> {
|
|
112
|
+
if extends.is_empty()
|
|
113
|
+
|| extends.starts_with('@')
|
|
114
|
+
|| (!extends.starts_with('.') && !extends.starts_with('/'))
|
|
115
|
+
{
|
|
116
|
+
return None;
|
|
117
|
+
}
|
|
118
|
+
let config_dir = Path::new(config_path)
|
|
119
|
+
.parent()
|
|
120
|
+
.unwrap_or_else(|| Path::new(""));
|
|
121
|
+
let mut candidate = if Path::new(extends).is_absolute() {
|
|
122
|
+
PathBuf::from(extends.strip_prefix('/').unwrap_or(extends))
|
|
123
|
+
} else {
|
|
124
|
+
config_dir.join(extends)
|
|
125
|
+
};
|
|
126
|
+
if candidate.extension().is_none() {
|
|
127
|
+
candidate.set_extension("json");
|
|
128
|
+
}
|
|
129
|
+
Some(super::normalize(candidate))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fn strip_jsonc(content: &str) -> String {
|
|
133
|
+
let mut stripped = String::with_capacity(content.len());
|
|
134
|
+
let mut chars = content.chars().peekable();
|
|
135
|
+
let mut in_string = false;
|
|
136
|
+
let mut escaped = false;
|
|
137
|
+
while let Some(character) = chars.next() {
|
|
138
|
+
if in_string {
|
|
139
|
+
if character == '"' && !escaped {
|
|
140
|
+
in_string = false;
|
|
141
|
+
}
|
|
142
|
+
stripped.push(character);
|
|
143
|
+
escaped = character == '\\' && !escaped;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if character == '"' {
|
|
147
|
+
in_string = true;
|
|
148
|
+
stripped.push(character);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if character == '/' && chars.peek() == Some(&'/') {
|
|
152
|
+
chars.next();
|
|
153
|
+
for next in chars.by_ref() {
|
|
154
|
+
if next == '\n' {
|
|
155
|
+
stripped.push('\n');
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if character == '/' && chars.peek() == Some(&'*') {
|
|
162
|
+
chars.next();
|
|
163
|
+
let mut previous = '\0';
|
|
164
|
+
for next in chars.by_ref() {
|
|
165
|
+
if next == '\n' {
|
|
166
|
+
stripped.push('\n');
|
|
167
|
+
}
|
|
168
|
+
if previous == '*' && next == '/' {
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
previous = next;
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
stripped.push(character);
|
|
176
|
+
}
|
|
177
|
+
remove_trailing_commas(&stripped)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
fn remove_trailing_commas(content: &str) -> String {
|
|
181
|
+
let mut output = String::with_capacity(content.len());
|
|
182
|
+
let chars = content.chars().collect::<Vec<_>>();
|
|
183
|
+
let mut index = 0usize;
|
|
184
|
+
while index < chars.len() {
|
|
185
|
+
if chars[index] == ',' {
|
|
186
|
+
let mut next = index + 1;
|
|
187
|
+
while next < chars.len() && chars[next].is_whitespace() {
|
|
188
|
+
next += 1;
|
|
189
|
+
}
|
|
190
|
+
if next < chars.len() && matches!(chars[next], '}' | ']') {
|
|
191
|
+
index += 1;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
output.push(chars[index]);
|
|
196
|
+
index += 1;
|
|
197
|
+
}
|
|
198
|
+
output
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
fn nearest_configs(from_path: &str, repository_files: &BTreeSet<String>) -> Vec<String> {
|
|
202
|
+
let mut configs = Vec::new();
|
|
203
|
+
let mut current = Path::new(from_path)
|
|
204
|
+
.parent()
|
|
205
|
+
.unwrap_or_else(|| Path::new(""));
|
|
206
|
+
loop {
|
|
207
|
+
for file_name in ["tsconfig.json", "jsconfig.json"] {
|
|
208
|
+
let candidate = super::normalize(current.join(file_name));
|
|
209
|
+
if repository_files.contains(&candidate) {
|
|
210
|
+
configs.push(candidate);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
let Some(parent) = current.parent() else {
|
|
214
|
+
break;
|
|
215
|
+
};
|
|
216
|
+
if parent == current {
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
current = parent;
|
|
220
|
+
}
|
|
221
|
+
configs
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
fn alias_capture(pattern: &str, specifier: &str) -> Option<String> {
|
|
225
|
+
let Some((prefix, suffix)) = pattern.split_once('*') else {
|
|
226
|
+
return (pattern == specifier).then(String::new);
|
|
227
|
+
};
|
|
228
|
+
Some(
|
|
229
|
+
specifier
|
|
230
|
+
.strip_prefix(prefix)?
|
|
231
|
+
.strip_suffix(suffix)?
|
|
232
|
+
.to_string(),
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
fn alias_specificity(pattern: &str) -> usize {
|
|
237
|
+
pattern
|
|
238
|
+
.split_once('*')
|
|
239
|
+
.map(|(prefix, _)| prefix.len())
|
|
240
|
+
.unwrap_or(pattern.len())
|
|
241
|
+
}
|