@lamentis/naome 1.3.10 → 1.3.11

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.
@@ -0,0 +1,334 @@
1
+ use std::collections::BTreeSet;
2
+ use std::fs;
3
+ use std::path::{Path, PathBuf};
4
+
5
+ use super::{external_target, language_for_path};
6
+ use crate::architecture::scan::ImportTarget;
7
+
8
+ pub(super) fn resolve_import(
9
+ root: &Path,
10
+ from_path: &str,
11
+ specifier: &str,
12
+ repository_files: &BTreeSet<String>,
13
+ ) -> ImportTarget {
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
+ if specifier.starts_with('.') {
23
+ if from_path.ends_with(".py") {
24
+ return resolve_python_relative(from_path, specifier, repository_files)
25
+ .map(ImportTarget::File)
26
+ .unwrap_or_else(|| ImportTarget::Unknown(specifier.to_string()));
27
+ }
28
+ return resolve_relative(from_path, specifier, repository_files, language)
29
+ .map(ImportTarget::File)
30
+ .unwrap_or_else(|| ImportTarget::Unknown(specifier.to_string()));
31
+ }
32
+ if from_path.ends_with(".rs") && is_rust_local(specifier) {
33
+ return resolve_rust_local(from_path, specifier, repository_files)
34
+ .map(ImportTarget::File)
35
+ .unwrap_or_else(|| ImportTarget::Unknown(specifier.to_string()));
36
+ }
37
+ if let Some(path) = resolve_repo_absolute(specifier, repository_files, language) {
38
+ return ImportTarget::File(path);
39
+ }
40
+ if from_path.ends_with(".go") {
41
+ if let Some(path) = resolve_go_module_path(root, from_path, specifier, repository_files) {
42
+ return ImportTarget::File(path);
43
+ }
44
+ }
45
+ external_target(specifier)
46
+ }
47
+
48
+ fn resolve_relative(
49
+ from_path: &str,
50
+ specifier: &str,
51
+ repository_files: &BTreeSet<String>,
52
+ language: Option<&str>,
53
+ ) -> Option<String> {
54
+ let base = Path::new(from_path)
55
+ .parent()
56
+ .unwrap_or_else(|| Path::new(""));
57
+ let candidate = normalize(base.join(specifier));
58
+ resolve_candidates(&candidate, repository_files, language)
59
+ }
60
+
61
+ fn resolve_python_relative(
62
+ from_path: &str,
63
+ specifier: &str,
64
+ repository_files: &BTreeSet<String>,
65
+ ) -> Option<String> {
66
+ let dots = specifier
67
+ .chars()
68
+ .take_while(|character| *character == '.')
69
+ .count();
70
+ let module = specifier[dots..].replace('.', "/");
71
+ let mut base = Path::new(from_path)
72
+ .parent()
73
+ .unwrap_or_else(|| Path::new(""));
74
+ for _ in 1..dots {
75
+ base = base.parent().unwrap_or_else(|| Path::new(""));
76
+ }
77
+ resolve_progressively(
78
+ &normalize(base.join(module)),
79
+ repository_files,
80
+ Some("python"),
81
+ '/',
82
+ )
83
+ }
84
+
85
+ fn resolve_repo_absolute(
86
+ specifier: &str,
87
+ repository_files: &BTreeSet<String>,
88
+ language: Option<&str>,
89
+ ) -> Option<String> {
90
+ let candidate = specifier.strip_prefix("@/").unwrap_or(specifier);
91
+ if candidate.starts_with("src/")
92
+ || candidate.starts_with("packages/")
93
+ || candidate.starts_with("apps/")
94
+ {
95
+ return resolve_candidates(candidate, repository_files, language);
96
+ }
97
+ let dotted_candidate = specifier.replace('.', "/");
98
+ if dotted_candidate.starts_with("src/")
99
+ || dotted_candidate.starts_with("packages/")
100
+ || dotted_candidate.starts_with("apps/")
101
+ {
102
+ return resolve_progressively(&dotted_candidate, repository_files, language, '/');
103
+ }
104
+ None
105
+ }
106
+
107
+ fn resolve_rust_local(
108
+ from_path: &str,
109
+ specifier: &str,
110
+ repository_files: &BTreeSet<String>,
111
+ ) -> Option<String> {
112
+ let mut parts = specifier
113
+ .split("::")
114
+ .map(str::trim)
115
+ .filter(|part| !part.is_empty())
116
+ .collect::<Vec<_>>();
117
+ if parts.is_empty() {
118
+ return None;
119
+ }
120
+ match parts[0] {
121
+ "crate" => {
122
+ parts.remove(0);
123
+ let crate_root = rust_crate_src_root(from_path);
124
+ resolve_progressively(
125
+ &normalize(Path::new(&crate_root).join(parts.join("/"))),
126
+ repository_files,
127
+ Some("rust"),
128
+ '/',
129
+ )
130
+ }
131
+ "self" => {
132
+ parts.remove(0);
133
+ let base = PathBuf::from(rust_module_directory(from_path));
134
+ resolve_progressively(
135
+ &normalize(base.join(parts.join("/"))),
136
+ repository_files,
137
+ Some("rust"),
138
+ '/',
139
+ )
140
+ }
141
+ "super" => {
142
+ let mut base = PathBuf::from(rust_module_directory(from_path));
143
+ while parts.first() == Some(&"super") {
144
+ parts.remove(0);
145
+ base.pop();
146
+ }
147
+ resolve_progressively(
148
+ &normalize(base.join(parts.join("/"))),
149
+ repository_files,
150
+ Some("rust"),
151
+ '/',
152
+ )
153
+ }
154
+ _ => None,
155
+ }
156
+ }
157
+
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
+ fn rust_module_directory(from_path: &str) -> String {
172
+ let source = Path::new(from_path);
173
+ let parent = source.parent().unwrap_or_else(|| Path::new(""));
174
+ let stem = source
175
+ .file_stem()
176
+ .and_then(|value| value.to_str())
177
+ .unwrap_or("");
178
+ if matches!(stem, "lib" | "main" | "mod") {
179
+ normalize(parent)
180
+ } else {
181
+ normalize(parent.join(stem))
182
+ }
183
+ }
184
+
185
+ fn rust_crate_src_root(from_path: &str) -> String {
186
+ let parts = from_path.split('/').collect::<Vec<_>>();
187
+ if let Some(index) = parts.iter().rposition(|part| *part == "src") {
188
+ return parts[..=index].join("/");
189
+ }
190
+ "src".to_string()
191
+ }
192
+
193
+ fn resolve_go_module_path(
194
+ root: &Path,
195
+ from_path: &str,
196
+ specifier: &str,
197
+ repository_files: &BTreeSet<String>,
198
+ ) -> Option<String> {
199
+ let module = go_module_for_file(root, from_path, repository_files)?;
200
+ let suffix = specifier.strip_prefix(&module)?;
201
+ let suffix = suffix.strip_prefix('/')?;
202
+ if suffix.is_empty() {
203
+ return None;
204
+ }
205
+ if let Some(path) = resolve_candidates(suffix, repository_files, Some("go")) {
206
+ return Some(path);
207
+ }
208
+ let package_prefix = format!("{suffix}/");
209
+ repository_files
210
+ .iter()
211
+ .find(|path| path.starts_with(&package_prefix) && path.ends_with(".go"))
212
+ .cloned()
213
+ }
214
+
215
+ fn go_module_for_file(
216
+ root: &Path,
217
+ from_path: &str,
218
+ repository_files: &BTreeSet<String>,
219
+ ) -> Option<String> {
220
+ let mut current = Path::new(from_path).parent().unwrap_or_else(|| Path::new(""));
221
+ loop {
222
+ let go_mod_path = normalize(current.join("go.mod"));
223
+ if repository_files.contains(&go_mod_path) {
224
+ return go_mod_module(root, &go_mod_path);
225
+ }
226
+ let Some(parent) = current.parent() else {
227
+ break;
228
+ };
229
+ if parent == current {
230
+ break;
231
+ }
232
+ current = parent;
233
+ }
234
+ None
235
+ }
236
+
237
+ fn go_mod_module(root: &Path, go_mod_path: &str) -> Option<String> {
238
+ let content = fs::read_to_string(root.join(go_mod_path)).ok()?;
239
+ content.lines().find_map(|line| {
240
+ let line = line.trim();
241
+ let module = line.strip_prefix("module ")?;
242
+ module.split_whitespace().next().map(ToString::to_string)
243
+ })
244
+ }
245
+
246
+ fn resolve_candidates(
247
+ candidate: &str,
248
+ repository_files: &BTreeSet<String>,
249
+ language: Option<&str>,
250
+ ) -> Option<String> {
251
+ candidate_paths(candidate, language)
252
+ .into_iter()
253
+ .find(|path| repository_files.contains(path))
254
+ }
255
+
256
+ fn resolve_progressively(
257
+ candidate: &str,
258
+ repository_files: &BTreeSet<String>,
259
+ language: Option<&str>,
260
+ separator: char,
261
+ ) -> Option<String> {
262
+ let mut current = candidate.to_string();
263
+ loop {
264
+ if let Some(path) = resolve_candidates(&current, repository_files, language) {
265
+ return Some(path);
266
+ }
267
+ let Some((parent, _)) = current.rsplit_once(separator) else {
268
+ return None;
269
+ };
270
+ current = parent.to_string();
271
+ }
272
+ }
273
+
274
+ fn candidate_paths(candidate: &str, language: Option<&str>) -> Vec<String> {
275
+ let extensions = extensions_for_language(language);
276
+ let mut candidates = extensions
277
+ .iter()
278
+ .map(|extension| format!("{candidate}{extension}"))
279
+ .collect::<Vec<_>>();
280
+ match language {
281
+ Some("rust") => candidates.push(format!("{candidate}/mod.rs")),
282
+ Some("python") => candidates.push(format!("{candidate}/__init__.py")),
283
+ Some("go") => {}
284
+ Some("typescript") | Some("javascript") => candidates.extend([
285
+ format!("{candidate}/index.ts"),
286
+ format!("{candidate}/index.tsx"),
287
+ format!("{candidate}/index.js"),
288
+ format!("{candidate}/index.jsx"),
289
+ ]),
290
+ _ => candidates.extend([
291
+ format!("{candidate}/index.ts"),
292
+ format!("{candidate}/index.tsx"),
293
+ format!("{candidate}/index.js"),
294
+ format!("{candidate}/index.jsx"),
295
+ format!("{candidate}/mod.rs"),
296
+ format!("{candidate}/__init__.py"),
297
+ ]),
298
+ }
299
+ candidates
300
+ }
301
+
302
+ fn extensions_for_language(language: Option<&str>) -> &'static [&'static str] {
303
+ match language {
304
+ Some("rust") => &["", ".rs"],
305
+ Some("python") => &["", ".py"],
306
+ Some("go") => &["", ".go"],
307
+ Some("typescript") => &["", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
308
+ Some("javascript") => &["", ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"],
309
+ _ => &[
310
+ "", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".rs", ".py", ".go",
311
+ ],
312
+ }
313
+ }
314
+
315
+ fn is_rust_local(specifier: &str) -> bool {
316
+ specifier.starts_with("crate::")
317
+ || specifier.starts_with("self::")
318
+ || specifier.starts_with("super::")
319
+ || specifier.starts_with('.')
320
+ }
321
+
322
+ fn normalize(path: impl AsRef<Path>) -> String {
323
+ let mut normalized = PathBuf::new();
324
+ for component in path.as_ref().components() {
325
+ match component {
326
+ std::path::Component::ParentDir => {
327
+ normalized.pop();
328
+ }
329
+ std::path::Component::CurDir => {}
330
+ other => normalized.push(other.as_os_str()),
331
+ }
332
+ }
333
+ normalized.to_string_lossy().replace('\\', "/")
334
+ }
@@ -0,0 +1,59 @@
1
+ use std::collections::BTreeSet;
2
+ use std::path::Path;
3
+
4
+ use super::{ImportFact, ImportTarget};
5
+
6
+ mod extractors;
7
+ mod resolver;
8
+
9
+ pub(super) fn extract_imports(
10
+ root: &Path,
11
+ path: &str,
12
+ content: &str,
13
+ repository_files: &BTreeSet<String>,
14
+ ) -> Vec<ImportFact> {
15
+ let raw_imports = extractors::extract_raw_imports(path, content);
16
+ raw_imports
17
+ .into_iter()
18
+ .map(|raw| ImportFact {
19
+ target: resolver::resolve_import(root, path, &raw.specifier, repository_files),
20
+ specifier: raw.specifier,
21
+ source_range: Some(raw.source_range),
22
+ confidence: raw.confidence,
23
+ extractor: raw.extractor,
24
+ })
25
+ .collect()
26
+ }
27
+
28
+ pub(super) fn package_name(specifier: &str) -> String {
29
+ if let Some(rest) = specifier.strip_prefix('@') {
30
+ let mut parts = rest.split('/');
31
+ let scope = parts.next().unwrap_or_default();
32
+ let package = parts.next().unwrap_or_default();
33
+ return format!("@{scope}/{package}");
34
+ }
35
+ specifier
36
+ .split("::")
37
+ .next()
38
+ .unwrap_or(specifier)
39
+ .split('/')
40
+ .next()
41
+ .unwrap_or(specifier)
42
+ .to_string()
43
+ }
44
+
45
+ pub(super) fn external_target(specifier: &str) -> ImportTarget {
46
+ ImportTarget::ExternalDependency(package_name(specifier))
47
+ }
48
+
49
+ pub(super) fn language_for_path(path: &str) -> Option<&'static str> {
50
+ let extension = path.rsplit('.').next()?;
51
+ match extension {
52
+ "ts" | "tsx" => Some("typescript"),
53
+ "js" | "jsx" | "mjs" | "cjs" => Some("javascript"),
54
+ "rs" => Some("rust"),
55
+ "py" => Some("python"),
56
+ "go" => Some("go"),
57
+ _ => None,
58
+ }
59
+ }
@@ -10,6 +10,7 @@ use super::config::{read_architecture_config, ArchitectureConfig};
10
10
  use super::model::ArchitectureGraph;
11
11
 
12
12
  mod graph_builder;
13
+ mod imports;
13
14
  mod path_scan;
14
15
 
15
16
  #[derive(Debug, Clone, Default)]
@@ -42,6 +43,25 @@ pub struct FileFact {
42
43
  pub layers: Vec<String>,
43
44
  pub contexts: Vec<String>,
44
45
  pub ignored: Option<String>,
46
+ pub imports: Vec<ImportFact>,
47
+ }
48
+
49
+ #[derive(Debug, Clone, Serialize)]
50
+ #[serde(rename_all = "camelCase")]
51
+ pub struct ImportFact {
52
+ pub specifier: String,
53
+ pub target: ImportTarget,
54
+ pub source_range: Option<super::model::SourceRange>,
55
+ pub confidence: f32,
56
+ pub extractor: String,
57
+ }
58
+
59
+ #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
60
+ #[serde(rename_all = "camelCase")]
61
+ pub enum ImportTarget {
62
+ File(String),
63
+ ExternalDependency(String),
64
+ Unknown(String),
45
65
  }
46
66
 
47
67
  pub fn scan_architecture(