Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/infigraph-core/src/vuln/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
));
}
Expand Down
3 changes: 3 additions & 0 deletions crates/infigraph-languages/languages/rust/relations.scm
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@

; Struct inheritance via trait bounds isn't direct, but impl Trait for Type is
; We capture impl blocks as a relationship
(impl_item
trait: (type_identifier) @inherit.parent
type: (type_identifier) @inherit.child)
20 changes: 20 additions & 0 deletions crates/infigraph-languages/languages/typescript/relations.scm
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,23 @@
; Import statements
(import_statement
source: (string) @import.module)

; Class inheritance: class Foo extends Bar
(class_declaration
name: (type_identifier) @inherit.child
(class_heritage
(extends_clause

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idea is to draw an arrow ("INHERITS edge") in the code graph whenever one TypeScript class/interface extends or implements another. It works for the simple case (class Dog extends Animal) but breaks for two common real-world patterns because the tree-sitter query is too narrow — it only accepts a bare name, not the fuller syntax tree-sitter actually produces for more complex code.

Bug 1 — extends Something.Else (dotted/member names) gets dropped.
Real code: class MyComponent extends React.Component — extremely common in any React class-component codebase. The query says "parent must be a plain identifier node." But when you write React.Component, tree-sitter doesn't parse that as one identifier — it parses it as a member_expression (object=React, property=Component). The query's identifier-only filter doesn't match a member_expression, so the whole pattern silently fails and no edge gets created. Same bug class the PR is fixing, just missed a case.

Bug 2 — implements Something (generics) gets dropped.
Real code: class Square implements Drawable. Query says the implemented name must be a plain type_identifier node. But Drawable parses as a generic_type node (which wraps a type_identifier inside it), not a bare type_identifier. Query doesn't match generic_type, so again — no edge, silently.

  • Line 22: value: (identifier) @inherit.parent — bug 1, needs to accept member-expressions too.
  • Line 34: (type_identifier) @inherit.parent)) — bug 2, needs to accept generic_type/nested_type_identifier too.

value: (identifier) @inherit.parent)))

; Interface inheritance: interface Foo extends Bar
(interface_declaration
name: (type_identifier) @inherit.child
(extends_type_clause
type: (type_identifier) @inherit.parent))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

L28 (typescript/relations.scm, interface extends): 🔴 bug: type: (type_identifier) misses generic_type and nested_type_identifier — grammar's extends_type_clause.type field explicitly allows both. interface Circle extends Shape<T> or interface Foo extends ns.Bar silently drops the edge. Fix: type: (_) @inherit.parent. Add test for extends Shape<T>.

rust/relations.scm (impl_item query): 🔴 bug: both trait: (type_identifier) and type: (type_identifier) are too narrow — grammar allows trait field to be generic_type/scoped_type_identifier/type_identifier, and type field is the broad _type supertype. impl std::fmt::Display for MyType or impl Iterator<Item=T> for MyType (both common Rust patterns) silently drop the edge. Fix: trait: (_) @inherit.parent and type: (_) @inherit.child. Add tests for scoped trait path and generic trait.


; Class implements: class Foo implements Bar
(class_declaration
name: (type_identifier) @inherit.child
(class_heritage
(implements_clause
(type_identifier) @inherit.parent)))
72 changes: 72 additions & 0 deletions crates/infigraph-languages/tests/registry_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,34 @@ fn test_extraction_smoke_rust() {
assert!(names.contains(&"main"), "should extract main: {names:?}");
}

/// Regression test: rust/relations.scm had a comment describing intent to
/// capture `impl Trait for Type` as an INHERITS relationship, but no actual
/// query pattern was ever written -- every trait impl in every Rust codebase
/// silently produced zero INHERITS edges (confirmed against infigraph's own
/// `impl GraphBackend for KuzuBackend`, which had no corresponding edge).
#[test]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test recommendation — don't just add 4 more one-off assertions. Root cause is "query hardcodes narrowest node type instead of matching the field's actual declared type set." I suggest to

  1. Fix all 4 queries by loosening to (_) (wildcard, still respects the field name, just not the concrete node type) — this is the actual fix, not a workaround.
  2. Add table-driven test cases per language covering: plain identifier, member/scoped path, generic — 3 cases × (TS extends, TS implements, TS interface-extends, Rust impl-trait) = up to 12 cases, but can dedupe into one parametrized test per language rather than 12 separate #[test] fns.
  3. Not required to enumerate every grammar alternative by hand — using (_) sidesteps needing to keep the query in sync with grammar node-type additions later.

fn test_extraction_rust_impl_trait_produces_inherits_edge() {
use infigraph_core::model::RelationKind;

let registry = bundled_registry().unwrap();
let pack = registry.for_extension(".rs").unwrap();

let source = b"trait Greet {\n fn hello(&self);\n}\nstruct Person;\nimpl Greet for Person {\n fn hello(&self) {}\n}\n";
let extraction = infigraph_core::extract::extract_file("test.rs", source, pack)
.expect("extraction should succeed");

assert!(
extraction
.relations
.iter()
.any(|r| r.kind == RelationKind::Inherits
&& r.source_id.contains("Person")
&& r.target_id.contains("Greet")),
"expected an INHERITS edge from Person to Greet, got: {:?}",
extraction.relations
);
}

#[test]
fn test_extraction_smoke_typescript() {
let registry = bundled_registry().unwrap();
Expand All @@ -162,6 +190,50 @@ fn test_extraction_smoke_typescript() {
);
}

/// Regression test: typescript/relations.scm had no inheritance capture at
/// all (only calls + imports), unlike python/relations.scm and
/// javascript/relations.scm which both have working @inherit.child/
/// @inherit.parent patterns -- every `class X extends Y`, `interface X
/// extends Y`, and `class X implements Y` in every TypeScript codebase
/// silently produced zero INHERITS edges (confirmed against a real repo's
/// `InputProps extends React.ComponentProps<'input'>`, which had no
/// corresponding edge).
#[test]
fn test_extraction_typescript_inheritance_produces_edges() {
use infigraph_core::model::RelationKind;

let registry = bundled_registry().unwrap();
let pack = registry.for_extension(".ts").unwrap();

let source = b"class Animal {}\nclass Dog extends Animal {}\n\ninterface Shape {}\ninterface Circle extends Shape {}\n\ninterface Drawable {}\nclass Square implements Drawable {}\n";
let extraction = infigraph_core::extract::extract_file("test.ts", 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("Circle", "Shape"),
"interface extends: {:?}",
extraction.relations
);
assert!(
has_edge("Square", "Drawable"),
"class implements: {:?}",
extraction.relations
);
}

#[test]
fn test_extraction_smoke_go() {
let registry = bundled_registry().unwrap();
Expand Down
5 changes: 2 additions & 3 deletions crates/lsp-to-scip/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -502,11 +502,10 @@ fn parse_symbol(v: &serde_json::Value) -> Option<LspSymbol> {
.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 {
Expand Down
Loading