@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.
- package/Cargo.lock +2 -2
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/architecture_commands.rs +2 -6
- package/crates/naome-cli/tests/architecture_cli.rs +60 -0
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/architecture/config/parser/sections.rs +44 -1
- package/crates/naome-core/src/architecture/config/parser.rs +1 -0
- package/crates/naome-core/src/architecture/config.rs +35 -0
- package/crates/naome-core/src/architecture/output.rs +15 -1
- package/crates/naome-core/src/architecture/rules/budgets.rs +179 -0
- package/crates/naome-core/src/architecture/rules/context.rs +138 -0
- package/crates/naome-core/src/architecture/rules/cycles.rs +39 -0
- package/crates/naome-core/src/architecture/rules/external.rs +244 -0
- package/crates/naome-core/src/architecture/rules/graph.rs +177 -0
- package/crates/naome-core/src/architecture/rules/transitive.rs +89 -0
- package/crates/naome-core/src/architecture/rules.rs +13 -39
- package/crates/naome-core/src/architecture/scan/graph_builder.rs +130 -30
- package/crates/naome-core/src/architecture/scan/imports/extractors/swift.rs +48 -0
- package/crates/naome-core/src/architecture/scan/imports/extractors.rs +7 -7
- package/crates/naome-core/src/architecture/scan/imports/resolver.rs +44 -22
- package/crates/naome-core/src/architecture/scan/imports.rs +17 -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 +27 -1
- package/crates/naome-core/src/architecture.rs +1 -1
- package/crates/naome-core/src/lib.rs +1 -0
- package/crates/naome-core/tests/architecture.rs +53 -85
- package/crates/naome-core/tests/architecture_manifests.rs +289 -0
- package/crates/naome-core/tests/architecture_rules.rs +498 -0
- package/crates/naome-core/tests/architecture_support/mod.rs +80 -0
- package/crates/naome-core/tests/architecture_swift.rs +111 -0
- package/installer/harness-files.js +3 -3
- 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 +61 -8
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
use crate::architecture::scan::{ManifestDependency, ManifestFact};
|
|
2
|
+
|
|
3
|
+
use super::super::common::{dependency, fact, fallback_package_name, quoted_value};
|
|
4
|
+
|
|
5
|
+
pub(in crate::architecture::scan::manifest) fn go_mod(
|
|
6
|
+
path: &str,
|
|
7
|
+
content: &str,
|
|
8
|
+
) -> Option<ManifestFact> {
|
|
9
|
+
let package_name = content
|
|
10
|
+
.lines()
|
|
11
|
+
.find_map(|line| line.trim().strip_prefix("module "))
|
|
12
|
+
.map(str::trim)
|
|
13
|
+
.filter(|value| !value.is_empty())
|
|
14
|
+
.map(str::to_string)
|
|
15
|
+
.unwrap_or_else(|| fallback_package_name(path, "go"));
|
|
16
|
+
let mut dependencies = Vec::new();
|
|
17
|
+
let mut in_require_block = false;
|
|
18
|
+
for raw_line in content.lines() {
|
|
19
|
+
let line = raw_line.split("//").next().unwrap_or_default().trim();
|
|
20
|
+
if line == "require (" {
|
|
21
|
+
in_require_block = true;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if in_require_block && line == ")" {
|
|
25
|
+
in_require_block = false;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
let require_line = line.strip_prefix("require ").unwrap_or(line);
|
|
29
|
+
if !in_require_block && !line.starts_with("require ") {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if let Some((name, version)) = first_two_fields(require_line) {
|
|
33
|
+
dependencies.push(dependency(name, "runtime", version, 0.95));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
Some(fact(path, "go", package_name, dependencies, 0.95))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
pub(in crate::architecture::scan::manifest) fn pom_xml(
|
|
40
|
+
path: &str,
|
|
41
|
+
content: &str,
|
|
42
|
+
) -> Option<ManifestFact> {
|
|
43
|
+
let project_content = xml_without_parent_blocks(content);
|
|
44
|
+
let package_group = first_xml_tag(&project_content, "groupId")
|
|
45
|
+
.or_else(|| first_xml_tag(content, "groupId"))
|
|
46
|
+
.unwrap_or_else(|| "maven".to_string());
|
|
47
|
+
let package_artifact = first_xml_tag(&project_content, "artifactId")
|
|
48
|
+
.unwrap_or_else(|| fallback_package_name(path, "maven"));
|
|
49
|
+
let package_name = format!("{package_group}:{package_artifact}");
|
|
50
|
+
let dependency_content = xml_without_blocks(
|
|
51
|
+
&xml_without_blocks(content, "dependencyManagement"),
|
|
52
|
+
"build",
|
|
53
|
+
);
|
|
54
|
+
let dependencies = project_dependency_sections(&dependency_content)
|
|
55
|
+
.into_iter()
|
|
56
|
+
.flat_map(|section| section.split("<dependency>").skip(1).collect::<Vec<_>>())
|
|
57
|
+
.filter_map(|block| {
|
|
58
|
+
let group = first_xml_tag(block, "groupId")?;
|
|
59
|
+
let artifact = first_xml_tag(block, "artifactId")?;
|
|
60
|
+
Some(dependency(
|
|
61
|
+
format!("{group}:{artifact}"),
|
|
62
|
+
first_xml_tag(block, "scope").unwrap_or_else(|| "runtime".into()),
|
|
63
|
+
first_xml_tag(block, "version"),
|
|
64
|
+
0.75,
|
|
65
|
+
))
|
|
66
|
+
})
|
|
67
|
+
.collect();
|
|
68
|
+
Some(fact(path, "maven", package_name, dependencies, 0.75))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
pub(in crate::architecture::scan::manifest) fn build_gradle(
|
|
72
|
+
path: &str,
|
|
73
|
+
content: &str,
|
|
74
|
+
) -> Option<ManifestFact> {
|
|
75
|
+
let package_name = fallback_package_name(path, "gradle");
|
|
76
|
+
let dependencies = content.lines().filter_map(gradle_dependency).collect();
|
|
77
|
+
Some(fact(path, "gradle", package_name, dependencies, 0.7))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
pub(in crate::architecture::scan::manifest) fn package_swift(
|
|
81
|
+
path: &str,
|
|
82
|
+
content: &str,
|
|
83
|
+
) -> Option<ManifestFact> {
|
|
84
|
+
let active_content = swift_active_content(content);
|
|
85
|
+
let package_name = swift_argument_value(&active_content, "name")
|
|
86
|
+
.unwrap_or_else(|| fallback_package_name(path, "swift"));
|
|
87
|
+
let dependencies = active_content
|
|
88
|
+
.split(".package")
|
|
89
|
+
.skip(1)
|
|
90
|
+
.filter_map(|block| {
|
|
91
|
+
let url = swift_argument_value(block, "url")?;
|
|
92
|
+
Some(dependency(
|
|
93
|
+
package_name_from_url(&url),
|
|
94
|
+
"runtime",
|
|
95
|
+
swift_package_version(block),
|
|
96
|
+
0.8,
|
|
97
|
+
))
|
|
98
|
+
})
|
|
99
|
+
.collect();
|
|
100
|
+
Some(fact(path, "swift", package_name, dependencies, 0.8))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
pub(in crate::architecture::scan::manifest) fn xcode_project(
|
|
104
|
+
path: &str,
|
|
105
|
+
content: &str,
|
|
106
|
+
) -> Option<ManifestFact> {
|
|
107
|
+
let package_name = fallback_package_name(path, "xcode");
|
|
108
|
+
let dependencies = content
|
|
109
|
+
.lines()
|
|
110
|
+
.filter_map(|line| {
|
|
111
|
+
let url = line
|
|
112
|
+
.trim()
|
|
113
|
+
.strip_prefix("repositoryURL = ")
|
|
114
|
+
.and_then(quoted_value)?;
|
|
115
|
+
Some(dependency(
|
|
116
|
+
package_name_from_url(&url),
|
|
117
|
+
"runtime",
|
|
118
|
+
None,
|
|
119
|
+
0.65,
|
|
120
|
+
))
|
|
121
|
+
})
|
|
122
|
+
.collect();
|
|
123
|
+
Some(fact(path, "xcode", package_name, dependencies, 0.65))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn first_two_fields(line: &str) -> Option<(String, Option<String>)> {
|
|
127
|
+
let mut parts = line.split_whitespace();
|
|
128
|
+
Some((parts.next()?.to_string(), parts.next().map(str::to_string)))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fn first_xml_tag(content: &str, tag: &str) -> Option<String> {
|
|
132
|
+
let open = format!("<{tag}>");
|
|
133
|
+
let close = format!("</{tag}>");
|
|
134
|
+
content
|
|
135
|
+
.split_once(&open)?
|
|
136
|
+
.1
|
|
137
|
+
.split_once(&close)
|
|
138
|
+
.map(|(value, _)| value.trim().to_string())
|
|
139
|
+
.filter(|value| !value.is_empty())
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fn xml_without_parent_blocks(content: &str) -> String {
|
|
143
|
+
xml_without_blocks(content, "parent")
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fn xml_without_blocks(content: &str, tag: &str) -> String {
|
|
147
|
+
let open = format!("<{tag}>");
|
|
148
|
+
let close = format!("</{tag}>");
|
|
149
|
+
let mut remaining = content;
|
|
150
|
+
let mut output = String::new();
|
|
151
|
+
while let Some((before, after_open)) = remaining.split_once(&open) {
|
|
152
|
+
output.push_str(before);
|
|
153
|
+
let Some((_, after_close)) = after_open.split_once(&close) else {
|
|
154
|
+
output.push_str(after_open);
|
|
155
|
+
return output;
|
|
156
|
+
};
|
|
157
|
+
remaining = after_close;
|
|
158
|
+
}
|
|
159
|
+
output.push_str(remaining);
|
|
160
|
+
output
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fn project_dependency_sections(content: &str) -> Vec<&str> {
|
|
164
|
+
content
|
|
165
|
+
.split("<dependencies>")
|
|
166
|
+
.skip(1)
|
|
167
|
+
.filter_map(|block| block.split_once("</dependencies>").map(|(block, _)| block))
|
|
168
|
+
.collect()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
fn gradle_dependency(line: &str) -> Option<ManifestDependency> {
|
|
172
|
+
let trimmed = line.split("//").next().unwrap_or_default().trim();
|
|
173
|
+
if trimmed.is_empty() {
|
|
174
|
+
return None;
|
|
175
|
+
}
|
|
176
|
+
let kind = [
|
|
177
|
+
"implementation",
|
|
178
|
+
"api",
|
|
179
|
+
"compileOnly",
|
|
180
|
+
"runtimeOnly",
|
|
181
|
+
"testImplementation",
|
|
182
|
+
]
|
|
183
|
+
.into_iter()
|
|
184
|
+
.find(|kind| trimmed.contains(&format!("{kind} ")) || trimmed.contains(&format!("{kind}(")))?;
|
|
185
|
+
let coordinate_text = trimmed
|
|
186
|
+
.split_once(&format!("{kind} "))
|
|
187
|
+
.map(|(_, value)| value.trim())
|
|
188
|
+
.or_else(|| {
|
|
189
|
+
trimmed
|
|
190
|
+
.split_once(&format!("{kind}("))
|
|
191
|
+
.map(|(_, value)| value.trim_end_matches(')').trim())
|
|
192
|
+
})?;
|
|
193
|
+
if let Some(coordinate) = quoted_value(coordinate_text) {
|
|
194
|
+
return gradle_coordinate_dependency(&coordinate, kind);
|
|
195
|
+
}
|
|
196
|
+
let group = gradle_map_value(coordinate_text, "group")?;
|
|
197
|
+
let artifact = gradle_map_value(coordinate_text, "name")?;
|
|
198
|
+
Some(dependency(
|
|
199
|
+
format!("{group}:{artifact}"),
|
|
200
|
+
kind,
|
|
201
|
+
gradle_map_value(coordinate_text, "version"),
|
|
202
|
+
0.7,
|
|
203
|
+
))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
fn gradle_coordinate_dependency(coordinate: &str, kind: &str) -> Option<ManifestDependency> {
|
|
207
|
+
let mut parts = coordinate.split(':');
|
|
208
|
+
let group = parts.next()?;
|
|
209
|
+
let artifact = parts.next()?;
|
|
210
|
+
Some(dependency(
|
|
211
|
+
format!("{group}:{artifact}"),
|
|
212
|
+
kind,
|
|
213
|
+
parts.next().map(str::to_string),
|
|
214
|
+
0.7,
|
|
215
|
+
))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
fn gradle_map_value(text: &str, key: &str) -> Option<String> {
|
|
219
|
+
text.split(',').find_map(|part| {
|
|
220
|
+
let (name, value) = part.trim().split_once(':')?;
|
|
221
|
+
if name.trim() == key {
|
|
222
|
+
quoted_value(value.trim())
|
|
223
|
+
} else {
|
|
224
|
+
None
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
fn swift_argument_value(content: &str, key: &str) -> Option<String> {
|
|
230
|
+
let marker = format!("{key}:");
|
|
231
|
+
let (_, value) = content.split_once(&marker)?;
|
|
232
|
+
quoted_value(value.trim())
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
fn swift_active_content(content: &str) -> String {
|
|
236
|
+
content
|
|
237
|
+
.lines()
|
|
238
|
+
.map(strip_swift_line_comment)
|
|
239
|
+
.collect::<Vec<_>>()
|
|
240
|
+
.join("\n")
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
fn strip_swift_line_comment(line: &str) -> &str {
|
|
244
|
+
let mut in_quote = false;
|
|
245
|
+
let mut escaped = false;
|
|
246
|
+
let mut previous = '\0';
|
|
247
|
+
for (index, character) in line.char_indices() {
|
|
248
|
+
if in_quote {
|
|
249
|
+
if character == '"' && !escaped {
|
|
250
|
+
in_quote = false;
|
|
251
|
+
}
|
|
252
|
+
escaped = character == '\\' && !escaped;
|
|
253
|
+
} else if character == '"' {
|
|
254
|
+
in_quote = true;
|
|
255
|
+
escaped = false;
|
|
256
|
+
} else if previous == '/' && character == '/' {
|
|
257
|
+
return &line[..index - 1];
|
|
258
|
+
}
|
|
259
|
+
if character != '\\' {
|
|
260
|
+
escaped = false;
|
|
261
|
+
}
|
|
262
|
+
previous = character;
|
|
263
|
+
}
|
|
264
|
+
line
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
fn swift_package_version(block: &str) -> Option<String> {
|
|
268
|
+
swift_argument_value(block, "from")
|
|
269
|
+
.or_else(|| swift_argument_value(block, "exact"))
|
|
270
|
+
.or_else(|| swift_argument_value(block, "branch"))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
fn package_name_from_url(url: &str) -> String {
|
|
274
|
+
url.trim_end_matches(".git")
|
|
275
|
+
.trim_end_matches('/')
|
|
276
|
+
.rsplit('/')
|
|
277
|
+
.next()
|
|
278
|
+
.unwrap_or(url)
|
|
279
|
+
.to_string()
|
|
280
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
use crate::architecture::scan::{ManifestDependency, ManifestFact};
|
|
2
|
+
|
|
3
|
+
use super::super::common::{
|
|
4
|
+
clean_toml_line, dependency, fact, fallback_package_name, quoted_value,
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
pub(in crate::architecture::scan::manifest) fn cargo_toml(
|
|
8
|
+
path: &str,
|
|
9
|
+
content: &str,
|
|
10
|
+
) -> Option<ManifestFact> {
|
|
11
|
+
let package_name = toml_string(content, "package", "name")
|
|
12
|
+
.unwrap_or_else(|| fallback_package_name(path, "cargo"));
|
|
13
|
+
let dependencies = toml_dependency_sections(
|
|
14
|
+
content,
|
|
15
|
+
&[
|
|
16
|
+
("dependencies", "runtime"),
|
|
17
|
+
("dev-dependencies", "development"),
|
|
18
|
+
("build-dependencies", "build"),
|
|
19
|
+
],
|
|
20
|
+
);
|
|
21
|
+
Some(fact(path, "cargo", package_name, dependencies, 0.9))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
pub(in crate::architecture::scan::manifest) fn pyproject_toml(
|
|
25
|
+
path: &str,
|
|
26
|
+
content: &str,
|
|
27
|
+
) -> Option<ManifestFact> {
|
|
28
|
+
let package_name = toml_string(content, "project", "name")
|
|
29
|
+
.unwrap_or_else(|| fallback_package_name(path, "python"));
|
|
30
|
+
let mut dependencies = toml_string_array(content, "project", "dependencies")
|
|
31
|
+
.into_iter()
|
|
32
|
+
.map(|value| dependency(python_dependency_name(&value), "runtime", Some(value), 0.85))
|
|
33
|
+
.collect::<Vec<_>>();
|
|
34
|
+
dependencies.extend(
|
|
35
|
+
toml_dependency_sections(content, &[("tool.poetry.dependencies", "runtime")])
|
|
36
|
+
.into_iter()
|
|
37
|
+
.filter(|dependency| dependency.name != "python"),
|
|
38
|
+
);
|
|
39
|
+
Some(fact(path, "python", package_name, dependencies, 0.85))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn toml_string(content: &str, section: &str, key: &str) -> Option<String> {
|
|
43
|
+
section_value(content, section, key).and_then(quoted_value)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fn toml_string_array(content: &str, section: &str, key: &str) -> Vec<String> {
|
|
47
|
+
let mut current_section = "";
|
|
48
|
+
let mut collecting = false;
|
|
49
|
+
let mut array = String::new();
|
|
50
|
+
for line in content.lines().map(clean_toml_line) {
|
|
51
|
+
if !collecting {
|
|
52
|
+
if let Some(section_name) = line
|
|
53
|
+
.strip_prefix('[')
|
|
54
|
+
.and_then(|line| line.strip_suffix(']'))
|
|
55
|
+
{
|
|
56
|
+
current_section = section_name.trim();
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if current_section != section {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
let Some(value) = line
|
|
63
|
+
.strip_prefix(key)
|
|
64
|
+
.and_then(|line| line.trim().strip_prefix('='))
|
|
65
|
+
else {
|
|
66
|
+
continue;
|
|
67
|
+
};
|
|
68
|
+
array.push_str(value.trim());
|
|
69
|
+
if value.contains(']') {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
collecting = value.trim_start().starts_with('[');
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
array.push(' ');
|
|
76
|
+
array.push_str(line);
|
|
77
|
+
if line.contains(']') {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
array
|
|
82
|
+
.trim()
|
|
83
|
+
.trim_start_matches('[')
|
|
84
|
+
.trim_end_matches(']')
|
|
85
|
+
.split(',')
|
|
86
|
+
.filter_map(quoted_value)
|
|
87
|
+
.collect()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fn section_value<'a>(content: &'a str, section: &str, key: &str) -> Option<&'a str> {
|
|
91
|
+
let mut current_section = "";
|
|
92
|
+
for line in content.lines().map(clean_toml_line) {
|
|
93
|
+
if let Some(section_name) = line
|
|
94
|
+
.strip_prefix('[')
|
|
95
|
+
.and_then(|line| line.strip_suffix(']'))
|
|
96
|
+
{
|
|
97
|
+
current_section = section_name.trim();
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if current_section == section {
|
|
101
|
+
if let Some(value) = line
|
|
102
|
+
.strip_prefix(key)
|
|
103
|
+
.and_then(|line| line.trim().strip_prefix('='))
|
|
104
|
+
{
|
|
105
|
+
return Some(value.trim());
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
None
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fn toml_dependency_sections(content: &str, sections: &[(&str, &str)]) -> Vec<ManifestDependency> {
|
|
113
|
+
let mut current_kind = None;
|
|
114
|
+
let mut table_dependency = None;
|
|
115
|
+
let mut dependencies = Vec::new();
|
|
116
|
+
for line in content.lines().map(clean_toml_line) {
|
|
117
|
+
if let Some(section_name) = line
|
|
118
|
+
.strip_prefix('[')
|
|
119
|
+
.and_then(|line| line.strip_suffix(']'))
|
|
120
|
+
{
|
|
121
|
+
push_table_dependency(&mut dependencies, table_dependency.take());
|
|
122
|
+
let section_name = section_name.trim();
|
|
123
|
+
current_kind = sections
|
|
124
|
+
.iter()
|
|
125
|
+
.find(|(section, _)| *section == section_name)
|
|
126
|
+
.map(|(_, kind)| *kind);
|
|
127
|
+
if current_kind.is_none() {
|
|
128
|
+
table_dependency = dependency_table(section_name, sections);
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if let Some((name, kind, version)) = table_dependency.as_mut() {
|
|
133
|
+
if let Some((key, value)) = line.split_once('=') {
|
|
134
|
+
if key.trim() == "version" {
|
|
135
|
+
*version = quoted_value(value.trim());
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if !name.is_empty() && !kind.is_empty() {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if let (Some(kind), Some((name, value))) = (current_kind, line.split_once('=')) {
|
|
143
|
+
dependencies.push(dependency(
|
|
144
|
+
name.trim().to_string(),
|
|
145
|
+
kind,
|
|
146
|
+
quoted_value(value.trim()),
|
|
147
|
+
0.85,
|
|
148
|
+
));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
push_table_dependency(&mut dependencies, table_dependency);
|
|
152
|
+
dependencies
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
fn dependency_table<'a>(
|
|
156
|
+
section_name: &str,
|
|
157
|
+
sections: &[(&str, &'a str)],
|
|
158
|
+
) -> Option<(String, &'a str, Option<String>)> {
|
|
159
|
+
sections.iter().find_map(|(section, kind)| {
|
|
160
|
+
let dependency_name = section_name.strip_prefix(&format!("{section}."))?;
|
|
161
|
+
if dependency_name.is_empty() || dependency_name.contains('.') {
|
|
162
|
+
return None;
|
|
163
|
+
}
|
|
164
|
+
Some((dependency_name.to_string(), *kind, None))
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
fn push_table_dependency(
|
|
169
|
+
dependencies: &mut Vec<ManifestDependency>,
|
|
170
|
+
table_dependency: Option<(String, &str, Option<String>)>,
|
|
171
|
+
) {
|
|
172
|
+
if let Some((name, kind, version)) = table_dependency {
|
|
173
|
+
dependencies.push(dependency(name, kind, version, 0.85));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fn python_dependency_name(value: &str) -> String {
|
|
178
|
+
value
|
|
179
|
+
.split(['<', '>', '=', '!', '~', '[', ';', ' '])
|
|
180
|
+
.next()
|
|
181
|
+
.unwrap_or(value)
|
|
182
|
+
.trim()
|
|
183
|
+
.to_string()
|
|
184
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
use std::fs;
|
|
2
|
+
use std::path::Path;
|
|
3
|
+
|
|
4
|
+
use super::ManifestFact;
|
|
5
|
+
|
|
6
|
+
mod common;
|
|
7
|
+
mod parsers;
|
|
8
|
+
|
|
9
|
+
use common::{manifest_kind, ManifestKind};
|
|
10
|
+
|
|
11
|
+
pub(super) fn extract_manifests(root: &Path, files: &[String]) -> Vec<ManifestFact> {
|
|
12
|
+
let mut manifests = files
|
|
13
|
+
.iter()
|
|
14
|
+
.filter_map(|path| extract_manifest(root, path))
|
|
15
|
+
.collect::<Vec<_>>();
|
|
16
|
+
manifests.sort_by(|left, right| left.path.cmp(&right.path));
|
|
17
|
+
manifests
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
fn extract_manifest(root: &Path, path: &str) -> Option<ManifestFact> {
|
|
21
|
+
let kind = manifest_kind(path)?;
|
|
22
|
+
let content = fs::read_to_string(root.join(path)).ok()?;
|
|
23
|
+
match kind {
|
|
24
|
+
ManifestKind::PackageJson => parsers::json::package_json(path, &content),
|
|
25
|
+
ManifestKind::CargoToml => parsers::toml::cargo_toml(path, &content),
|
|
26
|
+
ManifestKind::PyprojectToml => parsers::toml::pyproject_toml(path, &content),
|
|
27
|
+
ManifestKind::GoMod => parsers::other::go_mod(path, &content),
|
|
28
|
+
ManifestKind::PomXml => parsers::other::pom_xml(path, &content),
|
|
29
|
+
ManifestKind::BuildGradle => parsers::other::build_gradle(path, &content),
|
|
30
|
+
ManifestKind::PackageSwift => parsers::other::package_swift(path, &content),
|
|
31
|
+
ManifestKind::XcodeProject => parsers::other::xcode_project(path, &content),
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -11,6 +11,7 @@ use super::model::ArchitectureGraph;
|
|
|
11
11
|
|
|
12
12
|
mod graph_builder;
|
|
13
13
|
mod imports;
|
|
14
|
+
mod manifest;
|
|
14
15
|
mod path_scan;
|
|
15
16
|
|
|
16
17
|
#[derive(Debug, Clone, Default)]
|
|
@@ -32,6 +33,8 @@ pub struct ArchitectureScanReport {
|
|
|
32
33
|
pub config: ArchitectureConfig,
|
|
33
34
|
#[serde(skip_serializing)]
|
|
34
35
|
pub file_facts: BTreeMap<String, FileFact>,
|
|
36
|
+
#[serde(skip_serializing)]
|
|
37
|
+
pub manifest_facts: Vec<ManifestFact>,
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
#[derive(Debug, Clone, Serialize)]
|
|
@@ -64,6 +67,26 @@ pub enum ImportTarget {
|
|
|
64
67
|
Unknown(String),
|
|
65
68
|
}
|
|
66
69
|
|
|
70
|
+
#[derive(Debug, Clone, Serialize)]
|
|
71
|
+
#[serde(rename_all = "camelCase")]
|
|
72
|
+
pub struct ManifestFact {
|
|
73
|
+
pub path: String,
|
|
74
|
+
pub ecosystem: String,
|
|
75
|
+
pub package_name: String,
|
|
76
|
+
pub dependencies: Vec<ManifestDependency>,
|
|
77
|
+
pub confidence: f32,
|
|
78
|
+
pub extractor: String,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#[derive(Debug, Clone, Serialize)]
|
|
82
|
+
#[serde(rename_all = "camelCase")]
|
|
83
|
+
pub struct ManifestDependency {
|
|
84
|
+
pub name: String,
|
|
85
|
+
pub dependency_kind: String,
|
|
86
|
+
pub version: Option<String>,
|
|
87
|
+
pub confidence: f32,
|
|
88
|
+
}
|
|
89
|
+
|
|
67
90
|
pub fn scan_architecture(
|
|
68
91
|
root: &Path,
|
|
69
92
|
options: ArchitectureScanOptions,
|
|
@@ -71,7 +94,9 @@ pub fn scan_architecture(
|
|
|
71
94
|
let config = read_architecture_config(root, options.config_path.as_deref())?;
|
|
72
95
|
let changed_paths = changed_paths(root, options.changed_only)?;
|
|
73
96
|
let files = path_scan::repository_files(root, &config)?;
|
|
74
|
-
let
|
|
97
|
+
let manifest_facts = manifest::extract_manifests(root, &files);
|
|
98
|
+
let (mut graph, file_facts) =
|
|
99
|
+
graph_builder::build_path_graph(root, files, &config, &manifest_facts);
|
|
75
100
|
|
|
76
101
|
graph.sort_stable();
|
|
77
102
|
Ok(ArchitectureScanReport {
|
|
@@ -83,6 +108,7 @@ pub fn scan_architecture(
|
|
|
83
108
|
changed_paths,
|
|
84
109
|
config,
|
|
85
110
|
file_facts,
|
|
111
|
+
manifest_facts,
|
|
86
112
|
})
|
|
87
113
|
}
|
|
88
114
|
|
|
@@ -18,7 +18,7 @@ pub use model::{
|
|
|
18
18
|
pub use output::{
|
|
19
19
|
format_architecture_explain, format_architecture_scan, format_architecture_validation,
|
|
20
20
|
ArchitectureAgentFeedback, ArchitectureValidation, ArchitectureViolation, Severity,
|
|
21
|
-
ViolationSummary,
|
|
21
|
+
ViolationSummary, ARCHITECTURE_RULE_IDS,
|
|
22
22
|
};
|
|
23
23
|
pub use scan::{scan_architecture, ArchitectureScanOptions, ArchitectureScanReport};
|
|
24
24
|
|
|
@@ -27,6 +27,7 @@ pub use architecture::{
|
|
|
27
27
|
ArchitectureGraph, ArchitectureMetadata, ArchitectureNode, ArchitectureNodeKind,
|
|
28
28
|
ArchitectureScanOptions, ArchitectureScanReport, ArchitectureValidation, ArchitectureViolation,
|
|
29
29
|
ContextConfig, LayerConfig, RuleConfig, Severity, SourceRange, ViolationSummary,
|
|
30
|
+
ARCHITECTURE_RULE_IDS,
|
|
30
31
|
};
|
|
31
32
|
pub use context::{
|
|
32
33
|
select_context_for_changed_paths, select_context_for_prompt, ContextBudgetLedger,
|