fix/audit-findings #5
2 changed files with 97 additions and 3 deletions
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.
commit
a03c36969e
46
src/scan.rs
46
src/scan.rs
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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())]
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue