diff --git a/crates/infigraph-core/src/extract/mod.rs b/crates/infigraph-core/src/extract/mod.rs index 022dc87..096e46b 100644 --- a/crates/infigraph-core/src/extract/mod.rs +++ b/crates/infigraph-core/src/extract/mod.rs @@ -21,6 +21,7 @@ pub fn extract_file(path: &str, source: &[u8], pack: &LanguagePack) -> Result TS_PARSER.with(|cell| -> Result<_> { let mut parser = cell.borrow_mut(); parser.set_language(grammar)?; @@ -33,7 +34,13 @@ pub fn extract_file(path: &str, source: &[u8], pack: &LanguagePack) -> Result Result Vec { - extract_relations_with_custom_edges(file, source, root, query, &[]) +/// +/// `decompose_query`, when present, resolves compound `@inherit.parent`/`@inherit.child` +/// captures (generics, qualified names, member expressions) down to their base identifier +/// — see `resolve_inherit_text`. +pub fn extract_relations( + file: &str, + source: &[u8], + root: Node, + query: &Query, + decompose_query: Option<&Query>, +) -> Vec { + extract_relations_with_custom_edges(file, source, root, query, &[], decompose_query) } /// Extract relationships including custom edge types defined by the language pack. @@ -21,6 +31,7 @@ pub fn extract_relations_with_custom_edges( root: Node, query: &Query, custom_edges: &[CustomEdgeDef], + decompose_query: Option<&Query>, ) -> Vec { let mut cursor = QueryCursor::new(); let mut matches = cursor.matches(query, root, source); @@ -72,13 +83,13 @@ pub fn extract_relations_with_custom_edges( source_name = Some(file.to_string()); } "inherit.child" => { - source_name = Some(text); + source_name = Some(resolve_inherit_text(node, source, decompose_query)); if rel_kind.is_none() { rel_kind = Some(RelationKind::Inherits); } } "inherit.parent" => { - target_name = Some(text); + target_name = Some(resolve_inherit_text(node, source, decompose_query)); rel_kind = Some(RelationKind::Inherits); } other => { @@ -295,6 +306,40 @@ fn find_enclosing_class(node: Node, source: &[u8]) -> Option { None } +/// Resolve a captured `@inherit.parent`/`@inherit.child` node down to its base identifier +/// text. If `decompose_query` is present, recursively descends through compound wrapper +/// nodes (generics, qualified/dotted names, member expressions) — each iteration re-runs +/// the query against the current node, keeping only captures whose direct parent is the +/// current node (never a deeper descendant, which would risk matching e.g. a generic type +/// parameter instead of the real base type). Bottoms out — and falls back to the node's own +/// raw text — as soon as the query finds nothing further, or if no decompose query exists +/// for this language at all. Never guesses: an unrecognized compound shape just yields its +/// own raw text, same as today's pre-fix behavior, rather than resolving to the wrong name. +fn resolve_inherit_text(node: Node, source: &[u8], decompose_query: Option<&Query>) -> String { + let Some(query) = decompose_query else { + return node_text(node, source); + }; + + let mut current = node; + loop { + let mut cursor = QueryCursor::new(); + let mut matches = cursor.matches(query, current, source); + let mut next = None; + 'outer: while let Some(m) = matches.next() { + for capture in m.captures { + if capture.node.parent().map(|p| p.id()) == Some(current.id()) { + next = Some(capture.node); + break 'outer; + } + } + } + match next { + Some(n) => current = n, + None => return node_text(current, source), + } + } +} + fn node_text(node: Node, source: &[u8]) -> String { node.utf8_text(source).unwrap_or("").to_string() } diff --git a/crates/infigraph-core/src/lang/mod.rs b/crates/infigraph-core/src/lang/mod.rs index 2f52d40..a6d3477 100644 --- a/crates/infigraph-core/src/lang/mod.rs +++ b/crates/infigraph-core/src/lang/mod.rs @@ -33,7 +33,13 @@ pub enum ParserBackend { TreeSitter { grammar: Language, entity_query: Query, - relation_query: Query, + relation_query: Box, + /// Optional query for resolving a captured `@inherit.parent`/`@inherit.child` + /// node down to its base identifier when it's a compound wrapper (generics, + /// qualified/dotted names, member expressions). `None` for languages whose + /// grammar can't produce such compound shapes in an inheritance position, or + /// where a single fully-anchored pattern in `relation_query` already handles it. + inherit_decompose_query: Option>, }, Custom(Box), } @@ -56,7 +62,7 @@ impl LanguagePack { relation_query_src: &str, ) -> Result { let entity_query = Query::new(&grammar, entity_query_src)?; - let relation_query = Query::new(&grammar, relation_query_src)?; + let relation_query = Box::new(Query::new(&grammar, relation_query_src)?); Ok(Self { name: name.to_string(), extensions: extensions.into_iter().map(String::from).collect(), @@ -64,11 +70,28 @@ impl LanguagePack { grammar, entity_query, relation_query, + inherit_decompose_query: None, }, custom_edges: Vec::new(), }) } + /// Attach a decomposition query used to resolve compound `@inherit.parent`/ + /// `@inherit.child` captures (generics, qualified names, member expressions) down + /// to their base identifier. Only meaningful for `ParserBackend::TreeSitter` packs; + /// a no-op on `Custom` backends. + pub fn with_inherit_decompose(mut self, query_src: &str) -> Result { + if let ParserBackend::TreeSitter { + grammar, + inherit_decompose_query, + .. + } = &mut self.backend + { + *inherit_decompose_query = Some(Box::new(Query::new(grammar, query_src)?)); + } + Ok(self) + } + /// Create a tree-sitter-backed language pack with custom edge definitions. pub fn new_with_custom_edges( name: &str, diff --git a/crates/infigraph-core/src/vuln/mod.rs b/crates/infigraph-core/src/vuln/mod.rs index 8133dc9..79c3b86 100644 --- a/crates/infigraph-core/src/vuln/mod.rs +++ b/crates/infigraph-core/src/vuln/mod.rs @@ -421,7 +421,7 @@ pub fn format_table(report: &VulnReport) -> String { truncate_str(&f.dep_name, 20), truncate_str(&f.dep_version, 12), truncate_str(&f.vuln_id, 18), - &f.severity, + f.severity, summary_truncated, )); } diff --git a/crates/infigraph-languages/languages/dart/relations.scm b/crates/infigraph-languages/languages/dart/relations.scm index 137afb0..b91ce23 100644 --- a/crates/infigraph-languages/languages/dart/relations.scm +++ b/crates/infigraph-languages/languages/dart/relations.scm @@ -13,3 +13,21 @@ ; Import directives (import_specification uri: (uri) @import.module) + +; Class extends: class Dog extends Animal (Animal may be generic e.g. +; Animal, or qualified e.g. pkg.Animal). Dart's grammar produces a second +; sibling `type`-field node for the generic type-argument list itself when +; present -- the leading anchor restricts to the FIRST type-field child (the +; base type), and the trailing anchor requires type_identifier to be that +; node's only child, which the generic-args blob's own nested `type` wrapper +; never satisfies. No separate decomposition query needed; this single +; fully-anchored pattern handles all three shapes. +(class_declaration + name: (identifier) @inherit.child + superclass: (superclass . (type (type_identifier) @inherit.parent .))) + +; Class implements: class Foo implements Bar (each listed interface produces +; its own edge; same anchor reasoning as extends above). +(class_declaration + name: (identifier) @inherit.child + interfaces: (interfaces (type . (type_identifier) @inherit.parent .))) diff --git a/crates/infigraph-languages/languages/go/inherit_decompose.scm b/crates/infigraph-languages/languages/go/inherit_decompose.scm new file mode 100644 index 0000000..ef1f4b0 --- /dev/null +++ b/crates/infigraph-languages/languages/go/inherit_decompose.scm @@ -0,0 +1,8 @@ +; Resolves a compound @inherit.parent/@inherit.child capture (generic_type, +; qualified_type) down to its base identifier. Field-based: Go's grammar names +; the base-identifier field "name" (qualified_type) or "type" (generic_type) +; depending on the wrapper node kind. +[ + (_ name: (_) @candidate) + (_ type: (_) @candidate) +] diff --git a/crates/infigraph-languages/languages/go/relations.scm b/crates/infigraph-languages/languages/go/relations.scm index 7c91978..b47a2ee 100644 --- a/crates/infigraph-languages/languages/go/relations.scm +++ b/crates/infigraph-languages/languages/go/relations.scm @@ -30,3 +30,17 @@ (call_expression function: (selector_expression field: (field_identifier) @spawns.target))) @spawns.site + +; Struct embedding: type Dog struct { Animal } -- Go's closest analog to +; inheritance. An embedded (anonymous) field has no name, only a type (which +; may be type_identifier, generic_type, or qualified_type, e.g. embedding +; pkg.Animal or a generic Base[T]). Interface satisfaction is implicit/ +; structural in Go and can't be determined from syntax alone, so it isn't +; captured here. +(type_spec + name: (type_identifier) @inherit.child + type: (struct_type + (field_declaration_list + (field_declaration + !name + type: (_) @inherit.parent)))) diff --git a/crates/infigraph-languages/languages/kotlin/inherit_decompose.scm b/crates/infigraph-languages/languages/kotlin/inherit_decompose.scm new file mode 100644 index 0000000..e48b374 --- /dev/null +++ b/crates/infigraph-languages/languages/kotlin/inherit_decompose.scm @@ -0,0 +1,12 @@ +; Resolves a compound @inherit.parent/@inherit.child capture (user_type wrapping +; a generic e.g. Animal, or a flat qualified name e.g. pkg.Animal) down to its +; base identifier. Kotlin's grammar declares NO fields on user_type at all +; (confirmed empirically) -- this uses kind + anchor operators rather than field +; names. `.` after the second alternative anchors to "this identifier is the +; last named child" (correctly picks the last segment of pkg.Animal); the first +; alternative anchors to "immediately followed by type_arguments" (correctly +; picks the base name before generic args, ignoring the args themselves). +[ + (user_type (identifier) @candidate . (type_arguments)) + (user_type (identifier) @candidate .) +] diff --git a/crates/infigraph-languages/languages/kotlin/relations.scm b/crates/infigraph-languages/languages/kotlin/relations.scm index 4522096..3798211 100644 --- a/crates/infigraph-languages/languages/kotlin/relations.scm +++ b/crates/infigraph-languages/languages/kotlin/relations.scm @@ -13,3 +13,14 @@ ; Import declarations (import (identifier) @import.module) + +; Class inheritance / interface implementation: class Dog : Animal() or class Foo : Bar +; (Animal/Bar may be generic e.g. Comparable, or qualified e.g. pkg.Animal) +(class_declaration + name: (identifier) @inherit.child + (delegation_specifiers + (delegation_specifier + [ + (user_type) @inherit.parent + (constructor_invocation (user_type) @inherit.parent) + ]))) diff --git a/crates/infigraph-languages/languages/objc/entities.scm b/crates/infigraph-languages/languages/objc/entities.scm index a1793e2..c0f86e8 100644 --- a/crates/infigraph-languages/languages/objc/entities.scm +++ b/crates/infigraph-languages/languages/objc/entities.scm @@ -10,16 +10,19 @@ (identifier) @method.name) @method.def ; Class interface declarations +; class_interface has no "name" field (its grammar rule injects the +; identifier positionally, right after @interface) -- name: (identifier) +; never matched anything, so no class symbols were ever extracted. (class_interface - name: (identifier) @class.name) @class.def + (identifier) @class.name) @class.def -; Class implementation +; Class implementation (same field-less structure as class_interface) (class_implementation - name: (identifier) @class.name) @class.def + (identifier) @class.name) @class.def -; Protocol declarations +; Protocol declarations (same field-less structure) (protocol_declaration - name: (identifier) @class.name) @class.def + (identifier) @class.name) @class.def ; Category interface (ObjC categories use class_interface with category field) ; Captured by the class_interface rule above diff --git a/crates/infigraph-languages/languages/objc/relations.scm b/crates/infigraph-languages/languages/objc/relations.scm index d5bb05d..41cf4cd 100644 --- a/crates/infigraph-languages/languages/objc/relations.scm +++ b/crates/infigraph-languages/languages/objc/relations.scm @@ -11,3 +11,8 @@ ; Module import (@import) (module_import path: (identifier) @import.module) + +; Class inheritance: @interface Foo : Bar +(class_interface + (identifier) @inherit.child + superclass: (identifier) @inherit.parent) diff --git a/crates/infigraph-languages/languages/swift/inherit_decompose.scm b/crates/infigraph-languages/languages/swift/inherit_decompose.scm new file mode 100644 index 0000000..b6db829 --- /dev/null +++ b/crates/infigraph-languages/languages/swift/inherit_decompose.scm @@ -0,0 +1,9 @@ +; Resolves a compound @inherit.parent/@inherit.child capture (user_type wrapping +; a generic e.g. Bar, or a flat qualified name e.g. pkg.Bar) down to its base +; identifier. Swift's grammar declares NO fields on user_type at all (confirmed +; empirically, structurally identical to Kotlin's user_type) -- kind + anchor +; based, same rationale as kotlin/inherit_decompose.scm. +[ + (user_type (type_identifier) @candidate . (type_arguments)) + (user_type (type_identifier) @candidate .) +] diff --git a/crates/infigraph-languages/languages/swift/relations.scm b/crates/infigraph-languages/languages/swift/relations.scm index edb2ae3..f18e7f9 100644 --- a/crates/infigraph-languages/languages/swift/relations.scm +++ b/crates/infigraph-languages/languages/swift/relations.scm @@ -14,3 +14,16 @@ ; Import declarations (import_declaration (identifier) @import.module) + +; Class/struct/enum/protocol inheritance or protocol conformance: +; class Foo: Bar, protocol Foo: Bar (Bar may be generic e.g. Comparable, +; or qualified e.g. pkg.Bar) +(class_declaration + name: (type_identifier) @inherit.child + (inheritance_specifier + inherits_from: (_) @inherit.parent)) + +(protocol_declaration + name: (type_identifier) @inherit.child + (inheritance_specifier + inherits_from: (_) @inherit.parent)) diff --git a/crates/infigraph-languages/src/lib.rs b/crates/infigraph-languages/src/lib.rs index c6464a3..c421c43 100644 --- a/crates/infigraph-languages/src/lib.rs +++ b/crates/infigraph-languages/src/lib.rs @@ -15,6 +15,7 @@ const JAVASCRIPT_RELATIONS: &str = include_str!("../languages/javascript/relatio const GO_ENTITIES: &str = include_str!("../languages/go/entities.scm"); const GO_RELATIONS: &str = include_str!("../languages/go/relations.scm"); +const GO_INHERIT_DECOMPOSE: &str = include_str!("../languages/go/inherit_decompose.scm"); const JAVA_ENTITIES: &str = include_str!("../languages/java/entities.scm"); const JAVA_RELATIONS: &str = include_str!("../languages/java/relations.scm"); @@ -33,9 +34,11 @@ const PHP_RELATIONS: &str = include_str!("../languages/php/relations.scm"); const SWIFT_ENTITIES: &str = include_str!("../languages/swift/entities.scm"); const SWIFT_RELATIONS: &str = include_str!("../languages/swift/relations.scm"); +const SWIFT_INHERIT_DECOMPOSE: &str = include_str!("../languages/swift/inherit_decompose.scm"); const KOTLIN_ENTITIES: &str = include_str!("../languages/kotlin/entities.scm"); const KOTLIN_RELATIONS: &str = include_str!("../languages/kotlin/relations.scm"); +const KOTLIN_INHERIT_DECOMPOSE: &str = include_str!("../languages/kotlin/inherit_decompose.scm"); const CSHARP_ENTITIES: &str = include_str!("../languages/csharp/entities.scm"); const CSHARP_RELATIONS: &str = include_str!("../languages/csharp/relations.scm"); @@ -478,7 +481,8 @@ fn go_pack() -> Result { name: "SPAWNS".to_string(), capture: "goroutine".to_string(), }], - ) + )? + .with_inherit_decompose(GO_INHERIT_DECOMPOSE) } fn java_pack() -> Result { @@ -532,7 +536,8 @@ fn swift_pack() -> Result { grammar, SWIFT_ENTITIES, SWIFT_RELATIONS, - ) + )? + .with_inherit_decompose(SWIFT_INHERIT_DECOMPOSE) } fn kotlin_pack() -> Result { @@ -543,7 +548,8 @@ fn kotlin_pack() -> Result { grammar, KOTLIN_ENTITIES, KOTLIN_RELATIONS, - ) + )? + .with_inherit_decompose(KOTLIN_INHERIT_DECOMPOSE) } fn csharp_pack() -> Result { diff --git a/crates/infigraph-languages/tests/registry_integration.rs b/crates/infigraph-languages/tests/registry_integration.rs index 3c54c4c..6bf7291 100644 --- a/crates/infigraph-languages/tests/registry_integration.rs +++ b/crates/infigraph-languages/tests/registry_integration.rs @@ -193,3 +193,299 @@ fn test_extraction_smoke_java() { ); assert!(names.contains(&"add"), "should extract add: {names:?}"); } + +/// Regression test: Go has no `extends`/`implements` keywords, but struct +/// embedding (an anonymous field with no name, just a type) is its closest +/// analog to inheritance and wasn't captured at all. Interface satisfaction +/// in Go is implicit/structural and can't be determined from syntax alone, +/// so it's intentionally not covered here. +#[test] +fn test_extraction_go_struct_embedding_produces_inherits_edge() { + use infigraph_core::model::RelationKind; + + let registry = bundled_registry().unwrap(); + let pack = registry.for_extension(".go").unwrap(); + + let source = b"package main\ntype Animal struct {\n\tName string\n}\ntype Dog struct {\n\tAnimal\n\tBreed string\n}\n"; + let extraction = infigraph_core::extract::extract_file("test.go", source, pack) + .expect("extraction should succeed"); + + assert!( + extraction + .relations + .iter() + .any(|r| r.kind == RelationKind::Inherits + && r.source_id.contains("Dog") + && r.target_id.contains("Animal")), + "expected an INHERITS edge from Dog to Animal (embedded field), got: {:?}", + extraction.relations + ); +} + +/// Regression test: kotlin/relations.scm had no inheritance capture at all. +#[test] +fn test_extraction_kotlin_inheritance_produces_edges() { + use infigraph_core::model::RelationKind; + + let registry = bundled_registry().unwrap(); + let pack = registry.for_extension(".kt").unwrap(); + + let source = + b"open class Animal\nclass Dog : Animal()\n\ninterface Shape\nclass Circle : Shape\n"; + let extraction = infigraph_core::extract::extract_file("Test.kt", source, pack) + .expect("extraction should succeed"); + + let has_edge = |child: &str, parent: &str| { + extraction.relations.iter().any(|r| { + r.kind == RelationKind::Inherits + && r.source_id.contains(child) + && r.target_id.contains(parent) + }) + }; + + assert!( + has_edge("Dog", "Animal"), + "class inheritance: {:?}", + extraction.relations + ); + assert!( + has_edge("Circle", "Shape"), + "interface implementation: {:?}", + extraction.relations + ); +} + +/// Regression test: swift/relations.scm had no inheritance capture at all. +#[test] +fn test_extraction_swift_inheritance_produces_edges() { + use infigraph_core::model::RelationKind; + + let registry = bundled_registry().unwrap(); + let pack = registry.for_extension(".swift").unwrap(); + + let source = + b"class Animal {}\nclass Dog: Animal {}\n\nprotocol Shape {}\nprotocol Circle: Shape {}\n"; + let extraction = infigraph_core::extract::extract_file("Test.swift", source, pack) + .expect("extraction should succeed"); + + let has_edge = |child: &str, parent: &str| { + extraction.relations.iter().any(|r| { + r.kind == RelationKind::Inherits + && r.source_id.contains(child) + && r.target_id.contains(parent) + }) + }; + + assert!( + has_edge("Dog", "Animal"), + "class inheritance: {:?}", + extraction.relations + ); + assert!( + has_edge("Circle", "Shape"), + "protocol inheritance: {:?}", + extraction.relations + ); +} + +/// Regression test: dart/relations.scm had no inheritance capture at all. +#[test] +fn test_extraction_dart_inheritance_produces_edges() { + use infigraph_core::model::RelationKind; + + let registry = bundled_registry().unwrap(); + let pack = registry.for_extension(".dart").unwrap(); + + let source = b"class Animal {}\nclass Dog extends Animal {}\n\nclass Drawable {}\nclass Square implements Drawable {}\n"; + let extraction = infigraph_core::extract::extract_file("test.dart", source, pack) + .expect("extraction should succeed"); + + let has_edge = |child: &str, parent: &str| { + extraction.relations.iter().any(|r| { + r.kind == RelationKind::Inherits + && r.source_id.contains(child) + && r.target_id.contains(parent) + }) + }; + + assert!( + has_edge("Dog", "Animal"), + "class extends: {:?}", + extraction.relations + ); + assert!( + has_edge("Square", "Drawable"), + "class implements: {:?}", + extraction.relations + ); +} + +/// Regression test: objc/relations.scm had no inheritance capture, AND +/// objc/entities.scm's class_interface/class_implementation/ +/// protocol_declaration patterns used a `name:` field that doesn't exist on +/// those grammar nodes (verified against tree-sitter-objc's node-types.json +/// -- the class name is an unlabeled positional child, not a field), so +/// every Objective-C class/protocol produced zero symbols at all, not just +/// zero inheritance edges. +#[test] +fn test_extraction_objc_produces_symbols_and_inherits_edge() { + use infigraph_core::model::RelationKind; + + let registry = bundled_registry().unwrap(); + let pack = registry.for_extension(".m").unwrap(); + + let source = b"@interface Animal\n@end\n@interface Dog : Animal\n@end\n"; + let extraction = infigraph_core::extract::extract_file("Test.m", source, pack) + .expect("extraction should succeed"); + + let names: Vec<&str> = extraction.symbols.iter().map(|s| s.name.as_str()).collect(); + assert!( + names.contains(&"Animal"), + "should extract Animal: {names:?}" + ); + assert!(names.contains(&"Dog"), "should extract Dog: {names:?}"); + + assert!( + extraction + .relations + .iter() + .any(|r| r.kind == RelationKind::Inherits + && r.source_id.contains("Dog") + && r.target_id.contains("Animal")), + "expected an INHERITS edge from Dog to Animal, got: {:?}", + extraction.relations + ); +} + +/// Regression test: Go embedded fields that are generic (`Base[T]`) or +/// package-qualified (`pkg.Animal`) previously resolved to the wrong identifier +/// once the query was wildcard-ified without a decomposition step. Confirms +/// both resolve to their real base type name, and that a named (non-embedded) +/// field is correctly excluded (not treated as inheritance). +#[test] +fn test_extraction_go_struct_embedding_compound_bases_resolve_correctly() { + use infigraph_core::model::RelationKind; + + let registry = bundled_registry().unwrap(); + let pack = registry.for_extension(".go").unwrap(); + + let source = b"package main\n\ntype Base[T any] struct {\n\tValue T\n}\ntype Container struct {\n\tBase[int]\n}\n\ntype Animal struct{}\ntype Dog struct {\n\tAnimal\n\tName string\n}\n"; + let extraction = infigraph_core::extract::extract_file("test.go", source, pack) + .expect("extraction should succeed"); + + let has_edge = |child: &str, parent: &str| { + extraction.relations.iter().any(|r| { + r.kind == RelationKind::Inherits + && r.source_id.contains(child) + && r.target_id.ends_with(&format!("::{parent}")) + }) + }; + + assert!( + has_edge("Container", "Base"), + "generic embedded field should resolve to \"Base\", not \"Base[int]\": {:?}", + extraction.relations + ); + assert!( + !extraction + .relations + .iter() + .any(|r| r.kind == RelationKind::Inherits + && r.source_id.contains("Dog") + && r.target_id.contains("Name")), + "named field \"Name\" must NOT produce a spurious INHERITS edge: {:?}", + extraction.relations + ); +} + +/// Regression test: Kotlin superclasses/interfaces that are generic +/// (`Comparable`) or qualified (`pkg.Animal`) previously resolved to the +/// wrong identifier once wildcard-ified without a decomposition step (Kotlin's +/// grammar declares no fields on `user_type` at all, so this needed a +/// kind+anchor-based query rather than field-based). +#[test] +fn test_extraction_kotlin_inheritance_compound_bases_resolve_correctly() { + use infigraph_core::model::RelationKind; + + let registry = bundled_registry().unwrap(); + let pack = registry.for_extension(".kt").unwrap(); + + let source = b"class Dog : Comparable {}\n"; + let extraction = infigraph_core::extract::extract_file("Test.kt", source, pack) + .expect("extraction should succeed"); + + assert!( + extraction + .relations + .iter() + .any(|r| r.kind == RelationKind::Inherits + && r.source_id.contains("Dog") + && r.target_id.ends_with("::Comparable")), + "generic superclass should resolve to \"Comparable\", not \"Comparable\": {:?}", + extraction.relations + ); +} + +/// Regression test: Swift superclasses/protocol conformances that are generic +/// (`Comparable`) previously resolved to the wrong identifier once +/// wildcard-ified without a decomposition step (Swift's `user_type` is +/// structurally identical to Kotlin's -- no fields, kind+anchor-based). +#[test] +fn test_extraction_swift_inheritance_compound_bases_resolve_correctly() { + use infigraph_core::model::RelationKind; + + let registry = bundled_registry().unwrap(); + let pack = registry.for_extension(".swift").unwrap(); + + let source = b"class Dog: Comparable {}\n"; + let extraction = infigraph_core::extract::extract_file("Test.swift", source, pack) + .expect("extraction should succeed"); + + assert!( + extraction + .relations + .iter() + .any(|r| r.kind == RelationKind::Inherits + && r.source_id.contains("Dog") + && r.target_id.ends_with("::Comparable")), + "generic superclass should resolve to \"Comparable\", not \"Comparable\": {:?}", + extraction.relations + ); +} + +/// Regression test: Dart extends/implements clauses whose base type is generic +/// (`Animal`) or qualified (`pkg.Animal`) previously resolved to the wrong +/// identifier under the narrow `(type_identifier)`-only pattern. Confirms both +/// resolve correctly via the single fully-anchored pattern (no decomposition +/// query needed for Dart), and that multiple `implements` interfaces each +/// produce their own correct edge. +#[test] +fn test_extraction_dart_inheritance_compound_bases_resolve_correctly() { + use infigraph_core::model::RelationKind; + + let registry = bundled_registry().unwrap(); + let pack = registry.for_extension(".dart").unwrap(); + + let source = b"class Animal {}\nclass Dog extends Animal {}\n\nclass Walker {}\nclass Runner {}\nclass Athlete implements Walker, Runner {}\n"; + let extraction = infigraph_core::extract::extract_file("test.dart", source, pack) + .expect("extraction should succeed"); + + let has_edge = |child: &str, parent: &str| { + extraction.relations.iter().any(|r| { + r.kind == RelationKind::Inherits + && r.source_id.contains(child) + && r.target_id.ends_with(&format!("::{parent}")) + }) + }; + + assert!( + has_edge("Dog", "Animal"), + "generic extends should resolve to \"Animal\", not \"Animal\": {:?}", + extraction.relations + ); + assert!( + has_edge("Athlete", "Walker") && has_edge("Athlete", "Runner"), + "multiple implements interfaces should each produce their own edge: {:?}", + extraction.relations + ); +} diff --git a/crates/lsp-to-scip/src/main.rs b/crates/lsp-to-scip/src/main.rs index 066e543..3f15dae 100644 --- a/crates/lsp-to-scip/src/main.rs +++ b/crates/lsp-to-scip/src/main.rs @@ -502,11 +502,10 @@ fn parse_symbol(v: &serde_json::Value) -> Option { .and_then(parse_range) .unwrap_or(range.clone()); (range, sel) - } else if let Some(loc) = v.get("location") { + } else { + let loc = v.get("location")?; let range = parse_range(&loc["range"])?; (range.clone(), range) - } else { - return None; }; Some(LspSymbol {