@lamentis/naome 1.3.16 → 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 -4
- 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 +22 -162
- package/crates/naome-core/src/architecture/rules.rs +4 -3
- 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 +7 -3
- package/crates/naome-core/src/lib.rs +1 -0
- 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 +87 -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
|
@@ -5,6 +5,11 @@ use std::path::{Path, PathBuf};
|
|
|
5
5
|
use super::{external_target, language_for_path};
|
|
6
6
|
use crate::architecture::scan::ImportTarget;
|
|
7
7
|
|
|
8
|
+
mod candidates;
|
|
9
|
+
mod js_ts_alias;
|
|
10
|
+
|
|
11
|
+
use candidates::{resolve_candidates, resolve_progressively};
|
|
12
|
+
|
|
8
13
|
pub(super) fn resolve_import(
|
|
9
14
|
root: &Path,
|
|
10
15
|
from_path: &str,
|
|
@@ -27,9 +32,30 @@ pub(super) fn resolve_import(
|
|
|
27
32
|
.map(ImportTarget::File)
|
|
28
33
|
.unwrap_or_else(|| ImportTarget::Unknown(specifier.to_string()));
|
|
29
34
|
}
|
|
35
|
+
if matches!(language, Some("typescript") | Some("javascript")) {
|
|
36
|
+
match js_ts_alias::resolve(root, from_path, specifier, repository_files, language) {
|
|
37
|
+
Some(js_ts_alias::AliasResolution::Resolved(path)) => return ImportTarget::File(path),
|
|
38
|
+
Some(js_ts_alias::AliasResolution::Unresolved) => {
|
|
39
|
+
return ImportTarget::Unknown(specifier.to_string());
|
|
40
|
+
}
|
|
41
|
+
None => {}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
30
44
|
if let Some(path) = resolve_repo_absolute(specifier, repository_files, language) {
|
|
31
45
|
return ImportTarget::File(path);
|
|
32
46
|
}
|
|
47
|
+
if is_repo_absolute_specifier(specifier) {
|
|
48
|
+
return ImportTarget::Unknown(specifier.to_string());
|
|
49
|
+
}
|
|
50
|
+
if from_path.ends_with(".py") {
|
|
51
|
+
if let Some(path) = resolve_python_absolute_package(from_path, specifier, repository_files)
|
|
52
|
+
{
|
|
53
|
+
return ImportTarget::File(path);
|
|
54
|
+
}
|
|
55
|
+
if is_python_local_package_specifier(from_path, specifier, repository_files) {
|
|
56
|
+
return ImportTarget::Unknown(specifier.to_string());
|
|
57
|
+
}
|
|
58
|
+
}
|
|
33
59
|
if from_path.ends_with(".go") {
|
|
34
60
|
if let Some(path) = resolve_go_module_path(root, from_path, specifier, repository_files) {
|
|
35
61
|
return ImportTarget::File(path);
|
|
@@ -72,12 +98,7 @@ fn resolve_python_relative(
|
|
|
72
98
|
for _ in 1..dots {
|
|
73
99
|
base = base.parent().unwrap_or_else(|| Path::new(""));
|
|
74
100
|
}
|
|
75
|
-
|
|
76
|
-
&normalize(base.join(module)),
|
|
77
|
-
repository_files,
|
|
78
|
-
Some("python"),
|
|
79
|
-
'/',
|
|
80
|
-
)
|
|
101
|
+
resolve_python_package_candidate(&normalize(base.join(module)), repository_files)
|
|
81
102
|
}
|
|
82
103
|
|
|
83
104
|
fn resolve_repo_absolute(
|
|
@@ -102,6 +123,86 @@ fn resolve_repo_absolute(
|
|
|
102
123
|
None
|
|
103
124
|
}
|
|
104
125
|
|
|
126
|
+
fn resolve_python_absolute_package(
|
|
127
|
+
from_path: &str,
|
|
128
|
+
specifier: &str,
|
|
129
|
+
repository_files: &BTreeSet<String>,
|
|
130
|
+
) -> Option<String> {
|
|
131
|
+
if specifier.contains('/') {
|
|
132
|
+
return None;
|
|
133
|
+
}
|
|
134
|
+
let dotted_candidate = specifier.replace('.', "/");
|
|
135
|
+
let package_name = dotted_candidate.split('/').next()?;
|
|
136
|
+
python_package_roots(package_name, from_path, repository_files)
|
|
137
|
+
.into_iter()
|
|
138
|
+
.find_map(|root| {
|
|
139
|
+
resolve_python_package_candidate(&format!("{root}{dotted_candidate}"), repository_files)
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
fn resolve_python_package_candidate(
|
|
144
|
+
candidate: &str,
|
|
145
|
+
repository_files: &BTreeSet<String>,
|
|
146
|
+
) -> Option<String> {
|
|
147
|
+
resolve_candidates(candidate, repository_files, Some("python")).or_else(|| {
|
|
148
|
+
let (module_candidate, _) = candidate.rsplit_once('/')?;
|
|
149
|
+
resolve_candidates(module_candidate, repository_files, Some("python"))
|
|
150
|
+
.filter(|path| !path.ends_with("/__init__.py"))
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fn is_python_local_package_specifier(
|
|
155
|
+
from_path: &str,
|
|
156
|
+
specifier: &str,
|
|
157
|
+
repository_files: &BTreeSet<String>,
|
|
158
|
+
) -> bool {
|
|
159
|
+
if specifier.contains('/') || !specifier.contains('.') {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
let Some(package_name) = specifier.split('.').next() else {
|
|
163
|
+
return false;
|
|
164
|
+
};
|
|
165
|
+
!python_package_roots(package_name, from_path, repository_files).is_empty()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
fn python_package_roots(
|
|
169
|
+
package_name: &str,
|
|
170
|
+
from_path: &str,
|
|
171
|
+
repository_files: &BTreeSet<String>,
|
|
172
|
+
) -> Vec<String> {
|
|
173
|
+
let package_marker = format!("{package_name}/__init__.py");
|
|
174
|
+
let module_marker = format!("{package_name}.py");
|
|
175
|
+
let mut roots = repository_files
|
|
176
|
+
.iter()
|
|
177
|
+
.filter_map(|path| {
|
|
178
|
+
if path == &package_marker || path == &module_marker {
|
|
179
|
+
Some(String::new())
|
|
180
|
+
} else if let Some(prefix) = path.strip_suffix(&package_marker) {
|
|
181
|
+
Some(prefix.to_string())
|
|
182
|
+
} else {
|
|
183
|
+
path.strip_suffix(&module_marker).map(ToString::to_string)
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
.collect::<Vec<_>>();
|
|
187
|
+
roots.sort_by(|left, right| {
|
|
188
|
+
let left_local = from_path.starts_with(left.as_str());
|
|
189
|
+
let right_local = from_path.starts_with(right.as_str());
|
|
190
|
+
right_local
|
|
191
|
+
.cmp(&left_local)
|
|
192
|
+
.then_with(|| right.len().cmp(&left.len()))
|
|
193
|
+
.then_with(|| left.cmp(right))
|
|
194
|
+
});
|
|
195
|
+
roots.dedup();
|
|
196
|
+
roots
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
fn is_repo_absolute_specifier(specifier: &str) -> bool {
|
|
200
|
+
specifier.starts_with("@/")
|
|
201
|
+
|| specifier.starts_with("src/")
|
|
202
|
+
|| specifier.starts_with("packages/")
|
|
203
|
+
|| specifier.starts_with("apps/")
|
|
204
|
+
}
|
|
205
|
+
|
|
105
206
|
fn resolve_rust_local(
|
|
106
207
|
from_path: &str,
|
|
107
208
|
specifier: &str,
|
|
@@ -119,22 +220,27 @@ fn resolve_rust_local(
|
|
|
119
220
|
"crate" => {
|
|
120
221
|
parts.remove(0);
|
|
121
222
|
let crate_root = rust_crate_src_root(from_path);
|
|
122
|
-
|
|
223
|
+
let allow_root_fallback = parts.len() == 1;
|
|
224
|
+
resolve_rust_path_candidate(
|
|
123
225
|
&normalize(Path::new(&crate_root).join(parts.join("/"))),
|
|
124
226
|
repository_files,
|
|
125
|
-
Some("rust"),
|
|
126
|
-
'/',
|
|
127
227
|
)
|
|
228
|
+
.or_else(|| {
|
|
229
|
+
allow_root_fallback
|
|
230
|
+
.then(|| rust_crate_root_file(&crate_root, repository_files))
|
|
231
|
+
.flatten()
|
|
232
|
+
})
|
|
128
233
|
}
|
|
129
234
|
"self" => {
|
|
130
235
|
parts.remove(0);
|
|
131
236
|
let base = PathBuf::from(rust_module_directory(from_path));
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
237
|
+
let allow_module_fallback = parts.len() == 1;
|
|
238
|
+
resolve_rust_path_candidate(&normalize(base.join(parts.join("/"))), repository_files)
|
|
239
|
+
.or_else(|| {
|
|
240
|
+
allow_module_fallback
|
|
241
|
+
.then(|| rust_module_file_for_directory(&base, repository_files))
|
|
242
|
+
.flatten()
|
|
243
|
+
})
|
|
138
244
|
}
|
|
139
245
|
"super" => {
|
|
140
246
|
let mut base = PathBuf::from(rust_module_directory(from_path));
|
|
@@ -142,17 +248,52 @@ fn resolve_rust_local(
|
|
|
142
248
|
parts.remove(0);
|
|
143
249
|
base.pop();
|
|
144
250
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
251
|
+
let allow_module_fallback = parts.len() == 1;
|
|
252
|
+
resolve_rust_path_candidate(&normalize(base.join(parts.join("/"))), repository_files)
|
|
253
|
+
.or_else(|| {
|
|
254
|
+
allow_module_fallback
|
|
255
|
+
.then(|| rust_module_file_for_directory(&base, repository_files))
|
|
256
|
+
.flatten()
|
|
257
|
+
})
|
|
151
258
|
}
|
|
152
259
|
_ => None,
|
|
153
260
|
}
|
|
154
261
|
}
|
|
155
262
|
|
|
263
|
+
fn resolve_rust_path_candidate(
|
|
264
|
+
candidate: &str,
|
|
265
|
+
repository_files: &BTreeSet<String>,
|
|
266
|
+
) -> Option<String> {
|
|
267
|
+
resolve_candidates(candidate, repository_files, Some("rust")).or_else(|| {
|
|
268
|
+
let (module_candidate, _) = candidate.rsplit_once('/')?;
|
|
269
|
+
resolve_candidates(module_candidate, repository_files, Some("rust"))
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
fn rust_crate_root_file(crate_root: &str, repository_files: &BTreeSet<String>) -> Option<String> {
|
|
274
|
+
["lib.rs", "main.rs"]
|
|
275
|
+
.iter()
|
|
276
|
+
.map(|file_name| normalize(Path::new(crate_root).join(file_name)))
|
|
277
|
+
.find(|path| repository_files.contains(path))
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
fn rust_module_file_for_directory(
|
|
281
|
+
directory: &Path,
|
|
282
|
+
repository_files: &BTreeSet<String>,
|
|
283
|
+
) -> Option<String> {
|
|
284
|
+
let normalized = normalize(directory);
|
|
285
|
+
let mut candidates = Vec::new();
|
|
286
|
+
if !normalized.is_empty() {
|
|
287
|
+
candidates.push(format!("{normalized}.rs"));
|
|
288
|
+
candidates.push(format!("{normalized}/mod.rs"));
|
|
289
|
+
candidates.push(format!("{normalized}/lib.rs"));
|
|
290
|
+
candidates.push(format!("{normalized}/main.rs"));
|
|
291
|
+
}
|
|
292
|
+
candidates
|
|
293
|
+
.into_iter()
|
|
294
|
+
.find(|path| repository_files.contains(path))
|
|
295
|
+
}
|
|
296
|
+
|
|
156
297
|
fn rust_module_directory(from_path: &str) -> String {
|
|
157
298
|
let source = Path::new(from_path);
|
|
158
299
|
let parent = source.parent().unwrap_or_else(|| Path::new(""));
|
|
@@ -264,76 +405,6 @@ fn swift_sources_root(from_path: &str) -> Option<String> {
|
|
|
264
405
|
Some(format!("{prefix}/Sources/"))
|
|
265
406
|
}
|
|
266
407
|
|
|
267
|
-
fn resolve_candidates(
|
|
268
|
-
candidate: &str,
|
|
269
|
-
repository_files: &BTreeSet<String>,
|
|
270
|
-
language: Option<&str>,
|
|
271
|
-
) -> Option<String> {
|
|
272
|
-
candidate_paths(candidate, language)
|
|
273
|
-
.into_iter()
|
|
274
|
-
.find(|path| repository_files.contains(path))
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
fn resolve_progressively(
|
|
278
|
-
candidate: &str,
|
|
279
|
-
repository_files: &BTreeSet<String>,
|
|
280
|
-
language: Option<&str>,
|
|
281
|
-
separator: char,
|
|
282
|
-
) -> Option<String> {
|
|
283
|
-
let mut current = candidate.to_string();
|
|
284
|
-
loop {
|
|
285
|
-
if let Some(path) = resolve_candidates(¤t, repository_files, language) {
|
|
286
|
-
return Some(path);
|
|
287
|
-
}
|
|
288
|
-
let Some((parent, _)) = current.rsplit_once(separator) else {
|
|
289
|
-
return None;
|
|
290
|
-
};
|
|
291
|
-
current = parent.to_string();
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
fn candidate_paths(candidate: &str, language: Option<&str>) -> Vec<String> {
|
|
296
|
-
let extensions = extensions_for_language(language);
|
|
297
|
-
let mut candidates = extensions
|
|
298
|
-
.iter()
|
|
299
|
-
.map(|extension| format!("{candidate}{extension}"))
|
|
300
|
-
.collect::<Vec<_>>();
|
|
301
|
-
match language {
|
|
302
|
-
Some("rust") => candidates.push(format!("{candidate}/mod.rs")),
|
|
303
|
-
Some("python") => candidates.push(format!("{candidate}/__init__.py")),
|
|
304
|
-
Some("go") => {}
|
|
305
|
-
Some("typescript") | Some("javascript") => candidates.extend([
|
|
306
|
-
format!("{candidate}/index.ts"),
|
|
307
|
-
format!("{candidate}/index.tsx"),
|
|
308
|
-
format!("{candidate}/index.js"),
|
|
309
|
-
format!("{candidate}/index.jsx"),
|
|
310
|
-
]),
|
|
311
|
-
_ => candidates.extend([
|
|
312
|
-
format!("{candidate}/index.ts"),
|
|
313
|
-
format!("{candidate}/index.tsx"),
|
|
314
|
-
format!("{candidate}/index.js"),
|
|
315
|
-
format!("{candidate}/index.jsx"),
|
|
316
|
-
format!("{candidate}/mod.rs"),
|
|
317
|
-
format!("{candidate}/__init__.py"),
|
|
318
|
-
]),
|
|
319
|
-
}
|
|
320
|
-
candidates
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
fn extensions_for_language(language: Option<&str>) -> &'static [&'static str] {
|
|
324
|
-
match language {
|
|
325
|
-
Some("rust") => &["", ".rs"],
|
|
326
|
-
Some("python") => &["", ".py"],
|
|
327
|
-
Some("go") => &["", ".go"],
|
|
328
|
-
Some("swift") => &["", ".swift"],
|
|
329
|
-
Some("typescript") => &["", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
|
|
330
|
-
Some("javascript") => &["", ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"],
|
|
331
|
-
_ => &[
|
|
332
|
-
"", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".rs", ".py", ".go", ".swift",
|
|
333
|
-
],
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
408
|
fn is_rust_local(specifier: &str) -> bool {
|
|
338
409
|
specifier.starts_with("crate::")
|
|
339
410
|
|| specifier.starts_with("self::")
|
|
@@ -19,6 +19,22 @@ mod imports;
|
|
|
19
19
|
mod manifest;
|
|
20
20
|
mod path_scan;
|
|
21
21
|
|
|
22
|
+
const IMPORT_RESOLVER_CONTEXT_FILENAMES: &[&str] = &[
|
|
23
|
+
"Cargo.toml",
|
|
24
|
+
"Cartfile",
|
|
25
|
+
"Package.swift",
|
|
26
|
+
"Podfile",
|
|
27
|
+
"build.gradle",
|
|
28
|
+
"build.gradle.kts",
|
|
29
|
+
"go.mod",
|
|
30
|
+
"jsconfig.json",
|
|
31
|
+
"package.json",
|
|
32
|
+
"pom.xml",
|
|
33
|
+
"project.pbxproj",
|
|
34
|
+
"pyproject.toml",
|
|
35
|
+
"tsconfig.json",
|
|
36
|
+
];
|
|
37
|
+
|
|
22
38
|
#[derive(Debug, Clone, Default)]
|
|
23
39
|
pub struct ArchitectureScanOptions {
|
|
24
40
|
pub config_path: Option<PathBuf>,
|
|
@@ -262,12 +278,10 @@ fn cached_clean_content_changed(
|
|
|
262
278
|
}
|
|
263
279
|
|
|
264
280
|
fn import_resolver_context_changed(changed_paths: &[String]) -> bool {
|
|
265
|
-
changed_paths
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
.is_some_and(|name| matches!(name, "go.mod" | "tsconfig.json" | "jsconfig.json"))
|
|
270
|
-
})
|
|
281
|
+
changed_paths
|
|
282
|
+
.iter()
|
|
283
|
+
.filter_map(|path| Path::new(path).file_name()?.to_str())
|
|
284
|
+
.any(|name| IMPORT_RESOLVER_CONTEXT_FILENAMES.contains(&name))
|
|
271
285
|
}
|
|
272
286
|
|
|
273
287
|
fn degraded_full_scan(
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
mod config;
|
|
2
|
+
mod config_findings;
|
|
3
|
+
mod explain;
|
|
2
4
|
mod model;
|
|
3
5
|
mod output;
|
|
4
6
|
mod rules;
|
|
@@ -11,14 +13,16 @@ use crate::models::NaomeError;
|
|
|
11
13
|
pub use config::{
|
|
12
14
|
default_architecture_config_text, ArchitectureConfig, ContextConfig, LayerConfig, RuleConfig,
|
|
13
15
|
};
|
|
16
|
+
pub use config_findings::config_findings_for;
|
|
17
|
+
pub use explain::format_architecture_explain;
|
|
14
18
|
pub use model::{
|
|
15
19
|
ArchitectureEdge, ArchitectureEdgeKind, ArchitectureGraph, ArchitectureMetadata,
|
|
16
20
|
ArchitectureNode, ArchitectureNodeKind, Severity, SourceRange,
|
|
17
21
|
};
|
|
18
22
|
pub use output::{
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
format_architecture_scan, format_architecture_validation, ArchitectureAgentFeedback,
|
|
24
|
+
ArchitectureConfigFinding, ArchitectureValidation, ArchitectureViolation, ViolationSummary,
|
|
25
|
+
ARCHITECTURE_RULE_IDS,
|
|
22
26
|
};
|
|
23
27
|
pub use scan::{scan_architecture, ArchitectureScanOptions, ArchitectureScanReport};
|
|
24
28
|
|
|
@@ -44,6 +44,7 @@ pub use install_plan::{MACHINE_OWNED_PATHS, PROJECT_OWNED_PATHS};
|
|
|
44
44
|
pub use intent::{evaluate_intent, format_intent, IntentDecision, PromptEvidence};
|
|
45
45
|
pub use journal::{append_task_journal, TaskJournalEntry};
|
|
46
46
|
pub use models::{Decision, NaomeError};
|
|
47
|
+
pub use paths::{matches_any as path_matches_any, naomeignore_patterns};
|
|
47
48
|
pub use quality::{
|
|
48
49
|
check_repository_quality, check_repository_quality_paths, check_semantic_legacy,
|
|
49
50
|
check_semantic_legacy_paths, clear_quality_cache, explain_repository_structure,
|
|
@@ -376,6 +376,36 @@ fn unresolved_relative_imports_are_represented_explicitly() {
|
|
|
376
376
|
}));
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
+
#[test]
|
|
380
|
+
fn rust_root_module_item_imports_resolve_to_crate_root_files() {
|
|
381
|
+
let repo = FixtureRepo::new();
|
|
382
|
+
repo.write(
|
|
383
|
+
"src/lib.rs",
|
|
384
|
+
"pub fn repository_model_drift() {}\npub fn matches_any() {}\n",
|
|
385
|
+
);
|
|
386
|
+
repo.write(
|
|
387
|
+
"src/quality/mod.rs",
|
|
388
|
+
"use crate::repository_model_drift;\npub fn check() { repository_model_drift(); }\n",
|
|
389
|
+
);
|
|
390
|
+
repo.write(
|
|
391
|
+
"src/paths.rs",
|
|
392
|
+
"use super::matches_any;\npub fn check() { matches_any(); }\n",
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
let scan = scan_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
|
|
396
|
+
|
|
397
|
+
assert!(scan.graph.edges.iter().any(|edge| {
|
|
398
|
+
edge.kind == ArchitectureEdgeKind::Imports
|
|
399
|
+
&& edge.from == "file:src/quality/mod.rs"
|
|
400
|
+
&& edge.to == "file:src/lib.rs"
|
|
401
|
+
}));
|
|
402
|
+
assert!(scan.graph.edges.iter().any(|edge| {
|
|
403
|
+
edge.kind == ArchitectureEdgeKind::Imports
|
|
404
|
+
&& edge.from == "file:src/paths.rs"
|
|
405
|
+
&& edge.to == "file:src/lib.rs"
|
|
406
|
+
}));
|
|
407
|
+
}
|
|
408
|
+
|
|
379
409
|
#[test]
|
|
380
410
|
fn validates_forbidden_layer_dependency_from_import_edges() {
|
|
381
411
|
let repo = FixtureRepo::new();
|