-
Notifications
You must be signed in to change notification settings - Fork 7
fix: extract INHERITS edges for TypeScript and Rust #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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)) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. L28 (typescript/relations.scm, interface extends): 🔴 bug: rust/relations.scm (impl_item query): 🔴 bug: both |
||
|
|
||
| ; Class implements: class Foo implements Bar | ||
| (class_declaration | ||
| name: (type_identifier) @inherit.child | ||
| (class_heritage | ||
| (implements_clause | ||
| (type_identifier) @inherit.parent))) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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] | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||
| 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 +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(); | ||
|
|
||
There was a problem hiding this comment.
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.