fix/audit-findings #5

Merged
trevon merged 14 commits from fix/audit-findings into main 2026-09-25 02:39:30 +00:00
2 changed files with 97 additions and 3 deletions
Showing only changes of commit a03c36969e - Show all commits

feat(dart): resolve calls through imported libraries and own package: URIs

A Dart import exposes a library's public top-level names without a
per-name binding, so the binding-based lookup never matched and no
cross-file Dart call resolved, even when the import itself did (pipe's
VerifyCommand::run lost loadProject, analyzeProject, verifyAnalysis and
decodeBaselineEntries).

After the lexical-owner walk and binding lookup fail, a Dart call now
resolves to the one public (not `_`-prefixed) top-level declaration of
that name in the libraries the file imports; several such declarations
make it `ambiguous`, none leaves it external. `package:<name>/x.dart`
imports resolve to `lib/x.dart` when `<name>` is the root pubspec's
package; other packages stay external. `show`/`hide` combinators are not
modelled (noted as a follow-up).

Test: languages::dart_calls_resolve_through_imported_libraries fails
before this change (helper stays not-visible); an unimported file and a
library-private name stay unresolved.
trvon 2026-09-24 20:09:01 -06:00
Signed by: trevon
GPG key ID: 38686EA797D7A8A7

View file

@ -248,11 +248,23 @@ struct ScanState {
references: HashMap<String, HashSet<String>>,
/// Lexical-language file -> repository files it imports (resolved edges).
imported_paths: HashMap<String, Rc<HashSet<String>>>,
/// `name:` of the root pubspec.yaml, so `package:<name>/x.dart` maps to lib/x.dart.
dart_package: Option<String>,
}
impl ScanState {
fn new(root: PathBuf, root_id: String, options: &ScanOptions) -> Self {
let dart_package = fs::read_to_string(root.join("pubspec.yaml"))
.ok()
.and_then(|pubspec| {
pubspec.lines().find_map(|line| {
line.strip_prefix("name:")
.map(|name| name.trim().trim_matches(['\'', '"']).to_owned())
.filter(|name| !name.is_empty())
})
});
Self {
dart_package,
root,
root_id,
nodes: Vec::new(),
@ -1374,7 +1386,7 @@ impl ScanState {
if let Some(id) = self.resolve_qualified_symbol(relationship) {
return (id, None, None);
}
let imported = if language == "python" {
let imported = if language == "python" || language == "dart" {
self.imported_paths(&relationship.path)
} else {
Rc::default()
@ -1414,6 +1426,26 @@ impl ScanState {
if let Some(id) = self.resolve_imported_symbol(relationship) {
return (id, None, None);
}
// A Dart import exposes the library's public top-level names with no
// per-name binding: resolve through the libraries this file imports.
if language == "dart" && reason != "ambiguous" {
let exposed: Vec<_> = candidates
.iter()
.filter(|n| {
!n.name.starts_with('_')
&& n.path.as_ref().is_some_and(|p| imported.contains(p))
&& n.parent
.as_deref()
.and_then(|parent| self.node(parent))
.is_some_and(|parent| parent.kind == NodeKind::File)
})
.collect();
match exposed.as_slice() {
[only] => return (only.id.clone(), None, None),
[] => {}
_ => reason = "ambiguous",
}
}
} else {
// Lexical languages: the caller's own file first, then (Python)
// only files it imports. Swift files in one module see each other
@ -2173,10 +2205,18 @@ impl ScanState {
}
}
"dart" => {
if name.contains(':') {
if let Some(rest) = name.strip_prefix("package:") {
// Only this repository's own package maps into it: `lib/<rest>`.
let (package, rest) = rest.split_once('/')?;
if self.dart_package.as_deref() != Some(package) {
return None;
}
paths.push(normalize_import_path(&format!("lib/{rest}"))?);
} else if name.contains(':') {
return None;
} else {
paths.push(local()?);
}
paths.push(local()?);
}
"cpp" | "p4" => {
if language == "cpp" && !name.contains('/') && Path::new(name).extension().is_none()

View file

@ -648,3 +648,57 @@ fn python_calls_keep_their_function_and_resolve_only_what_is_visible() {
"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())]
);
}