Since #5 Python calls resolve only through import edges, but the lexical importer could not produce those edges for the common forms: - `from .util import helper` never matched (the regex needed a leading letter), so the call went unresolved; - `from pkg import f` looked up the stem "pkg" instead of pkg/__init__.py; - `from x.common import f` failed when y/common.py also existed, and `from other.common import f` (a module the repository does not have) resolved to y/common.py by basename. Imports now resolve Python-specifically: leading dots are relative to the importing package, and a dotted name maps to `a/b.py` or `a/b/__init__.py` from the repository root or `src/`. Only a single-component name (`import foo`) may still fall back to a unique module basename; a dotted name never does. `from . import x` stays unresolved (the module is in the import list, not the name). Tests (both failed before): relative, package and duplicate-stem imports resolve their calls to pkg/util.py, pkg/__init__.py and x/common.py; an unknown dotted module does not bind to y/common.py.
80 lines
2.8 KiB
Rust
80 lines
2.8 KiB
Rust
use brick::model::{Building, EdgeKind};
|
|
use brick::scan;
|
|
use std::fs;
|
|
|
|
fn scan_files(files: &[(&str, &str)]) -> Building {
|
|
let root = tempfile::tempdir().unwrap();
|
|
for (path, content) in files {
|
|
let file = root.path().join(path);
|
|
fs::create_dir_all(file.parent().unwrap()).unwrap();
|
|
fs::write(file, content).unwrap();
|
|
}
|
|
scan::scan(root.path()).unwrap()
|
|
}
|
|
|
|
/// (caller name, target path) for every resolved call.
|
|
fn resolved_calls(model: &Building) -> Vec<(String, String)> {
|
|
model
|
|
.edges
|
|
.iter()
|
|
.filter(|edge| edge.kind == EdgeKind::Calls)
|
|
.filter_map(|edge| {
|
|
let source = model.nodes.iter().find(|n| n.id == edge.source)?;
|
|
let target = model.nodes.iter().find(|n| n.id == edge.target)?;
|
|
Some((source.name.clone(), target.path.clone()?))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn relative_package_and_qualified_python_imports_resolve_to_the_right_file() {
|
|
// Python calls resolve only through import edges, and the lexical importer
|
|
// produced none for relative imports (the regex needed a leading letter),
|
|
// for packages (`from pkg import f` looked up the stem "pkg", not
|
|
// pkg/__init__.py), or when two files shared a stem.
|
|
let model = scan_files(&[
|
|
("pkg/__init__.py", "def from_package():\n return 1\n"),
|
|
("pkg/util.py", "def helper():\n return 1\n"),
|
|
(
|
|
"pkg/rel.py",
|
|
"from .util import helper\n\ndef use_relative():\n return helper()\n",
|
|
),
|
|
("x/common.py", "def only_here():\n return 1\n"),
|
|
("y/common.py", "def elsewhere():\n return 2\n"),
|
|
(
|
|
"main.py",
|
|
"from x.common import only_here\nfrom pkg import from_package\n\ndef run():\n only_here()\n return from_package()\n",
|
|
),
|
|
]);
|
|
let calls = resolved_calls(&model);
|
|
for (caller, target) in [
|
|
("use_relative", "pkg/util.py"),
|
|
("run", "x/common.py"),
|
|
("run", "pkg/__init__.py"),
|
|
] {
|
|
assert!(
|
|
calls
|
|
.iter()
|
|
.any(|(source, path)| source == caller && path == target),
|
|
"{caller} -> {target} missing: {calls:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn an_unknown_dotted_python_module_does_not_fall_back_to_a_basename() {
|
|
// `from other.common import f` names a module this repository does not have;
|
|
// matching the stem "common" to y/common.py would invent a dependency.
|
|
let model = scan_files(&[
|
|
("y/common.py", "def f():\n return 2\n"),
|
|
(
|
|
"main.py",
|
|
"from other.common import f\n\ndef run():\n return f()\n",
|
|
),
|
|
]);
|
|
let calls = resolved_calls(&model);
|
|
assert!(
|
|
!calls.iter().any(|(_, path)| path == "y/common.py"),
|
|
"{calls:?}"
|
|
);
|
|
}
|