From 6e75b5b5201b2fe7bb34e16fd1233a03c47a082d Mon Sep 17 00:00:00 2001 From: Pradeep Mouli Date: Sun, 19 Jul 2026 01:19:51 -0400 Subject: [PATCH 1/3] fix: extract INHERITS edges for TypeScript and Rust Both languages silently produced zero inheritance edges: - typescript/relations.scm had no @inherit.child/@inherit.parent capture at all -- only calls and imports -- unlike python/relations.scm and javascript/relations.scm, which both have working patterns for this. Every `class X extends Y`, `interface X extends Y`, and `class X implements Y` was invisible to the graph. - rust/relations.scm had a comment describing intent to capture `impl Trait for Type` as a relationship ("We capture impl blocks as a relationship"), but no actual query pattern was ever written. Every trait impl in every Rust codebase produced zero INHERITS edges. Confirmed against infigraph's own codebase: `impl GraphBackend for KuzuBackend` (crates/infigraph-core/src/graph/kuzu_backend.rs) and a real external repo's `InputProps extends React.ComponentProps<'input'>` both had no corresponding graph edge before this fix. Verified tree-sitter grammar node structure against the vendored tree-sitter-typescript/tree-sitter-rust node-types.json (class_heritage -> extends_clause/implements_clause, interface_declaration -> extends_type_clause, impl_item -> trait/type fields) rather than guessing. Tested end-to-end against a synthetic project covering all four patterns (class extends, interface extends, class implements, Rust trait impl) -- all four now produce correct INHERITS edges, with no language-pack load warnings (confirming the query syntax doesn't break existing extraction). Added regression tests in infigraph-languages/tests/registry_integration.rs covering both languages. This also directly affects the existing pattern-detection module (patterns/mod.rs) -- Strategy and Decorator both rely on INHERITS edges as their core structural signal, and were effectively blind for TypeScript and Rust codebases (i.e. most of infigraph's own supported languages) until this fix. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TAXDJyFdnA4BV1U2fxufdC --- .../languages/rust/relations.scm | 3 + .../languages/typescript/relations.scm | 20 +++++++ .../tests/registry_integration.rs | 57 +++++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/crates/infigraph-languages/languages/rust/relations.scm b/crates/infigraph-languages/languages/rust/relations.scm index 3170364..a2a3cbc 100644 --- a/crates/infigraph-languages/languages/rust/relations.scm +++ b/crates/infigraph-languages/languages/rust/relations.scm @@ -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) diff --git a/crates/infigraph-languages/languages/typescript/relations.scm b/crates/infigraph-languages/languages/typescript/relations.scm index 378cb0c..ce00a4d 100644 --- a/crates/infigraph-languages/languages/typescript/relations.scm +++ b/crates/infigraph-languages/languages/typescript/relations.scm @@ -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 + 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)) + +; Class implements: class Foo implements Bar +(class_declaration + name: (type_identifier) @inherit.child + (class_heritage + (implements_clause + (type_identifier) @inherit.parent))) diff --git a/crates/infigraph-languages/tests/registry_integration.rs b/crates/infigraph-languages/tests/registry_integration.rs index 3c54c4c..cc13ad7 100644 --- a/crates/infigraph-languages/tests/registry_integration.rs +++ b/crates/infigraph-languages/tests/registry_integration.rs @@ -142,6 +142,31 @@ 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] +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(); @@ -162,6 +187,38 @@ 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(); From e7df05ad80b2e17ff3d4937ccf4f76af49345061 Mon Sep 17 00:00:00 2001 From: Pradeep Mouli Date: Mon, 20 Jul 2026 12:10:40 -0400 Subject: [PATCH 2/3] chore: allow pre-existing clippy lints blocking commits Unrelated to this PR's INHERITS-extraction change. These lints (question_mark, useless_borrows_in_formatting) newly apply because 'stable' toolchain drifted since this code was written in May, not because the code changed. CI's dtolnay/rust-toolchain@stable resolves to the same rustc 1.97.x. Suppressing at the call site rather than rewriting, to keep this diff minimal. --- crates/infigraph-core/src/vuln/mod.rs | 1 + .../tests/registry_integration.rs | 27 ++++++++++++++----- crates/lsp-to-scip/src/main.rs | 1 + 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/crates/infigraph-core/src/vuln/mod.rs b/crates/infigraph-core/src/vuln/mod.rs index 8133dc9..a34e0ce 100644 --- a/crates/infigraph-core/src/vuln/mod.rs +++ b/crates/infigraph-core/src/vuln/mod.rs @@ -416,6 +416,7 @@ pub fn format_table(report: &VulnReport) -> String { } else { f.summary.clone() }; + #[allow(clippy::useless_borrows_in_formatting)] out.push_str(&format!( " {:<20} {:<12} {:<18} {:<10} {}\n", truncate_str(&f.dep_name, 20), diff --git a/crates/infigraph-languages/tests/registry_integration.rs b/crates/infigraph-languages/tests/registry_integration.rs index cc13ad7..f538a2e 100644 --- a/crates/infigraph-languages/tests/registry_integration.rs +++ b/crates/infigraph-languages/tests/registry_integration.rs @@ -159,9 +159,12 @@ fn test_extraction_rust_impl_trait_produces_inherits_edge() { .expect("extraction should succeed"); assert!( - extraction.relations.iter().any(|r| r.kind == RelationKind::Inherits - && r.source_id.contains("Person") - && r.target_id.contains("Greet")), + 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 ); @@ -214,9 +217,21 @@ fn test_extraction_typescript_inheritance_produces_edges() { }) }; - 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); + 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] diff --git a/crates/lsp-to-scip/src/main.rs b/crates/lsp-to-scip/src/main.rs index 066e543..ffc82e6 100644 --- a/crates/lsp-to-scip/src/main.rs +++ b/crates/lsp-to-scip/src/main.rs @@ -495,6 +495,7 @@ fn parse_symbol(v: &serde_json::Value) -> Option { // DocumentSymbol has range + selectionRange // SymbolInformation has location.range + #[allow(clippy::question_mark)] let (range, sel_range) = if let Some(r) = v.get("range") { let range = parse_range(r)?; let sel = v From bed91847522d624f8c877bbd63e81816444faccf Mon Sep 17 00:00:00 2001 From: Pradeep Mouli Date: Mon, 20 Jul 2026 13:49:09 -0400 Subject: [PATCH 3/3] fix: replace clippy lint suppressions with proper fixes Rewrites the two spots suppressed in the prior commit instead of disabling the lints: - lsp-to-scip/main.rs: replace if-let/else-if-let/else with ? operator - vuln/mod.rs: remove redundant & in format! arg --- crates/infigraph-core/src/vuln/mod.rs | 3 +-- crates/lsp-to-scip/src/main.rs | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/infigraph-core/src/vuln/mod.rs b/crates/infigraph-core/src/vuln/mod.rs index a34e0ce..79c3b86 100644 --- a/crates/infigraph-core/src/vuln/mod.rs +++ b/crates/infigraph-core/src/vuln/mod.rs @@ -416,13 +416,12 @@ pub fn format_table(report: &VulnReport) -> String { } else { f.summary.clone() }; - #[allow(clippy::useless_borrows_in_formatting)] out.push_str(&format!( " {:<20} {:<12} {:<18} {:<10} {}\n", 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/lsp-to-scip/src/main.rs b/crates/lsp-to-scip/src/main.rs index ffc82e6..3f15dae 100644 --- a/crates/lsp-to-scip/src/main.rs +++ b/crates/lsp-to-scip/src/main.rs @@ -495,7 +495,6 @@ fn parse_symbol(v: &serde_json::Value) -> Option { // DocumentSymbol has range + selectionRange // SymbolInformation has location.range - #[allow(clippy::question_mark)] let (range, sel_range) = if let Some(r) = v.get("range") { let range = parse_range(r)?; let sel = v @@ -503,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 {