@lamentis/naome 1.3.11 → 1.3.13

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.
Files changed (41) hide show
  1. package/Cargo.lock +2 -2
  2. package/crates/naome-cli/Cargo.toml +1 -1
  3. package/crates/naome-cli/src/architecture_commands.rs +2 -6
  4. package/crates/naome-cli/tests/architecture_cli.rs +60 -0
  5. package/crates/naome-core/Cargo.toml +1 -1
  6. package/crates/naome-core/src/architecture/config/parser/sections.rs +44 -1
  7. package/crates/naome-core/src/architecture/config/parser.rs +1 -0
  8. package/crates/naome-core/src/architecture/config.rs +35 -0
  9. package/crates/naome-core/src/architecture/output.rs +15 -1
  10. package/crates/naome-core/src/architecture/rules/budgets.rs +179 -0
  11. package/crates/naome-core/src/architecture/rules/context.rs +138 -0
  12. package/crates/naome-core/src/architecture/rules/cycles.rs +39 -0
  13. package/crates/naome-core/src/architecture/rules/external.rs +244 -0
  14. package/crates/naome-core/src/architecture/rules/graph.rs +177 -0
  15. package/crates/naome-core/src/architecture/rules/transitive.rs +89 -0
  16. package/crates/naome-core/src/architecture/rules.rs +13 -39
  17. package/crates/naome-core/src/architecture/scan/graph_builder.rs +130 -30
  18. package/crates/naome-core/src/architecture/scan/imports/extractors/swift.rs +48 -0
  19. package/crates/naome-core/src/architecture/scan/imports/extractors.rs +7 -7
  20. package/crates/naome-core/src/architecture/scan/imports/resolver.rs +44 -22
  21. package/crates/naome-core/src/architecture/scan/imports.rs +17 -0
  22. package/crates/naome-core/src/architecture/scan/manifest/common.rs +102 -0
  23. package/crates/naome-core/src/architecture/scan/manifest/parsers/json.rs +46 -0
  24. package/crates/naome-core/src/architecture/scan/manifest/parsers/other.rs +280 -0
  25. package/crates/naome-core/src/architecture/scan/manifest/parsers/toml.rs +184 -0
  26. package/crates/naome-core/src/architecture/scan/manifest/parsers.rs +3 -0
  27. package/crates/naome-core/src/architecture/scan/manifest.rs +33 -0
  28. package/crates/naome-core/src/architecture/scan.rs +27 -1
  29. package/crates/naome-core/src/architecture.rs +1 -1
  30. package/crates/naome-core/src/lib.rs +1 -0
  31. package/crates/naome-core/tests/architecture.rs +53 -85
  32. package/crates/naome-core/tests/architecture_manifests.rs +289 -0
  33. package/crates/naome-core/tests/architecture_rules.rs +498 -0
  34. package/crates/naome-core/tests/architecture_support/mod.rs +80 -0
  35. package/crates/naome-core/tests/architecture_swift.rs +111 -0
  36. package/installer/harness-files.js +3 -3
  37. package/native/darwin-arm64/naome +0 -0
  38. package/native/linux-x64/naome +0 -0
  39. package/package.json +1 -1
  40. package/templates/naome-root/.naome/manifest.json +2 -2
  41. package/templates/naome-root/docs/naome/architecture-fitness.md +61 -8
@@ -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,13 +16,16 @@ 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
21
  let mut graph = ArchitectureGraph::default();
21
22
  let mut file_facts = BTreeMap::new();
23
+ let mut emitted_external_nodes = BTreeSet::new();
22
24
  let file_set = files.iter().cloned().collect::<BTreeSet<_>>();
23
25
 
24
26
  push_repository_and_policy_nodes(&mut graph, config);
25
27
  push_directories(&mut graph, &files);
28
+ push_manifests(&mut graph, manifests, &mut emitted_external_nodes);
26
29
 
27
30
  for path in files {
28
31
  let content = fs::read_to_string(root.join(&path)).unwrap_or_default();
@@ -32,7 +35,7 @@ pub(super) fn build_path_graph(
32
35
  file_facts.insert(path, fact);
33
36
  }
34
37
 
35
- push_imports(&mut graph, &file_facts);
38
+ push_imports(&mut graph, &file_facts, &mut emitted_external_nodes);
36
39
  (graph, file_facts)
37
40
  }
38
41
 
@@ -121,44 +124,117 @@ fn push_file(graph: &mut ArchitectureGraph, fact: &FileFact) {
121
124
  push_membership_edges(graph, path, "context", &fact.contexts);
122
125
  }
123
126
 
124
- fn push_imports(graph: &mut ArchitectureGraph, file_facts: &BTreeMap<String, FileFact>) {
125
- let mut emitted_target_nodes = BTreeSet::new();
127
+ fn push_manifests(
128
+ graph: &mut ArchitectureGraph,
129
+ manifests: &[ManifestFact],
130
+ emitted_external_nodes: &mut BTreeSet<String>,
131
+ ) {
132
+ for manifest in manifests {
133
+ let package_id = manifest_package_id(manifest);
134
+ emit::push_node_with_metadata(
135
+ graph,
136
+ &package_id,
137
+ ArchitectureNodeKind::Package,
138
+ &manifest.package_name,
139
+ Some(manifest.path.clone()),
140
+ None,
141
+ manifest.confidence,
142
+ &manifest.extractor,
143
+ json!({
144
+ "path": manifest.path,
145
+ "ecosystem": manifest.ecosystem,
146
+ "packageName": manifest.package_name
147
+ }),
148
+ );
149
+ emit::push_edge(
150
+ graph,
151
+ facts::parent_node_id(&manifest.path)
152
+ .as_deref()
153
+ .unwrap_or("repository:."),
154
+ &package_id,
155
+ ArchitectureEdgeKind::Contains,
156
+ "contains",
157
+ Some(manifest.path.clone()),
158
+ );
159
+ for dependency in &manifest.dependencies {
160
+ let external_id = format!("external:{}", dependency.name);
161
+ push_external_node(
162
+ graph,
163
+ emitted_external_nodes,
164
+ &external_id,
165
+ &dependency.name,
166
+ dependency.confidence,
167
+ &manifest.extractor,
168
+ json!({
169
+ "manifestPath": manifest.path,
170
+ "ecosystem": manifest.ecosystem,
171
+ "dependencyKind": dependency.dependency_kind,
172
+ "version": dependency.version
173
+ }),
174
+ );
175
+ emit::push_edge_with_metadata(
176
+ graph,
177
+ &package_id,
178
+ &external_id,
179
+ ArchitectureEdgeKind::DependsOn,
180
+ "depends_on",
181
+ Some(manifest.path.clone()),
182
+ None,
183
+ None,
184
+ dependency.confidence,
185
+ &manifest.extractor,
186
+ json!({
187
+ "specifier": dependency.name,
188
+ "manifestPath": manifest.path,
189
+ "dependencyKind": dependency.dependency_kind,
190
+ "version": dependency.version
191
+ }),
192
+ );
193
+ }
194
+ }
195
+ }
196
+
197
+ fn manifest_package_id(manifest: &ManifestFact) -> String {
198
+ format!(
199
+ "package:{}:{}",
200
+ stable_fragment(&manifest.ecosystem),
201
+ stable_fragment(&manifest.path)
202
+ )
203
+ }
204
+
205
+ fn push_imports(
206
+ graph: &mut ArchitectureGraph,
207
+ file_facts: &BTreeMap<String, FileFact>,
208
+ emitted_external_nodes: &mut BTreeSet<String>,
209
+ ) {
126
210
  for fact in file_facts.values() {
127
211
  for import in &fact.imports {
128
212
  let target_id = match &import.target {
129
213
  super::ImportTarget::File(path) => format!("file:{path}"),
130
214
  super::ImportTarget::ExternalDependency(name) => {
131
215
  let id = format!("external:{name}");
132
- if emitted_target_nodes.insert(id.clone()) {
133
- emit::push_node_with_metadata(
134
- graph,
135
- &id,
136
- ArchitectureNodeKind::ExternalDependency,
137
- name,
138
- None,
139
- None,
140
- import.confidence,
141
- &import.extractor,
142
- json!({ "specifier": import.specifier, "resolvedAs": "external" }),
143
- );
144
- }
216
+ push_external_node(
217
+ graph,
218
+ emitted_external_nodes,
219
+ &id,
220
+ name,
221
+ import.confidence,
222
+ &import.extractor,
223
+ json!({ "specifier": import.specifier, "resolvedAs": "external" }),
224
+ );
145
225
  id
146
226
  }
147
227
  super::ImportTarget::Unknown(specifier) => {
148
228
  let id = format!("unknown-import:{}", stable_fragment(specifier));
149
- if emitted_target_nodes.insert(id.clone()) {
150
- emit::push_node_with_metadata(
151
- graph,
152
- &id,
153
- ArchitectureNodeKind::ExternalDependency,
154
- specifier,
155
- None,
156
- None,
157
- import.confidence,
158
- &import.extractor,
159
- json!({ "specifier": specifier, "resolvedAs": "unknown" }),
160
- );
161
- }
229
+ push_external_node(
230
+ graph,
231
+ emitted_external_nodes,
232
+ &id,
233
+ specifier,
234
+ import.confidence,
235
+ &import.extractor,
236
+ json!({ "specifier": specifier, "resolvedAs": "unknown" }),
237
+ );
162
238
  id
163
239
  }
164
240
  };
@@ -179,6 +255,30 @@ fn push_imports(graph: &mut ArchitectureGraph, file_facts: &BTreeMap<String, Fil
179
255
  }
180
256
  }
181
257
 
258
+ fn push_external_node(
259
+ graph: &mut ArchitectureGraph,
260
+ emitted_external_nodes: &mut BTreeSet<String>,
261
+ id: &str,
262
+ label: &str,
263
+ confidence: f32,
264
+ extractor: &str,
265
+ raw_origin: serde_json::Value,
266
+ ) {
267
+ if emitted_external_nodes.insert(id.to_string()) {
268
+ emit::push_node_with_metadata(
269
+ graph,
270
+ id,
271
+ ArchitectureNodeKind::ExternalDependency,
272
+ label,
273
+ None,
274
+ None,
275
+ confidence,
276
+ extractor,
277
+ raw_origin,
278
+ );
279
+ }
280
+ }
281
+
182
282
  fn push_membership_edges(
183
283
  graph: &mut ArchitectureGraph,
184
284
  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
  }
@@ -69,12 +72,6 @@ fn extract_rust(content: &str) -> Vec<RawImport> {
69
72
  .strip_prefix("use ")
70
73
  .or_else(|| rust_visible_use_value(trimmed))
71
74
  .map(rust_use_specifiers)
72
- .or_else(|| {
73
- trimmed
74
- .strip_prefix("mod ")
75
- .or_else(|| trimmed.strip_prefix("pub mod "))
76
- .map(|value| vec![format!("rustmod:{}", value.trim_end_matches(';').trim())])
77
- })
78
75
  .or_else(|| {
79
76
  trimmed
80
77
  .strip_prefix("extern crate ")
@@ -198,7 +195,10 @@ fn starts_javascript_pending_import(line: &str) -> bool {
198
195
  }
199
196
 
200
197
  fn is_comment_only(line: &str) -> bool {
201
- line.starts_with("//") || line.starts_with("/*") || line.starts_with('*') || line.starts_with('#')
198
+ line.starts_with("//")
199
+ || line.starts_with("/*")
200
+ || line.starts_with('*')
201
+ || line.starts_with('#')
202
202
  }
203
203
 
204
204
  fn rust_visible_use_value(line: &str) -> Option<&str> {
@@ -12,13 +12,6 @@ pub(super) fn resolve_import(
12
12
  repository_files: &BTreeSet<String>,
13
13
  ) -> ImportTarget {
14
14
  let language = language_for_path(from_path);
15
- if from_path.ends_with(".rs") {
16
- if let Some(module) = specifier.strip_prefix("rustmod:") {
17
- return resolve_rust_module_declaration(from_path, module, repository_files)
18
- .map(ImportTarget::File)
19
- .unwrap_or_else(|| ImportTarget::Unknown(specifier.to_string()));
20
- }
21
- }
22
15
  if specifier.starts_with('.') {
23
16
  if from_path.ends_with(".py") {
24
17
  return resolve_python_relative(from_path, specifier, repository_files)
@@ -42,6 +35,11 @@ pub(super) fn resolve_import(
42
35
  return ImportTarget::File(path);
43
36
  }
44
37
  }
38
+ if from_path.ends_with(".swift") {
39
+ if let Some(path) = resolve_swift_module_import(from_path, specifier, repository_files) {
40
+ return ImportTarget::File(path);
41
+ }
42
+ }
45
43
  external_target(specifier)
46
44
  }
47
45
 
@@ -155,19 +153,6 @@ fn resolve_rust_local(
155
153
  }
156
154
  }
157
155
 
158
- fn resolve_rust_module_declaration(
159
- from_path: &str,
160
- module: &str,
161
- repository_files: &BTreeSet<String>,
162
- ) -> Option<String> {
163
- let module = module.trim();
164
- if module.is_empty() || module.contains("::") || module.contains('/') {
165
- return None;
166
- }
167
- let module_base = normalize(Path::new(&rust_module_directory(from_path)).join(module));
168
- resolve_candidates(&module_base, repository_files, Some("rust"))
169
- }
170
-
171
156
  fn rust_module_directory(from_path: &str) -> String {
172
157
  let source = Path::new(from_path);
173
158
  let parent = source.parent().unwrap_or_else(|| Path::new(""));
@@ -217,7 +202,9 @@ fn go_module_for_file(
217
202
  from_path: &str,
218
203
  repository_files: &BTreeSet<String>,
219
204
  ) -> Option<String> {
220
- let mut current = Path::new(from_path).parent().unwrap_or_else(|| Path::new(""));
205
+ let mut current = Path::new(from_path)
206
+ .parent()
207
+ .unwrap_or_else(|| Path::new(""));
221
208
  loop {
222
209
  let go_mod_path = normalize(current.join("go.mod"));
223
210
  if repository_files.contains(&go_mod_path) {
@@ -243,6 +230,40 @@ fn go_mod_module(root: &Path, go_mod_path: &str) -> Option<String> {
243
230
  })
244
231
  }
245
232
 
233
+ fn resolve_swift_module_import(
234
+ from_path: &str,
235
+ specifier: &str,
236
+ repository_files: &BTreeSet<String>,
237
+ ) -> Option<String> {
238
+ if specifier.contains('/') || specifier.contains('.') {
239
+ return None;
240
+ }
241
+ let sources_root = swift_sources_root(from_path)?;
242
+ let target_prefix = format!("{sources_root}{specifier}/");
243
+ let conventional_file = format!("{target_prefix}{specifier}.swift");
244
+ if repository_files.contains(&conventional_file) {
245
+ return Some(conventional_file);
246
+ }
247
+ repository_files
248
+ .iter()
249
+ .find(|path| path.starts_with(&target_prefix) && path.ends_with(".swift"))
250
+ .cloned()
251
+ }
252
+
253
+ fn swift_sources_root(from_path: &str) -> Option<String> {
254
+ if from_path.starts_with("Sources/") {
255
+ return Some("Sources/".to_string());
256
+ }
257
+ if from_path.starts_with("Tests/") {
258
+ return Some("Sources/".to_string());
259
+ }
260
+ if let Some((prefix, _)) = from_path.split_once("/Sources/") {
261
+ return Some(format!("{prefix}/Sources/"));
262
+ }
263
+ let (prefix, _) = from_path.split_once("/Tests/")?;
264
+ Some(format!("{prefix}/Sources/"))
265
+ }
266
+
246
267
  fn resolve_candidates(
247
268
  candidate: &str,
248
269
  repository_files: &BTreeSet<String>,
@@ -304,10 +325,11 @@ fn extensions_for_language(language: Option<&str>) -> &'static [&'static str] {
304
325
  Some("rust") => &["", ".rs"],
305
326
  Some("python") => &["", ".py"],
306
327
  Some("go") => &["", ".go"],
328
+ Some("swift") => &["", ".swift"],
307
329
  Some("typescript") => &["", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
308
330
  Some("javascript") => &["", ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"],
309
331
  _ => &[
310
- "", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".rs", ".py", ".go",
332
+ "", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".rs", ".py", ".go", ".swift",
311
333
  ],
312
334
  }
313
335
  }
@@ -26,12 +26,25 @@ pub(super) fn extract_imports(
26
26
  }
27
27
 
28
28
  pub(super) fn package_name(specifier: &str) -> String {
29
+ let specifier = specifier
30
+ .split_once(" as ")
31
+ .map(|(package, _alias)| package)
32
+ .unwrap_or(specifier)
33
+ .trim();
29
34
  if let Some(rest) = specifier.strip_prefix('@') {
30
35
  let mut parts = rest.split('/');
31
36
  let scope = parts.next().unwrap_or_default();
32
37
  let package = parts.next().unwrap_or_default();
33
38
  return format!("@{scope}/{package}");
34
39
  }
40
+ if specifier
41
+ .split('/')
42
+ .next()
43
+ .is_some_and(|prefix| prefix.contains('.'))
44
+ && specifier.contains('/')
45
+ {
46
+ return specifier.to_string();
47
+ }
35
48
  specifier
36
49
  .split("::")
37
50
  .next()
@@ -39,6 +52,9 @@ pub(super) fn package_name(specifier: &str) -> String {
39
52
  .split('/')
40
53
  .next()
41
54
  .unwrap_or(specifier)
55
+ .split('.')
56
+ .next()
57
+ .unwrap_or(specifier)
42
58
  .to_string()
43
59
  }
44
60
 
@@ -54,6 +70,7 @@ pub(super) fn language_for_path(path: &str) -> Option<&'static str> {
54
70
  "rs" => Some("rust"),
55
71
  "py" => Some("python"),
56
72
  "go" => Some("go"),
73
+ "swift" => Some("swift"),
57
74
  _ => None,
58
75
  }
59
76
  }
@@ -0,0 +1,102 @@
1
+ use std::path::Path;
2
+
3
+ use crate::architecture::scan::{ManifestDependency, ManifestFact};
4
+
5
+ pub(super) enum ManifestKind {
6
+ PackageJson,
7
+ CargoToml,
8
+ PyprojectToml,
9
+ GoMod,
10
+ PomXml,
11
+ BuildGradle,
12
+ PackageSwift,
13
+ XcodeProject,
14
+ }
15
+
16
+ pub(super) fn manifest_kind(path: &str) -> Option<ManifestKind> {
17
+ match path.rsplit('/').next()? {
18
+ "package.json" => Some(ManifestKind::PackageJson),
19
+ "Cargo.toml" => Some(ManifestKind::CargoToml),
20
+ "pyproject.toml" => Some(ManifestKind::PyprojectToml),
21
+ "go.mod" => Some(ManifestKind::GoMod),
22
+ "pom.xml" => Some(ManifestKind::PomXml),
23
+ "build.gradle" | "build.gradle.kts" => Some(ManifestKind::BuildGradle),
24
+ "Package.swift" => Some(ManifestKind::PackageSwift),
25
+ "project.pbxproj" => {
26
+ if path.contains(".xcodeproj/") {
27
+ Some(ManifestKind::XcodeProject)
28
+ } else {
29
+ None
30
+ }
31
+ }
32
+ _ => None,
33
+ }
34
+ }
35
+
36
+ pub(super) fn fact(
37
+ path: &str,
38
+ ecosystem: &str,
39
+ package_name: String,
40
+ mut dependencies: Vec<ManifestDependency>,
41
+ confidence: f32,
42
+ ) -> ManifestFact {
43
+ dependencies.sort_by(|left, right| {
44
+ left.name
45
+ .cmp(&right.name)
46
+ .then(left.dependency_kind.cmp(&right.dependency_kind))
47
+ });
48
+ dependencies.dedup_by(|left, right| {
49
+ left.name == right.name && left.dependency_kind == right.dependency_kind
50
+ });
51
+ ManifestFact {
52
+ path: path.to_string(),
53
+ ecosystem: ecosystem.to_string(),
54
+ package_name,
55
+ dependencies,
56
+ confidence,
57
+ extractor: format!("manifest:{}", manifest_kind_label(path)),
58
+ }
59
+ }
60
+
61
+ pub(super) fn dependency(
62
+ name: impl Into<String>,
63
+ dependency_kind: impl Into<String>,
64
+ version: Option<String>,
65
+ confidence: f32,
66
+ ) -> ManifestDependency {
67
+ ManifestDependency {
68
+ name: name.into(),
69
+ dependency_kind: dependency_kind.into(),
70
+ version,
71
+ confidence,
72
+ }
73
+ }
74
+
75
+ pub(super) fn fallback_package_name(path: &str, ecosystem: &str) -> String {
76
+ let parent = Path::new(path)
77
+ .parent()
78
+ .map(|path| path.to_string_lossy().replace('\\', "/"))
79
+ .filter(|path| !path.is_empty())
80
+ .unwrap_or_else(|| path.to_string());
81
+ format!("{ecosystem}:{parent}")
82
+ }
83
+
84
+ pub(super) fn quoted_value(value: &str) -> Option<String> {
85
+ let trimmed = value.trim();
86
+ trimmed
87
+ .strip_prefix('"')
88
+ .and_then(|value| value.split_once('"').map(|(value, _)| value.to_string()))
89
+ .or_else(|| {
90
+ trimmed
91
+ .strip_prefix('\'')
92
+ .and_then(|value| value.split_once('\'').map(|(value, _)| value.to_string()))
93
+ })
94
+ }
95
+
96
+ pub(super) fn clean_toml_line(line: &str) -> &str {
97
+ line.split('#').next().unwrap_or_default().trim()
98
+ }
99
+
100
+ fn manifest_kind_label(path: &str) -> &str {
101
+ path.rsplit('/').next().unwrap_or(path)
102
+ }
@@ -0,0 +1,46 @@
1
+ use serde_json::Value;
2
+
3
+ use crate::architecture::scan::{ManifestDependency, ManifestFact};
4
+
5
+ use super::super::common::{dependency, fact, fallback_package_name};
6
+
7
+ pub(in crate::architecture::scan::manifest) fn package_json(
8
+ path: &str,
9
+ content: &str,
10
+ ) -> Option<ManifestFact> {
11
+ let value = serde_json::from_str::<Value>(content).ok()?;
12
+ let package_name = value
13
+ .get("name")
14
+ .and_then(Value::as_str)
15
+ .map(str::to_string)
16
+ .unwrap_or_else(|| fallback_package_name(path, "npm"));
17
+ let mut dependencies = Vec::new();
18
+ push_dependency_object(&mut dependencies, &value, "dependencies", "runtime");
19
+ push_dependency_object(&mut dependencies, &value, "devDependencies", "development");
20
+ push_dependency_object(&mut dependencies, &value, "peerDependencies", "peer");
21
+ push_dependency_object(
22
+ &mut dependencies,
23
+ &value,
24
+ "optionalDependencies",
25
+ "optional",
26
+ );
27
+ Some(fact(path, "npm", package_name, dependencies, 0.98))
28
+ }
29
+
30
+ fn push_dependency_object(
31
+ dependencies: &mut Vec<ManifestDependency>,
32
+ value: &Value,
33
+ field: &str,
34
+ kind: &str,
35
+ ) {
36
+ if let Some(object) = value.get(field).and_then(Value::as_object) {
37
+ dependencies.extend(object.iter().map(|(name, version)| {
38
+ dependency(
39
+ name.clone(),
40
+ kind,
41
+ version.as_str().map(str::to_string),
42
+ 0.98,
43
+ )
44
+ }));
45
+ }
46
+ }