A single invalid byte, such as a Latin-1 `é` in a comment, made `read_to_string` fail and the whole file and its symbols were skipped. Files are now read as bytes: a NUL byte still marks a binary file, which is skipped with a diagnostic; otherwise invalid UTF-8 is decoded lossily (U+FFFD) and a warning diagnostic records it, so the file keeps its symbols and calls. Test: languages::a_non_utf8_byte_does_not_drop_the_whole_file fails before this change (latin1.rs has no symbols).
731 lines
23 KiB
Rust
731 lines
23 KiB
Rust
use brick::model::{EdgeKind, Node, NodeKind};
|
|
use brick::{Building, scan};
|
|
use std::fs;
|
|
|
|
fn project(files: &[(&str, &str)]) -> (tempfile::TempDir, Building) {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
for (path, text) in files {
|
|
let dest = dir.path().join(path);
|
|
fs::create_dir_all(dest.parent().unwrap()).unwrap();
|
|
fs::write(dest, text).unwrap();
|
|
}
|
|
let model = scan::scan(dir.path()).unwrap();
|
|
model.validate_current_schema().unwrap();
|
|
(dir, model)
|
|
}
|
|
|
|
fn symbol<'a>(model: &'a Building, path: &str, name: &str) -> &'a Node {
|
|
let nodes: Vec<_> = model
|
|
.nodes
|
|
.iter()
|
|
.filter(|n| {
|
|
n.path.as_deref() == Some(path)
|
|
&& n.name == name
|
|
&& matches!(n.kind, NodeKind::Function | NodeKind::Class)
|
|
})
|
|
.collect();
|
|
assert_eq!(nodes.len(), 1, "expected one {path}:{name}");
|
|
nodes[0]
|
|
}
|
|
|
|
fn calls(model: &Building, source: &Node, target: &Node) -> bool {
|
|
model
|
|
.edges
|
|
.iter()
|
|
.any(|e| e.kind == EdgeKind::Calls && e.source == source.id && e.target == target.id)
|
|
}
|
|
|
|
#[test]
|
|
fn relative_imports_are_source_relative_and_never_guess_a_missing_path() {
|
|
for (extension, import) in [
|
|
("ts", "import { work } from"),
|
|
("js", "import { work } from"),
|
|
("dart", "import"),
|
|
] {
|
|
let main = format!("a/main.{extension}");
|
|
let local = format!("a/util.{extension}");
|
|
let other = format!("b/util.{extension}");
|
|
let specifier = if extension == "dart" {
|
|
"./util.dart"
|
|
} else {
|
|
"./util"
|
|
};
|
|
let text = format!("{import} '{specifier}';\n{import} './missing/util';\n");
|
|
let (_, model) = project(&[(&main, &text), (&local, ""), (&other, "")]);
|
|
let imports: Vec<_> = model
|
|
.edges
|
|
.iter()
|
|
.filter(|e| e.kind == EdgeKind::Imports)
|
|
.map(|e| model.nodes.iter().find(|n| n.id == e.target).unwrap())
|
|
.collect();
|
|
assert_eq!(imports.len(), 2);
|
|
assert!(imports.iter().any(|n| n.path.as_deref() == Some(&local)));
|
|
assert!(
|
|
imports
|
|
.iter()
|
|
.any(|n| n.kind == NodeKind::External && n.name == "./missing/util")
|
|
);
|
|
}
|
|
let (_, model) = project(&[("a/main.ts", "import './missing/util';"), ("b/util.ts", "")]);
|
|
assert!(
|
|
model
|
|
.edges
|
|
.iter()
|
|
.filter(|e| e.kind == EdgeKind::Imports)
|
|
.all(|e| model
|
|
.nodes
|
|
.iter()
|
|
.any(|n| n.id == e.target && n.kind == NodeKind::External))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn resolution_respects_language_and_lexical_owners() {
|
|
let (_, model) = project(&[
|
|
(
|
|
"main.rs",
|
|
"fn caller() { foreign(); local(); }\nfn local() {}",
|
|
),
|
|
("other.js", "function foreign() {}\nfunction local() {}"),
|
|
(
|
|
"types.dart",
|
|
"class A { void run() { helper(); } void helper() {} }\nclass B { void helper() {} }",
|
|
),
|
|
]);
|
|
let caller = symbol(&model, "main.rs", "caller");
|
|
assert!(calls(&model, caller, symbol(&model, "main.rs", "local")));
|
|
assert!(!calls(
|
|
&model,
|
|
caller,
|
|
symbol(&model, "other.js", "foreign")
|
|
));
|
|
let a = symbol(&model, "types.dart", "A");
|
|
let helper = model
|
|
.nodes
|
|
.iter()
|
|
.find(|n| n.name == "helper" && n.parent.as_deref() == Some(&a.id))
|
|
.unwrap();
|
|
assert!(calls(&model, symbol(&model, "types.dart", "run"), helper));
|
|
}
|
|
|
|
#[test]
|
|
fn cpp_inline_definitions_and_constructors_are_symbols_not_calls() {
|
|
let (_, model) = project(&[(
|
|
"main.cpp",
|
|
"void helper() {}\nclass Worker {\npublic:\n Worker() { helper(); }\n void run() { helper(); }\n};\nint main() { helper(); return 0; }",
|
|
)]);
|
|
let class = model
|
|
.nodes
|
|
.iter()
|
|
.find(|n| n.kind == NodeKind::Class && n.name == "Worker")
|
|
.unwrap();
|
|
let constructor = model
|
|
.nodes
|
|
.iter()
|
|
.find(|n| n.kind == NodeKind::Function && n.name == "Worker")
|
|
.unwrap();
|
|
assert_eq!(constructor.parent.as_deref(), Some(class.id.as_str()));
|
|
let helper = symbol(&model, "main.cpp", "helper");
|
|
for source in [
|
|
constructor,
|
|
symbol(&model, "main.cpp", "run"),
|
|
symbol(&model, "main.cpp", "main"),
|
|
] {
|
|
assert!(calls(&model, source, helper));
|
|
}
|
|
assert_eq!(
|
|
model
|
|
.edges
|
|
.iter()
|
|
.filter(|e| e.kind == EdgeKind::Calls)
|
|
.count(),
|
|
3
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn javascript_and_typescript_declarations_have_correct_call_owners() {
|
|
for ext in ["js", "ts", "tsx"] {
|
|
let path = format!("main.{ext}");
|
|
let (_, model) = project(&[(
|
|
&path,
|
|
"export const arrow = () => target();\nexport default function main() { target(); }\nclass Service { run() { target(); } }\nfunction target() {}",
|
|
)]);
|
|
let target = symbol(&model, &path, "target");
|
|
for name in ["arrow", "main", "run"] {
|
|
assert!(calls(&model, symbol(&model, &path, name), target));
|
|
}
|
|
assert_eq!(
|
|
symbol(&model, &path, "run").parent.as_deref(),
|
|
Some(symbol(&model, &path, "Service").id.as_str())
|
|
);
|
|
assert_eq!(
|
|
model
|
|
.edges
|
|
.iter()
|
|
.filter(|e| e.kind == EdgeKind::Calls)
|
|
.count(),
|
|
3
|
|
);
|
|
}
|
|
let (_, model) = project(&[(
|
|
"main.ts",
|
|
"export interface Service { run(): void; }\nexport function identity<T>(x: T): T { return x; }\nexport const arrow = () => identity(1);",
|
|
)]);
|
|
symbol(&model, "main.ts", "Service");
|
|
assert!(calls(
|
|
&model,
|
|
symbol(&model, "main.ts", "arrow"),
|
|
symbol(&model, "main.ts", "identity")
|
|
));
|
|
assert_eq!(
|
|
model
|
|
.edges
|
|
.iter()
|
|
.filter(|e| e.kind == EdgeKind::Calls)
|
|
.count(),
|
|
1
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dart_multiline_method_owns_its_body_calls() {
|
|
let (_, model) = project(&[(
|
|
"main.dart",
|
|
"class Service {\n Future<void> run(\n int value,\n ) async {\n helper();\n }\n void helper() {}\n}",
|
|
)]);
|
|
let run = symbol(&model, "main.dart", "run");
|
|
assert_eq!(
|
|
run.parent.as_deref(),
|
|
Some(symbol(&model, "main.dart", "Service").id.as_str())
|
|
);
|
|
assert!(calls(&model, run, symbol(&model, "main.dart", "helper")));
|
|
assert_eq!(
|
|
model
|
|
.edges
|
|
.iter()
|
|
.filter(|e| e.kind == EdgeKind::Calls)
|
|
.count(),
|
|
1
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rust_impl_owners_and_const_functions_survive_unrelated_changes() {
|
|
let (dir, before) = project(&[(
|
|
"main.rs",
|
|
"struct A;\nstruct B;\nimpl A { fn run(&self) {} }\nimpl B { fn run(&self) {} }\nconst fn constant() -> u32 { 1 }",
|
|
)]);
|
|
symbol(&before, "main.rs", "constant");
|
|
for owner in ["A", "B"] {
|
|
let class = symbol(&before, "main.rs", owner);
|
|
assert_eq!(
|
|
before
|
|
.nodes
|
|
.iter()
|
|
.filter(|n| n.name == "run" && n.parent.as_deref() == Some(&class.id))
|
|
.count(),
|
|
1
|
|
);
|
|
}
|
|
assert!(!before.edges.iter().any(|e| e.kind == EdgeKind::Calls));
|
|
let path = dir.path().join("main.rs");
|
|
fs::write(
|
|
&path,
|
|
format!(
|
|
"// comment\nstruct Unrelated;\n{}",
|
|
fs::read_to_string(&path).unwrap()
|
|
),
|
|
)
|
|
.unwrap();
|
|
let after = scan::scan(dir.path()).unwrap();
|
|
for node in before
|
|
.nodes
|
|
.iter()
|
|
.filter(|n| matches!(n.kind, NodeKind::Class | NodeKind::Function))
|
|
{
|
|
assert!(after.nodes.iter().any(|n| n.id == node.id));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn block_comments_and_strings_cannot_create_symbols_or_calls() {
|
|
for (ext, code) in [
|
|
(
|
|
"rs",
|
|
"/* fn ghost() { phantom(); } */\nfn real() { let s = r#\"ghost();\"#; }",
|
|
),
|
|
(
|
|
"cpp",
|
|
"/* void ghost() { phantom(); } */\nvoid real() { auto s = R\"(ghost();)\"; }",
|
|
),
|
|
(
|
|
"js",
|
|
"/* function ghost() { phantom(); } */\nfunction real() { const s = `ghost();`; }",
|
|
),
|
|
(
|
|
"ts",
|
|
"/* function ghost() { phantom(); } */\nfunction real() { const s = 'https://example/ghost()'; }",
|
|
),
|
|
(
|
|
"dart",
|
|
"/* void ghost() { phantom(); } */\nvoid real() { final s = '''ghost();'''; }",
|
|
),
|
|
] {
|
|
let path = format!("main.{ext}");
|
|
let (_, model) = project(&[(&path, code)]);
|
|
symbol(&model, &path, "real");
|
|
assert!(
|
|
!model
|
|
.nodes
|
|
.iter()
|
|
.any(|n| ["ghost", "phantom"].contains(&n.name.as_str())),
|
|
"{ext}"
|
|
);
|
|
assert!(
|
|
!model.edges.iter().any(|e| e.kind == EdgeKind::Calls),
|
|
"{ext}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn symlinks_are_skipped_with_diagnostics_including_cycles_and_source_files() {
|
|
use std::os::unix::fs::symlink;
|
|
let (dir, _) = project(&[("src/real.rs", "fn real() {}")]);
|
|
let outside = tempfile::tempdir().unwrap();
|
|
fs::write(outside.path().join("secret.rs"), "fn secret() {}").unwrap();
|
|
symlink(outside.path(), dir.path().join("outside")).unwrap();
|
|
symlink(dir.path(), dir.path().join("cycle")).unwrap();
|
|
symlink(
|
|
outside.path().join("secret.rs"),
|
|
dir.path().join("secret.rs"),
|
|
)
|
|
.unwrap();
|
|
symlink(dir.path().join("src"), dir.path().join("alias")).unwrap();
|
|
let model = scan::scan(dir.path()).unwrap();
|
|
assert_eq!(
|
|
model
|
|
.nodes
|
|
.iter()
|
|
.filter(|n| n.kind == NodeKind::File)
|
|
.count(),
|
|
1
|
|
);
|
|
assert_eq!(model.diagnostics.len(), 4);
|
|
assert!(
|
|
model
|
|
.diagnostics
|
|
.iter()
|
|
.all(|d| d.message.contains("symlink") && !d.path.as_deref().unwrap().starts_with('/'))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn namespaces_do_not_cross_resolve_and_do_not_renumber_unrelated_ids() {
|
|
let (dir, before) = project(&[(
|
|
"main.cpp",
|
|
"namespace a { void helper() {} void run() { helper(); } }\nnamespace b { void helper() {} }",
|
|
)]);
|
|
let run = symbol(&before, "main.cpp", "run");
|
|
let helper = before
|
|
.nodes
|
|
.iter()
|
|
.find(|n| {
|
|
n.name == "helper" && n.attributes.get("source.scope") == Some(&serde_json::json!("a"))
|
|
})
|
|
.unwrap();
|
|
assert!(calls(&before, run, helper));
|
|
let path = dir.path().join("main.cpp");
|
|
fs::write(
|
|
&path,
|
|
format!(
|
|
"namespace unrelated {{ void helper() {{}} }}\n{}",
|
|
fs::read_to_string(&path).unwrap()
|
|
),
|
|
)
|
|
.unwrap();
|
|
let after = scan::scan(dir.path()).unwrap();
|
|
assert!(after.nodes.iter().any(|n| n.id == helper.id));
|
|
}
|
|
|
|
#[test]
|
|
fn explicitly_imported_named_functions_resolve_without_global_name_guessing() {
|
|
let (_, model) = project(&[
|
|
(
|
|
"app/main.ts",
|
|
"import { work as execute } from './util';\nexport function run() { execute(); missing(); }",
|
|
),
|
|
("app/util.ts", "export function work() {}"),
|
|
(
|
|
"unrelated/util.ts",
|
|
"export function work() {}\nexport function missing() {}",
|
|
),
|
|
]);
|
|
let run = symbol(&model, "app/main.ts", "run");
|
|
assert!(calls(&model, run, symbol(&model, "app/util.ts", "work")));
|
|
assert!(!calls(
|
|
&model,
|
|
run,
|
|
symbol(&model, "unrelated/util.ts", "missing")
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn malformed_source_reports_errors_without_inventing_calls_from_recovery_text() {
|
|
let (_, model) = project(&[(
|
|
"main.ts",
|
|
"function complete() {}\nfunction broken( { !!! phantom()",
|
|
)]);
|
|
symbol(&model, "main.ts", "complete");
|
|
assert!(!model.diagnostics.is_empty());
|
|
assert!(!model.nodes.iter().any(|n| n.name == "phantom"));
|
|
}
|
|
|
|
#[test]
|
|
fn imports_never_resolve_to_nonexported_or_type_only_symbols() {
|
|
let (_, model) = project(&[
|
|
(
|
|
"main.ts",
|
|
"import { hidden } from './util';\nimport type { declared } from './util';\nfunction run() { hidden(); declared(); }",
|
|
),
|
|
(
|
|
"util.ts",
|
|
"function hidden() {}\nexport function declared() {}",
|
|
),
|
|
]);
|
|
let run = symbol(&model, "main.ts", "run");
|
|
for name in ["hidden", "declared"] {
|
|
assert!(!calls(&model, run, symbol(&model, "util.ts", name)));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn member_calls_stay_heuristic_and_unresolved_in_all_languages() {
|
|
for (ext, code) in [
|
|
("cpp", "void helper() {}\nvoid run() { object.helper(); }"),
|
|
(
|
|
"js",
|
|
"function helper() {}\nfunction run() { object.helper(); }",
|
|
),
|
|
(
|
|
"ts",
|
|
"function helper() {}\nfunction run() { object.helper(); }",
|
|
),
|
|
("dart", "void helper() {}\nvoid run() { object.helper(); }"),
|
|
("rs", "fn helper() {}\nfn run() { object.helper(); }"),
|
|
] {
|
|
let path = format!("main.{ext}");
|
|
let (_, model) = project(&[(&path, code)]);
|
|
assert!(!calls(
|
|
&model,
|
|
symbol(&model, &path, "run"),
|
|
symbol(&model, &path, "helper")
|
|
));
|
|
let edge = model
|
|
.edges
|
|
.iter()
|
|
.find(|e| e.kind == EdgeKind::Calls)
|
|
.unwrap();
|
|
assert!(
|
|
edge.evidence
|
|
.iter()
|
|
.all(|e| e.lane == brick::model::EvidenceLane::Heuristic
|
|
&& e.confidence == Some(brick::model::Confidence::Score(0.65))
|
|
&& e.provenance.starts_with(&path))
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_parameter_bindings_do_not_resolve_to_same_named_functions() {
|
|
for (ext, code) in [
|
|
(
|
|
"js",
|
|
"function helper() {}\nfunction run(helper) { helper(); }",
|
|
),
|
|
(
|
|
"ts",
|
|
"function helper() {}\nfunction run(helper: () => void) { helper(); }",
|
|
),
|
|
("rs", "fn helper() {}\nfn run(helper: fn()) { helper(); }"),
|
|
(
|
|
"dart",
|
|
"void helper() {}\nvoid run(void Function() helper) { helper(); }",
|
|
),
|
|
(
|
|
"cpp",
|
|
"void helper() {}\nvoid run(void (*helper)()) { helper(); }",
|
|
),
|
|
] {
|
|
let path = format!("main.{ext}");
|
|
let (_, model) = project(&[(&path, code)]);
|
|
assert!(
|
|
!calls(
|
|
&model,
|
|
symbol(&model, &path, "run"),
|
|
symbol(&model, &path, "helper")
|
|
),
|
|
"{ext}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn qualified_rust_module_calls_use_exact_paths_and_visibility() {
|
|
let (_, model) = project(&[
|
|
(
|
|
"src/api.rs",
|
|
"fn run() { crate::storage::query(); crate::other::query(); crate::storage::hidden(); }",
|
|
),
|
|
("src/storage.rs", "pub fn query() {}\nfn hidden() {}"),
|
|
("unrelated/storage.rs", "pub fn query() {}"),
|
|
]);
|
|
let run = symbol(&model, "src/api.rs", "run");
|
|
assert!(calls(
|
|
&model,
|
|
run,
|
|
symbol(&model, "src/storage.rs", "query")
|
|
));
|
|
assert!(!calls(
|
|
&model,
|
|
run,
|
|
symbol(&model, "src/storage.rs", "hidden")
|
|
));
|
|
assert!(!calls(
|
|
&model,
|
|
run,
|
|
symbol(&model, "unrelated/storage.rs", "query")
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn unresolved_impl_cannot_use_a_method_local_type_as_its_own_owner() {
|
|
let (_, model) = project(&[("main.rs", "impl Missing { fn run() { struct Missing; } }")]);
|
|
model.validate_current_schema().unwrap();
|
|
assert!(
|
|
model
|
|
.diagnostics
|
|
.iter()
|
|
.any(|d| d.message.contains("unresolved impl owner"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn cpp_parent_relative_includes_resolve_without_basename_fallback() {
|
|
let (_, model) = project(&[
|
|
(
|
|
"src/main.cpp",
|
|
"#include \"../include/base.h\"\n#include \"../missing/base.h\"\n",
|
|
),
|
|
("include/base.h", "struct Base {};"),
|
|
]);
|
|
let imports: Vec<_> = model
|
|
.edges
|
|
.iter()
|
|
.filter(|e| e.kind == EdgeKind::Imports)
|
|
.map(|e| model.nodes.iter().find(|n| n.id == e.target).unwrap())
|
|
.collect();
|
|
assert_eq!(imports.len(), 2);
|
|
assert!(
|
|
imports
|
|
.iter()
|
|
.any(|n| n.path.as_deref() == Some("include/base.h"))
|
|
);
|
|
assert!(
|
|
imports
|
|
.iter()
|
|
.any(|n| n.kind == NodeKind::External && n.name == "../missing/base.h")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn generic_type_arguments_are_not_base_classes() {
|
|
// `extends Bloc<Ev, St>` produced inherits edges to Ev and St: the base walker
|
|
// descended into the type-argument list. On pipe 39 of 145 Dart inherits edges
|
|
// (27%) pointed at type arguments. C++ `: public Base<Arg>` shared the walker.
|
|
let (_, model) = project(&[
|
|
(
|
|
"lib/bloc.dart",
|
|
"class Ev {}\nclass St {}\nclass Bloc<E, S> {}\nclass MyBloc extends Bloc<Ev, St> {}\n",
|
|
),
|
|
(
|
|
"src/base.cpp",
|
|
"template <class T> class Base {};\nclass Arg {};\nclass Derived : public Base<Arg> {};\n",
|
|
),
|
|
]);
|
|
let inherits = |source: &Node| {
|
|
let mut names: Vec<String> = model
|
|
.edges
|
|
.iter()
|
|
.filter(|e| e.kind == EdgeKind::Inherits && e.source == source.id)
|
|
.map(|e| {
|
|
model
|
|
.nodes
|
|
.iter()
|
|
.find(|n| n.id == e.target)
|
|
.unwrap()
|
|
.name
|
|
.clone()
|
|
})
|
|
.collect();
|
|
names.sort();
|
|
names
|
|
};
|
|
assert_eq!(
|
|
inherits(symbol(&model, "lib/bloc.dart", "MyBloc")),
|
|
["Bloc"]
|
|
);
|
|
assert_eq!(
|
|
inherits(symbol(&model, "src/base.cpp", "Derived")),
|
|
["Base"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn python_calls_keep_their_function_and_resolve_only_what_is_visible() {
|
|
let (_, model) = project(&[
|
|
(
|
|
"pkg/a.py",
|
|
"def process(x):\n return x\n\ndef unrelated():\n pass\n",
|
|
),
|
|
(
|
|
"pkg/b.py",
|
|
"class Base:\n pass\n\ndef helper():\n pass\n",
|
|
),
|
|
(
|
|
"pkg/c.py",
|
|
// A blank line and a comment inside the body used to pop the function
|
|
// scope, attributing the later calls to the file.
|
|
"from pkg.b import Base\n\nclass Child(Base):\n def go(self):\n x = 1\n\n # comment\n helper()\n process(1)\n unrelated()\n",
|
|
),
|
|
(
|
|
"pkg/d.py",
|
|
"from pkg.a import process\n\ndef use():\n return process(2)\n",
|
|
),
|
|
]);
|
|
let go = symbol(&model, "pkg/c.py", "go");
|
|
let outgoing = |source: &Node| {
|
|
let mut found: Vec<(String, String)> = model
|
|
.edges
|
|
.iter()
|
|
.filter(|e| e.kind == EdgeKind::Calls && e.source == source.id)
|
|
.map(|e| {
|
|
let target = model.nodes.iter().find(|n| n.id == e.target).unwrap();
|
|
(target.name.clone(), target.path.clone().unwrap_or_default())
|
|
})
|
|
.collect();
|
|
found.sort();
|
|
found
|
|
};
|
|
// helper comes from the imported pkg/b.py; process and unrelated are never
|
|
// imported by c.py, so they stay external instead of resolving by unique name.
|
|
assert_eq!(
|
|
outgoing(go),
|
|
[
|
|
("helper".to_owned(), "pkg/b.py".to_owned()),
|
|
("process".to_owned(), String::new()),
|
|
("unrelated".to_owned(), String::new()),
|
|
]
|
|
);
|
|
// An imported name still resolves across files.
|
|
assert_eq!(
|
|
outgoing(symbol(&model, "pkg/d.py", "use")),
|
|
[("process".to_owned(), "pkg/a.py".to_owned())]
|
|
);
|
|
// A class header is not a call.
|
|
let file_c = model
|
|
.nodes
|
|
.iter()
|
|
.find(|n| n.kind == NodeKind::File && n.path.as_deref() == Some("pkg/c.py"))
|
|
.unwrap();
|
|
assert!(
|
|
!model
|
|
.edges
|
|
.iter()
|
|
.any(|e| e.kind == EdgeKind::Calls && e.source == file_c.id),
|
|
"file-level call edges in pkg/c.py"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dart_calls_resolve_through_imported_libraries() {
|
|
// Dart imports expose a library's public top-level names without a binding per
|
|
// name, so no cross-file Dart call ever resolved even when its import did.
|
|
let (_, model) = project(&[
|
|
("pubspec.yaml", "name: demo\n"),
|
|
(
|
|
"lib/util.dart",
|
|
"int helper() => 1;\nint _secret() => 2;\nclass Tool { int use() => 3; }\n",
|
|
),
|
|
("lib/other.dart", "int shared() => 4;\n"),
|
|
("lib/twin.dart", "int shared() => 5;\n"),
|
|
(
|
|
"lib/main.dart",
|
|
"import 'util.dart';\nimport 'package:demo/other.dart';\nimport 'twin.dart';\nvoid run() {\n helper();\n _secret();\n shared();\n}\n",
|
|
),
|
|
// Clean negative: no import, no edge.
|
|
("lib/stranger.dart", "void stray() {\n helper();\n}\n"),
|
|
]);
|
|
let targets = |source: &Node| {
|
|
let mut found: Vec<(String, String, String)> = model
|
|
.edges
|
|
.iter()
|
|
.filter(|e| e.kind == EdgeKind::Calls && e.source == source.id)
|
|
.map(|e| {
|
|
let target = model.nodes.iter().find(|n| n.id == e.target).unwrap();
|
|
(
|
|
target.name.clone(),
|
|
target.path.clone().unwrap_or_default(),
|
|
e.attributes
|
|
.get("source.unresolved-reason")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("")
|
|
.to_owned(),
|
|
)
|
|
})
|
|
.collect();
|
|
found.sort();
|
|
found
|
|
};
|
|
assert_eq!(
|
|
targets(symbol(&model, "lib/main.dart", "run")),
|
|
[
|
|
("_secret".into(), String::new(), "not-visible".into()),
|
|
("helper".into(), "lib/util.dart".into(), String::new()),
|
|
("shared".into(), String::new(), "ambiguous".into()),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
targets(symbol(&model, "lib/stranger.dart", "stray")),
|
|
[("helper".into(), String::new(), "not-visible".into())]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_non_utf8_byte_does_not_drop_the_whole_file() {
|
|
// One Latin-1 `é` in a comment made the whole file (and its functions) vanish.
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let mut source = b"// caf".to_vec();
|
|
source.push(0xE9);
|
|
source.extend_from_slice(b"\nfn kept() {}\nfn caller() { kept(); }\n");
|
|
fs::write(dir.path().join("latin1.rs"), source).unwrap();
|
|
fs::write(dir.path().join("blob.rs"), [0u8, 159, 146, 150]).unwrap();
|
|
let model = scan::scan(dir.path()).unwrap();
|
|
let caller = symbol(&model, "latin1.rs", "caller");
|
|
assert!(calls(&model, caller, symbol(&model, "latin1.rs", "kept")));
|
|
assert!(
|
|
model
|
|
.diagnostics
|
|
.iter()
|
|
.any(|d| d.path.as_deref() == Some("latin1.rs") && d.message.contains("non-UTF-8"))
|
|
);
|
|
// A file with NUL bytes is binary, not source: still skipped with a diagnostic.
|
|
assert!(
|
|
!model
|
|
.nodes
|
|
.iter()
|
|
.any(|n| n.path.as_deref() == Some("blob.rs"))
|
|
);
|
|
}
|