feat: establish first-class lexical language parity #2

Merged
trevon merged 14 commits from feat/language-parity-foundation into bench/yams-architecture-reproducibility 2026-08-30 22:09:40 +00:00
Showing only changes of commit 9d5596f0dd - Show all commits

fix(scan): isolate language resolution and method ownership

trvon 2026-08-30 14:36:31 -06:00
Signed by: trevon
GPG key ID: 38686EA797D7A8A7

View file

@ -94,7 +94,7 @@ struct ScanState {
nodes: Vec<Node>,
edges: Vec<Edge>,
edge_keys: HashSet<(String, String, EdgeKind)>,
symbols_by_name: HashMap<String, Vec<String>>,
symbols_by_name: HashMap<(String, String), Vec<String>>,
files_by_key: HashMap<String, Vec<String>>,
pending: Vec<PendingRelationship>,
diagnostics: Vec<Diagnostic>,
@ -315,8 +315,14 @@ impl ScanState {
&& !is_keyword(name.as_str())
{
definition_name = Some(name.as_str().to_owned());
let parent_id = scopes
.iter()
.rev()
.find(|scope| self.node_kind(&scope.id) == Some(NodeKind::Class))
.map(|scope| scope.id.as_str())
.unwrap_or(file_id);
let id = self.add_symbol(
file_id,
parent_id,
path,
language,
name.as_str(),
@ -378,7 +384,7 @@ impl ScanState {
fn add_symbol(
&mut self,
file_id: &str,
parent_id: &str,
path: &str,
language: &str,
name: &str,
@ -397,18 +403,18 @@ impl ScanState {
kind,
name: name.into(),
path: Some(path.into()),
parent: Some(file_id.into()),
parent: Some(parent_id.into()),
language: Some(language.into()),
line: Some(line),
lines_of_code: 0,
attributes: BTreeMap::new(),
});
self.symbols_by_name
.entry(name.into())
.entry((language.into(), name.into()))
.or_default()
.push(id.clone());
self.add_edge(
file_id.into(),
parent_id.into(),
id.clone(),
EdgeKind::Defines,
"brick-lexical",
@ -452,10 +458,13 @@ impl ScanState {
None
} else {
self.symbols_by_name
.get(name)
.get(&(language.into(), name.into()))
.or_else(|| {
(!name.contains("::"))
.then(|| self.symbols_by_name.get(leaf_name))
.then(|| {
self.symbols_by_name
.get(&(language.into(), leaf_name.into()))
})
.flatten()
})
.and_then(|ids| (ids.len() == 1).then(|| ids[0].clone()))
@ -953,6 +962,82 @@ mod tests {
);
}
#[test]
fn does_not_resolve_calls_across_language_boundaries() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("library.rs"), "fn shared() {}\n").unwrap();
fs::write(
dir.path().join("application.js"),
"function local() {}\nfunction caller() { local(); shared(); }\n",
)
.unwrap();
let building = scan(dir.path()).unwrap();
let caller = building
.nodes
.iter()
.find(|node| node.language.as_deref() == Some("javascript") && node.name == "caller")
.unwrap();
let javascript_local = building
.nodes
.iter()
.find(|node| node.language.as_deref() == Some("javascript") && node.name == "local")
.unwrap();
let rust_shared = building
.nodes
.iter()
.find(|node| node.language.as_deref() == Some("rust") && node.name == "shared")
.unwrap();
let caller_edges: Vec<_> = building
.edges
.iter()
.filter(|edge| edge.kind == EdgeKind::Calls && edge.source == caller.id)
.collect();
assert!(
caller_edges
.iter()
.any(|edge| edge.target == javascript_local.id)
);
assert!(
!caller_edges
.iter()
.any(|edge| edge.target == rust_shared.id)
);
assert!(caller_edges.iter().any(|edge| {
building.nodes.iter().any(|node| {
node.id == edge.target && node.kind == NodeKind::External && node.name == "shared"
})
}));
}
#[test]
fn parents_recognized_methods_to_their_enclosing_type() {
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join("worker.dart"),
"class Worker {\n void execute() {}\n}\n",
)
.unwrap();
let building = scan(dir.path()).unwrap();
let worker = building
.nodes
.iter()
.find(|node| node.kind == NodeKind::Class && node.name == "Worker")
.unwrap();
let execute = building
.nodes
.iter()
.find(|node| node.kind == NodeKind::Function && node.name == "execute")
.unwrap();
assert_eq!(execute.parent.as_deref(), Some(worker.id.as_str()));
assert!(building.edges.iter().any(|edge| {
edge.kind == EdgeKind::Defines && edge.source == worker.id && edge.target == execute.id
}));
}
#[test]
fn scans_dart_structure_without_promoting_control_flow_or_strings() {
let dir = tempfile::tempdir().unwrap();