@lamentis/naome 1.3.12 → 1.3.14
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 +1 -1
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/architecture/output.rs +26 -4
- package/crates/naome-core/src/architecture/rules/external.rs +59 -0
- package/crates/naome-core/src/architecture/rules.rs +2 -0
- package/crates/naome-core/src/architecture/scan/cache.rs +145 -0
- package/crates/naome-core/src/architecture/scan/graph_builder.rs +159 -38
- package/crates/naome-core/src/architecture/scan/imports/extractors/swift.rs +48 -0
- package/crates/naome-core/src/architecture/scan/imports/extractors.rs +3 -0
- package/crates/naome-core/src/architecture/scan/imports/resolver.rs +41 -1
- package/crates/naome-core/src/architecture/scan/imports.rs +1 -0
- package/crates/naome-core/src/architecture/scan/manifest/common.rs +102 -0
- package/crates/naome-core/src/architecture/scan/manifest/parsers/json.rs +46 -0
- package/crates/naome-core/src/architecture/scan/manifest/parsers/other.rs +280 -0
- package/crates/naome-core/src/architecture/scan/manifest/parsers/toml.rs +184 -0
- package/crates/naome-core/src/architecture/scan/manifest/parsers.rs +3 -0
- package/crates/naome-core/src/architecture/scan/manifest.rs +33 -0
- package/crates/naome-core/src/architecture/scan.rs +254 -10
- package/crates/naome-core/tests/architecture_cache.rs +212 -0
- package/crates/naome-core/tests/architecture_manifests.rs +289 -0
- package/crates/naome-core/tests/architecture_support/mod.rs +2 -0
- package/crates/naome-core/tests/architecture_swift.rs +111 -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/manifest.json +2 -2
- package/templates/naome-root/docs/naome/architecture-fitness.md +44 -13
package/Cargo.lock
CHANGED
|
@@ -76,7 +76,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
|
|
76
76
|
|
|
77
77
|
[[package]]
|
|
78
78
|
name = "naome-cli"
|
|
79
|
-
version = "1.3.
|
|
79
|
+
version = "1.3.14"
|
|
80
80
|
dependencies = [
|
|
81
81
|
"naome-core",
|
|
82
82
|
"serde_json",
|
|
@@ -84,7 +84,7 @@ dependencies = [
|
|
|
84
84
|
|
|
85
85
|
[[package]]
|
|
86
86
|
name = "naome-core"
|
|
87
|
-
version = "1.3.
|
|
87
|
+
version = "1.3.14"
|
|
88
88
|
dependencies = [
|
|
89
89
|
"serde",
|
|
90
90
|
"serde_json",
|
|
@@ -53,7 +53,7 @@ fn run_arch_explain(root: &Path, args: &[String]) -> Result<(), Box<dyn std::err
|
|
|
53
53
|
"layers": scan.config.layers.keys().collect::<Vec<_>>(),
|
|
54
54
|
"contexts": scan.config.contexts.keys().collect::<Vec<_>>(),
|
|
55
55
|
"rules": ARCHITECTURE_RULE_IDS,
|
|
56
|
-
"extractors": ["path", "typescript", "javascript", "rust", "python", "go"]
|
|
56
|
+
"extractors": ["path", "typescript", "javascript", "rust", "python", "go", "swift"]
|
|
57
57
|
}))?
|
|
58
58
|
);
|
|
59
59
|
} else {
|
|
@@ -54,6 +54,8 @@ pub struct ArchitectureValidation {
|
|
|
54
54
|
pub rules_executed: Vec<String>,
|
|
55
55
|
pub changed_only_requested: bool,
|
|
56
56
|
pub changed_only_degraded_to_full_scan: bool,
|
|
57
|
+
pub changed_only_mode: String,
|
|
58
|
+
pub changed_only_degradation_reason: Option<String>,
|
|
57
59
|
pub violations: Vec<ArchitectureViolation>,
|
|
58
60
|
#[serde(rename = "agent_feedback")]
|
|
59
61
|
pub agent_feedback: Vec<ArchitectureAgentFeedback>,
|
|
@@ -134,7 +136,15 @@ pub fn format_architecture_validation(report: &ArchitectureValidation) -> String
|
|
|
134
136
|
];
|
|
135
137
|
|
|
136
138
|
if report.changed_only_degraded_to_full_scan {
|
|
137
|
-
|
|
139
|
+
let reason = report
|
|
140
|
+
.changed_only_degradation_reason
|
|
141
|
+
.as_deref()
|
|
142
|
+
.unwrap_or("soundness");
|
|
143
|
+
lines.push(format!(
|
|
144
|
+
"changed-only requested: degraded to full scan for soundness ({reason})"
|
|
145
|
+
));
|
|
146
|
+
} else if report.changed_only_requested {
|
|
147
|
+
lines.push("changed-only requested: using incremental architecture cache".to_string());
|
|
138
148
|
}
|
|
139
149
|
|
|
140
150
|
for violation in report.violations.iter().take(10) {
|
|
@@ -152,12 +162,24 @@ pub fn format_architecture_validation(report: &ArchitectureValidation) -> String
|
|
|
152
162
|
}
|
|
153
163
|
|
|
154
164
|
pub fn format_architecture_scan(report: &ArchitectureScanReport) -> String {
|
|
155
|
-
format!(
|
|
165
|
+
let mut output = format!(
|
|
156
166
|
"NAOME architecture scan\nfiles scanned: {}\ngraph nodes: {}\ngraph edges: {}\n",
|
|
157
167
|
report.files_scanned,
|
|
158
168
|
report.graph.nodes.len(),
|
|
159
169
|
report.graph.edges.len()
|
|
160
|
-
)
|
|
170
|
+
);
|
|
171
|
+
if report.changed_only_degraded_to_full_scan {
|
|
172
|
+
let reason = report
|
|
173
|
+
.changed_only_degradation_reason
|
|
174
|
+
.as_deref()
|
|
175
|
+
.unwrap_or("soundness");
|
|
176
|
+
output.push_str(&format!(
|
|
177
|
+
"changed-only requested: degraded to full scan for soundness ({reason})\n"
|
|
178
|
+
));
|
|
179
|
+
} else if report.changed_only_requested {
|
|
180
|
+
output.push_str("changed-only requested: using incremental architecture cache\n");
|
|
181
|
+
}
|
|
182
|
+
output
|
|
161
183
|
}
|
|
162
184
|
|
|
163
185
|
pub fn format_architecture_explain(scan: &ArchitectureScanReport) -> String {
|
|
@@ -176,7 +198,7 @@ pub fn format_architecture_explain(scan: &ArchitectureScanReport) -> String {
|
|
|
176
198
|
.collect::<Vec<_>>()
|
|
177
199
|
.join(", ");
|
|
178
200
|
format!(
|
|
179
|
-
"NAOME Architecture Fitness\nrules: {}\nlayers: {}\ncontexts: {}\npath extractor: enabled for every repository\nimport extractors: typescript, javascript, rust, python, go\n",
|
|
201
|
+
"NAOME Architecture Fitness\nrules: {}\nlayers: {}\ncontexts: {}\npath extractor: enabled for every repository\nimport extractors: typescript, javascript, rust, python, go, swift\n",
|
|
180
202
|
ARCHITECTURE_RULE_IDS.join(", "),
|
|
181
203
|
empty_label(&layers),
|
|
182
204
|
empty_label(&contexts)
|
|
@@ -73,6 +73,7 @@ fn is_standard_library_dependency(language: Option<&str>, package: &str) -> bool
|
|
|
73
73
|
Some("javascript") | Some("typescript") => NODE_BUILTINS.contains(&root),
|
|
74
74
|
Some("python") => PYTHON_STDLIB.contains(&root),
|
|
75
75
|
Some("rust") => matches!(root, "std" | "core" | "alloc" | "proc_macro" | "test"),
|
|
76
|
+
Some("swift") => SWIFT_APPLE_FRAMEWORKS.contains(&root),
|
|
76
77
|
_ => false,
|
|
77
78
|
}
|
|
78
79
|
}
|
|
@@ -183,3 +184,61 @@ const PYTHON_STDLIB: &[&str] = &[
|
|
|
183
184
|
"xml",
|
|
184
185
|
"zipfile",
|
|
185
186
|
];
|
|
187
|
+
|
|
188
|
+
const SWIFT_APPLE_FRAMEWORKS: &[&str] = &[
|
|
189
|
+
"Accelerate",
|
|
190
|
+
"AppIntents",
|
|
191
|
+
"AppKit",
|
|
192
|
+
"ARKit",
|
|
193
|
+
"AVFoundation",
|
|
194
|
+
"CloudKit",
|
|
195
|
+
"Combine",
|
|
196
|
+
"CoreData",
|
|
197
|
+
"CoreFoundation",
|
|
198
|
+
"CoreBluetooth",
|
|
199
|
+
"CoreGraphics",
|
|
200
|
+
"CoreImage",
|
|
201
|
+
"CoreLocation",
|
|
202
|
+
"CoreML",
|
|
203
|
+
"CoreMotion",
|
|
204
|
+
"CoreNFC",
|
|
205
|
+
"CoreSpotlight",
|
|
206
|
+
"CoreTelephony",
|
|
207
|
+
"CoreText",
|
|
208
|
+
"CryptoKit",
|
|
209
|
+
"Dispatch",
|
|
210
|
+
"EventKit",
|
|
211
|
+
"Foundation",
|
|
212
|
+
"GameKit",
|
|
213
|
+
"HealthKit",
|
|
214
|
+
"LocalAuthentication",
|
|
215
|
+
"MapKit",
|
|
216
|
+
"MessageUI",
|
|
217
|
+
"Metal",
|
|
218
|
+
"MetalKit",
|
|
219
|
+
"NaturalLanguage",
|
|
220
|
+
"Network",
|
|
221
|
+
"Observation",
|
|
222
|
+
"PassKit",
|
|
223
|
+
"PencilKit",
|
|
224
|
+
"Photos",
|
|
225
|
+
"QuartzCore",
|
|
226
|
+
"RealityKit",
|
|
227
|
+
"SafariServices",
|
|
228
|
+
"SceneKit",
|
|
229
|
+
"Security",
|
|
230
|
+
"SpriteKit",
|
|
231
|
+
"StoreKit",
|
|
232
|
+
"Swift",
|
|
233
|
+
"SwiftData",
|
|
234
|
+
"SwiftUI",
|
|
235
|
+
"ThreadNetwork",
|
|
236
|
+
"UIKit",
|
|
237
|
+
"UniformTypeIdentifiers",
|
|
238
|
+
"UserNotifications",
|
|
239
|
+
"Vision",
|
|
240
|
+
"WatchKit",
|
|
241
|
+
"WebKit",
|
|
242
|
+
"WidgetKit",
|
|
243
|
+
"XCTest",
|
|
244
|
+
];
|
|
@@ -55,6 +55,8 @@ pub fn validate_scan(scan: ArchitectureScanReport) -> ArchitectureValidation {
|
|
|
55
55
|
rules_executed,
|
|
56
56
|
changed_only_requested: scan.changed_only_requested,
|
|
57
57
|
changed_only_degraded_to_full_scan: scan.changed_only_degraded_to_full_scan,
|
|
58
|
+
changed_only_mode: scan.changed_only_mode,
|
|
59
|
+
changed_only_degradation_reason: scan.changed_only_degradation_reason,
|
|
58
60
|
violations,
|
|
59
61
|
agent_feedback,
|
|
60
62
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
2
|
+
use std::fs;
|
|
3
|
+
use std::path::Path;
|
|
4
|
+
|
|
5
|
+
use serde::{Deserialize, Serialize};
|
|
6
|
+
use sha2::{Digest, Sha256};
|
|
7
|
+
|
|
8
|
+
use super::FileFact;
|
|
9
|
+
use crate::models::NaomeError;
|
|
10
|
+
|
|
11
|
+
const CACHE_SCHEMA: &str = "naome.architecture-cache.v1";
|
|
12
|
+
const EXTRACTOR_VERSION: &str = "architecture-cache-v1.3.14";
|
|
13
|
+
const CACHE_PATH: &str = ".naome/cache/architecture/cache.json";
|
|
14
|
+
|
|
15
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
16
|
+
#[serde(rename_all = "camelCase")]
|
|
17
|
+
pub(super) struct ArchitectureCache {
|
|
18
|
+
pub schema: String,
|
|
19
|
+
pub extractor_version: String,
|
|
20
|
+
pub config_hash: String,
|
|
21
|
+
pub file_list_hash: String,
|
|
22
|
+
pub files: BTreeMap<String, CachedFileFact>,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
26
|
+
#[serde(rename_all = "camelCase")]
|
|
27
|
+
pub(super) struct CachedFileFact {
|
|
28
|
+
pub content_hash: String,
|
|
29
|
+
pub fact: FileFact,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl ArchitectureCache {
|
|
33
|
+
pub(super) fn is_compatible(&self, config_hash: &str, file_list_hash: &str) -> CacheStatus {
|
|
34
|
+
if self.schema != CACHE_SCHEMA || self.extractor_version != EXTRACTOR_VERSION {
|
|
35
|
+
return CacheStatus::Miss("cache_miss");
|
|
36
|
+
}
|
|
37
|
+
if self.config_hash != config_hash {
|
|
38
|
+
return CacheStatus::Miss("config_changed");
|
|
39
|
+
}
|
|
40
|
+
if self.file_list_hash != file_list_hash {
|
|
41
|
+
return CacheStatus::Miss("file_set_changed");
|
|
42
|
+
}
|
|
43
|
+
CacheStatus::Hit
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
48
|
+
pub(super) enum CacheStatus {
|
|
49
|
+
Hit,
|
|
50
|
+
Miss(&'static str),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
pub(super) fn read(root: &Path) -> Option<ArchitectureCache> {
|
|
54
|
+
let content = fs::read_to_string(root.join(CACHE_PATH)).ok()?;
|
|
55
|
+
serde_json::from_str(&content).ok()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
pub(super) fn write(
|
|
59
|
+
root: &Path,
|
|
60
|
+
config_hash: String,
|
|
61
|
+
file_list_hash: String,
|
|
62
|
+
file_facts: &BTreeMap<String, FileFact>,
|
|
63
|
+
) -> Result<(), NaomeError> {
|
|
64
|
+
let cache = ArchitectureCache {
|
|
65
|
+
schema: CACHE_SCHEMA.to_string(),
|
|
66
|
+
extractor_version: EXTRACTOR_VERSION.to_string(),
|
|
67
|
+
config_hash,
|
|
68
|
+
file_list_hash,
|
|
69
|
+
files: file_facts
|
|
70
|
+
.iter()
|
|
71
|
+
.filter_map(|(path, fact)| {
|
|
72
|
+
let content_hash = content_hash(root, path).ok()?;
|
|
73
|
+
Some((
|
|
74
|
+
path.clone(),
|
|
75
|
+
CachedFileFact {
|
|
76
|
+
content_hash,
|
|
77
|
+
fact: fact.clone(),
|
|
78
|
+
},
|
|
79
|
+
))
|
|
80
|
+
})
|
|
81
|
+
.collect(),
|
|
82
|
+
};
|
|
83
|
+
let path = root.join(CACHE_PATH);
|
|
84
|
+
if let Some(parent) = path.parent() {
|
|
85
|
+
fs::create_dir_all(parent)?;
|
|
86
|
+
}
|
|
87
|
+
fs::write(path, serde_json::to_string_pretty(&cache)?)?;
|
|
88
|
+
Ok(())
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
pub(super) fn config_hash(
|
|
92
|
+
root: &Path,
|
|
93
|
+
explicit_path: Option<&Path>,
|
|
94
|
+
default_content: &str,
|
|
95
|
+
) -> Result<String, NaomeError> {
|
|
96
|
+
let path = explicit_path
|
|
97
|
+
.map(Path::to_path_buf)
|
|
98
|
+
.unwrap_or_else(|| root.join("naome.arch.yaml"));
|
|
99
|
+
let source = if path.exists() {
|
|
100
|
+
fs::read_to_string(&path)?
|
|
101
|
+
} else {
|
|
102
|
+
default_content.to_string()
|
|
103
|
+
};
|
|
104
|
+
let label = display_path(root, &path);
|
|
105
|
+
Ok(stable_hash([
|
|
106
|
+
b"architecture-config-v1".as_slice(),
|
|
107
|
+
label.as_bytes(),
|
|
108
|
+
source.as_bytes(),
|
|
109
|
+
]))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
pub(super) fn file_list_hash(files: &[String]) -> String {
|
|
113
|
+
let mut hasher = Sha256::new();
|
|
114
|
+
hasher.update(b"architecture-file-list-v1");
|
|
115
|
+
for path in files {
|
|
116
|
+
hasher.update([0]);
|
|
117
|
+
hasher.update(path.as_bytes());
|
|
118
|
+
}
|
|
119
|
+
format!("sha256:{:x}", hasher.finalize())
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
pub(super) fn content_hash(root: &Path, path: &str) -> Result<String, NaomeError> {
|
|
123
|
+
let bytes = fs::read(root.join(path))?;
|
|
124
|
+
Ok(stable_hash([b"architecture-file-v1".as_slice(), &bytes]))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
pub(super) fn changed_set(changed_paths: &[String]) -> BTreeSet<String> {
|
|
128
|
+
changed_paths.iter().cloned().collect()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fn stable_hash<'a>(parts: impl IntoIterator<Item = &'a [u8]>) -> String {
|
|
132
|
+
let mut hasher = Sha256::new();
|
|
133
|
+
for part in parts {
|
|
134
|
+
hasher.update(part);
|
|
135
|
+
hasher.update([0]);
|
|
136
|
+
}
|
|
137
|
+
format!("sha256:{:x}", hasher.finalize())
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fn display_path(root: &Path, path: &Path) -> String {
|
|
141
|
+
path.strip_prefix(root)
|
|
142
|
+
.unwrap_or(path)
|
|
143
|
+
.to_string_lossy()
|
|
144
|
+
.replace('\\', "/")
|
|
145
|
+
}
|
|
@@ -5,7 +5,7 @@ use std::path::Path;
|
|
|
5
5
|
use serde_json::json;
|
|
6
6
|
|
|
7
7
|
use super::imports;
|
|
8
|
-
use super::FileFact;
|
|
8
|
+
use super::{FileFact, ManifestFact};
|
|
9
9
|
use crate::architecture::config::ArchitectureConfig;
|
|
10
10
|
use crate::architecture::model::{ArchitectureEdgeKind, ArchitectureGraph, ArchitectureNodeKind};
|
|
11
11
|
|
|
@@ -16,24 +16,48 @@ pub(super) fn build_path_graph(
|
|
|
16
16
|
root: &Path,
|
|
17
17
|
files: Vec<String>,
|
|
18
18
|
config: &ArchitectureConfig,
|
|
19
|
+
manifests: &[ManifestFact],
|
|
19
20
|
) -> (ArchitectureGraph, BTreeMap<String, FileFact>) {
|
|
20
|
-
let mut graph = ArchitectureGraph::default();
|
|
21
21
|
let mut file_facts = BTreeMap::new();
|
|
22
22
|
let file_set = files.iter().cloned().collect::<BTreeSet<_>>();
|
|
23
|
+
for path in &files {
|
|
24
|
+
file_facts.insert(path.clone(), scan_file_fact(root, path, config, &file_set));
|
|
25
|
+
}
|
|
26
|
+
let graph = build_graph_from_facts(&files, &file_facts, config, manifests);
|
|
27
|
+
(graph, file_facts)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
pub(super) fn scan_file_fact(
|
|
31
|
+
root: &Path,
|
|
32
|
+
path: &str,
|
|
33
|
+
config: &ArchitectureConfig,
|
|
34
|
+
file_set: &BTreeSet<String>,
|
|
35
|
+
) -> FileFact {
|
|
36
|
+
let content = fs::read_to_string(root.join(path)).unwrap_or_default();
|
|
37
|
+
let mut fact = facts::file_fact(path, &content, config);
|
|
38
|
+
fact.imports = imports::extract_imports(root, path, &content, file_set);
|
|
39
|
+
fact
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pub(super) fn build_graph_from_facts(
|
|
43
|
+
files: &[String],
|
|
44
|
+
file_facts: &BTreeMap<String, FileFact>,
|
|
45
|
+
config: &ArchitectureConfig,
|
|
46
|
+
manifests: &[ManifestFact],
|
|
47
|
+
) -> ArchitectureGraph {
|
|
48
|
+
let mut graph = ArchitectureGraph::default();
|
|
49
|
+
let mut emitted_external_nodes = BTreeSet::new();
|
|
23
50
|
|
|
24
51
|
push_repository_and_policy_nodes(&mut graph, config);
|
|
25
|
-
push_directories(&mut graph,
|
|
52
|
+
push_directories(&mut graph, files);
|
|
53
|
+
push_manifests(&mut graph, manifests, &mut emitted_external_nodes);
|
|
26
54
|
|
|
27
|
-
for
|
|
28
|
-
let content = fs::read_to_string(root.join(&path)).unwrap_or_default();
|
|
29
|
-
let mut fact = facts::file_fact(&path, &content, config);
|
|
30
|
-
fact.imports = imports::extract_imports(root, &path, &content, &file_set);
|
|
55
|
+
for fact in file_facts.values() {
|
|
31
56
|
push_file(&mut graph, &fact);
|
|
32
|
-
file_facts.insert(path, fact);
|
|
33
57
|
}
|
|
34
58
|
|
|
35
|
-
push_imports(&mut graph, &
|
|
36
|
-
|
|
59
|
+
push_imports(&mut graph, file_facts, &mut emitted_external_nodes);
|
|
60
|
+
graph
|
|
37
61
|
}
|
|
38
62
|
|
|
39
63
|
fn push_repository_and_policy_nodes(graph: &mut ArchitectureGraph, config: &ArchitectureConfig) {
|
|
@@ -121,44 +145,117 @@ fn push_file(graph: &mut ArchitectureGraph, fact: &FileFact) {
|
|
|
121
145
|
push_membership_edges(graph, path, "context", &fact.contexts);
|
|
122
146
|
}
|
|
123
147
|
|
|
124
|
-
fn
|
|
125
|
-
|
|
148
|
+
fn push_manifests(
|
|
149
|
+
graph: &mut ArchitectureGraph,
|
|
150
|
+
manifests: &[ManifestFact],
|
|
151
|
+
emitted_external_nodes: &mut BTreeSet<String>,
|
|
152
|
+
) {
|
|
153
|
+
for manifest in manifests {
|
|
154
|
+
let package_id = manifest_package_id(manifest);
|
|
155
|
+
emit::push_node_with_metadata(
|
|
156
|
+
graph,
|
|
157
|
+
&package_id,
|
|
158
|
+
ArchitectureNodeKind::Package,
|
|
159
|
+
&manifest.package_name,
|
|
160
|
+
Some(manifest.path.clone()),
|
|
161
|
+
None,
|
|
162
|
+
manifest.confidence,
|
|
163
|
+
&manifest.extractor,
|
|
164
|
+
json!({
|
|
165
|
+
"path": manifest.path,
|
|
166
|
+
"ecosystem": manifest.ecosystem,
|
|
167
|
+
"packageName": manifest.package_name
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
emit::push_edge(
|
|
171
|
+
graph,
|
|
172
|
+
facts::parent_node_id(&manifest.path)
|
|
173
|
+
.as_deref()
|
|
174
|
+
.unwrap_or("repository:."),
|
|
175
|
+
&package_id,
|
|
176
|
+
ArchitectureEdgeKind::Contains,
|
|
177
|
+
"contains",
|
|
178
|
+
Some(manifest.path.clone()),
|
|
179
|
+
);
|
|
180
|
+
for dependency in &manifest.dependencies {
|
|
181
|
+
let external_id = format!("external:{}", dependency.name);
|
|
182
|
+
push_external_node(
|
|
183
|
+
graph,
|
|
184
|
+
emitted_external_nodes,
|
|
185
|
+
&external_id,
|
|
186
|
+
&dependency.name,
|
|
187
|
+
dependency.confidence,
|
|
188
|
+
&manifest.extractor,
|
|
189
|
+
json!({
|
|
190
|
+
"manifestPath": manifest.path,
|
|
191
|
+
"ecosystem": manifest.ecosystem,
|
|
192
|
+
"dependencyKind": dependency.dependency_kind,
|
|
193
|
+
"version": dependency.version
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
emit::push_edge_with_metadata(
|
|
197
|
+
graph,
|
|
198
|
+
&package_id,
|
|
199
|
+
&external_id,
|
|
200
|
+
ArchitectureEdgeKind::DependsOn,
|
|
201
|
+
"depends_on",
|
|
202
|
+
Some(manifest.path.clone()),
|
|
203
|
+
None,
|
|
204
|
+
None,
|
|
205
|
+
dependency.confidence,
|
|
206
|
+
&manifest.extractor,
|
|
207
|
+
json!({
|
|
208
|
+
"specifier": dependency.name,
|
|
209
|
+
"manifestPath": manifest.path,
|
|
210
|
+
"dependencyKind": dependency.dependency_kind,
|
|
211
|
+
"version": dependency.version
|
|
212
|
+
}),
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
fn manifest_package_id(manifest: &ManifestFact) -> String {
|
|
219
|
+
format!(
|
|
220
|
+
"package:{}:{}",
|
|
221
|
+
stable_fragment(&manifest.ecosystem),
|
|
222
|
+
stable_fragment(&manifest.path)
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
fn push_imports(
|
|
227
|
+
graph: &mut ArchitectureGraph,
|
|
228
|
+
file_facts: &BTreeMap<String, FileFact>,
|
|
229
|
+
emitted_external_nodes: &mut BTreeSet<String>,
|
|
230
|
+
) {
|
|
126
231
|
for fact in file_facts.values() {
|
|
127
232
|
for import in &fact.imports {
|
|
128
233
|
let target_id = match &import.target {
|
|
129
234
|
super::ImportTarget::File(path) => format!("file:{path}"),
|
|
130
235
|
super::ImportTarget::ExternalDependency(name) => {
|
|
131
236
|
let id = format!("external:{name}");
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
&import.extractor,
|
|
142
|
-
json!({ "specifier": import.specifier, "resolvedAs": "external" }),
|
|
143
|
-
);
|
|
144
|
-
}
|
|
237
|
+
push_external_node(
|
|
238
|
+
graph,
|
|
239
|
+
emitted_external_nodes,
|
|
240
|
+
&id,
|
|
241
|
+
name,
|
|
242
|
+
import.confidence,
|
|
243
|
+
&import.extractor,
|
|
244
|
+
json!({ "specifier": import.specifier, "resolvedAs": "external" }),
|
|
245
|
+
);
|
|
145
246
|
id
|
|
146
247
|
}
|
|
147
248
|
super::ImportTarget::Unknown(specifier) => {
|
|
148
249
|
let id = format!("unknown-import:{}", stable_fragment(specifier));
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
&import.extractor,
|
|
159
|
-
json!({ "specifier": specifier, "resolvedAs": "unknown" }),
|
|
160
|
-
);
|
|
161
|
-
}
|
|
250
|
+
push_external_node(
|
|
251
|
+
graph,
|
|
252
|
+
emitted_external_nodes,
|
|
253
|
+
&id,
|
|
254
|
+
specifier,
|
|
255
|
+
import.confidence,
|
|
256
|
+
&import.extractor,
|
|
257
|
+
json!({ "specifier": specifier, "resolvedAs": "unknown" }),
|
|
258
|
+
);
|
|
162
259
|
id
|
|
163
260
|
}
|
|
164
261
|
};
|
|
@@ -179,6 +276,30 @@ fn push_imports(graph: &mut ArchitectureGraph, file_facts: &BTreeMap<String, Fil
|
|
|
179
276
|
}
|
|
180
277
|
}
|
|
181
278
|
|
|
279
|
+
fn push_external_node(
|
|
280
|
+
graph: &mut ArchitectureGraph,
|
|
281
|
+
emitted_external_nodes: &mut BTreeSet<String>,
|
|
282
|
+
id: &str,
|
|
283
|
+
label: &str,
|
|
284
|
+
confidence: f32,
|
|
285
|
+
extractor: &str,
|
|
286
|
+
raw_origin: serde_json::Value,
|
|
287
|
+
) {
|
|
288
|
+
if emitted_external_nodes.insert(id.to_string()) {
|
|
289
|
+
emit::push_node_with_metadata(
|
|
290
|
+
graph,
|
|
291
|
+
id,
|
|
292
|
+
ArchitectureNodeKind::ExternalDependency,
|
|
293
|
+
label,
|
|
294
|
+
None,
|
|
295
|
+
None,
|
|
296
|
+
confidence,
|
|
297
|
+
extractor,
|
|
298
|
+
raw_origin,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
182
303
|
fn push_membership_edges(
|
|
183
304
|
graph: &mut ArchitectureGraph,
|
|
184
305
|
path: &str,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
use super::{is_comment_only, raw, RawImport};
|
|
2
|
+
|
|
3
|
+
pub(super) fn extract(content: &str) -> Vec<RawImport> {
|
|
4
|
+
let mut imports = Vec::new();
|
|
5
|
+
for (index, line) in content.lines().enumerate() {
|
|
6
|
+
let trimmed = line.trim();
|
|
7
|
+
if is_comment_only(trimmed) {
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
let Some(import_value) = swift_import_value(trimmed) else {
|
|
11
|
+
continue;
|
|
12
|
+
};
|
|
13
|
+
if let Some(specifier) = swift_import_specifier(import_value) {
|
|
14
|
+
imports.push(raw(specifier, index, line, 0.88, "import:swift"));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
imports
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
fn swift_import_value(line: &str) -> Option<&str> {
|
|
21
|
+
let mut remaining = line.trim();
|
|
22
|
+
while remaining.starts_with('@') {
|
|
23
|
+
let Some((_, after_attribute)) = remaining.split_once(' ') else {
|
|
24
|
+
return None;
|
|
25
|
+
};
|
|
26
|
+
remaining = after_attribute.trim_start();
|
|
27
|
+
}
|
|
28
|
+
remaining.strip_prefix("import ")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fn swift_import_specifier(value: &str) -> Option<String> {
|
|
32
|
+
let mut parts = value.split_whitespace();
|
|
33
|
+
let first = parts.next()?;
|
|
34
|
+
let module = if matches!(
|
|
35
|
+
first,
|
|
36
|
+
"class" | "struct" | "enum" | "protocol" | "func" | "var" | "typealias"
|
|
37
|
+
) {
|
|
38
|
+
parts.next()?
|
|
39
|
+
} else {
|
|
40
|
+
first
|
|
41
|
+
};
|
|
42
|
+
module
|
|
43
|
+
.split('.')
|
|
44
|
+
.next()
|
|
45
|
+
.map(str::trim)
|
|
46
|
+
.filter(|value| !value.is_empty())
|
|
47
|
+
.map(ToString::to_string)
|
|
48
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
use super::language_for_path;
|
|
2
2
|
use crate::architecture::model::SourceRange;
|
|
3
3
|
|
|
4
|
+
mod swift;
|
|
5
|
+
|
|
4
6
|
#[derive(Debug, Clone)]
|
|
5
7
|
pub(super) struct RawImport {
|
|
6
8
|
pub specifier: String,
|
|
@@ -15,6 +17,7 @@ pub(super) fn extract_raw_imports(path: &str, content: &str) -> Vec<RawImport> {
|
|
|
15
17
|
Some("rust") => extract_rust(content),
|
|
16
18
|
Some("python") => extract_python(content),
|
|
17
19
|
Some("go") => extract_go(content),
|
|
20
|
+
Some("swift") => swift::extract(content),
|
|
18
21
|
_ => Vec::new(),
|
|
19
22
|
}
|
|
20
23
|
}
|