You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Stock 2.3.8, on the reproducer below:
Nodes: 5
Edges: 6
Files: 2
Languages: typescript
Last updated: 2026-09-17T16:37:33
Built on branch: main
Built at commit: 484715d3d1a5
Five nodes: two `File`, `Child` (Class), `Child.run`, and `base.ts::handle`. There is no node for`Base`.
code-review-graph build --repo . --data-dir <dir outside the repo>
sqlite3 <data-dir>/graph.db "SELECT kind, qualified_name FROM nodes;"
Delete the word abstract and rebuild to see the difference.
Expected vs actual behavior
Expected: a Class node for Base, and its method as <file>::Base.handle.
Actual: no Class node for Base at all, and its method indexed as <file>::handle — with no
class component, unlike every concrete class.
File | src/base.ts
| src/base.ts::handle <-- no class qualifier
File | src/child.ts
Class | src/child.ts::Child
| src/child.ts::Child.run
I isolated the factor by varying one thing at a time, with identical members:
Declaration
Indexed as Class
class Base { … }
yes
class Base<T> { … }
yes
abstract class Base { … }
no
abstract class Base<T> { … }
no
abstract is the discriminator, not the type parameter. (I first suspected generics and was wrong:
in an Angular codebase generic classes are overwhelmingly abstract bases, which confounds the two.)
Additional context
Root cause and fix
_CLASS_TYPES (parser.py:900) does not list the node kind the grammar produces for an abstract
class:
The name already appears elsewhere in the file, in _TS_TYPE_DECLARATIONS (parser.py:994), so the
grammar's use of a separate node kind is known — it just never reached _CLASS_TYPES.
Adding "abstract_class_declaration" to the typescript and tsx lists is sufficient for the node
half. Measured on a 727-file Angular project:
before
after
abstract classes indexed
0 / 21 (0 %)
21 / 21 (100 %)
concrete classes indexed
634 / 641 (99 %)
unchanged
Class nodes total
1696
1717
the base method's node
<file>::handle
<file>::Base.handle
This is TypeScript-specific. Java on the same run: 4 abstract classes, 4 indexed (100 %).
Additional context
Three downstream effects I measured before finding the cause, all of which trace back to it:
callers_of on an inherited method returns 0. A base method called as this.method() from
two subclasses in two files — 4 real call sites — returns nothing.
Inheritance chains break at an abstract intermediate.Child extends AbstractMiddle extends Base: eight classes correctly show INHERITS edges toAbstractMiddle, but none leaves it,
because it has no node. Anything walking the chain stops there.
The one-line fix resolves 3 and the node naming, and it unblocks 1 and 2 without closing them — callers_of is still 0/4 afterwards, because this.method() is still bound to the enclosing class
rather than walked up the (now complete) INHERITS chain.
Related
nothing checks that the tree-sitter node names the parser looks for are ones the grammar actually produces #986 — nothing checks that the node names the parser looks for are ones the grammar produces.
This issue is the complementary half: the audit there verifies that listed names are real, which
would not have caught a relevant name being absent from a list. A second check — for each
language, which grammar node kinds the parser never looks at — would have.
code-review-graph version
2.3.8
Operating system
macOS
Python version
3.10
AI platform
claude-code
Output of
code-review-graph statusSteps to reproduce
src/base.tssrc/child.tsDelete the word
abstractand rebuild to see the difference.Expected vs actual behavior
Expected: a
Classnode forBase, and its method as<file>::Base.handle.Actual: no
Classnode forBaseat all, and its method indexed as<file>::handle— with noclass component, unlike every concrete class.
I isolated the factor by varying one thing at a time, with identical members:
Classclass Base { … }class Base<T> { … }abstract class Base { … }abstract class Base<T> { … }abstractis the discriminator, not the type parameter. (I first suspected generics and was wrong:in an Angular codebase generic classes are overwhelmingly abstract bases, which confounds the two.)
Additional context
Root cause and fix
_CLASS_TYPES(parser.py:900) does not list the node kind the grammar produces for an abstractclass:
tree-sitter produces
abstract_class_declaration, which is real in both grammars — checked with thetechnique from #986:
The name already appears elsewhere in the file, in
_TS_TYPE_DECLARATIONS(parser.py:994), so thegrammar's use of a separate node kind is known — it just never reached
_CLASS_TYPES.Adding
"abstract_class_declaration"to thetypescriptandtsxlists is sufficient for the nodehalf. Measured on a 727-file Angular project:
Classnodes total<file>::handle<file>::Base.handleThis is TypeScript-specific. Java on the same run: 4 abstract classes, 4 indexed (100 %).
Additional context
Three downstream effects I measured before finding the cause, all of which trace back to it:
callers_ofon an inherited method returns 0. A base method called asthis.method()fromtwo subclasses in two files — 4 real call sites — returns nothing.
CALLSedges exist, but theirtargets are
subclass.ts::SubClass.method, names that exist as no node. With the owning classabsent, the bare method name binds to the calling class instead. This makes the present issue an
upstream cause of call targets are invented when they cannot be worked out, and the invented ones look exactly like real ones #984.
Child extends AbstractMiddle extends Base: eight classes correctly showINHERITSedges toAbstractMiddle, but none leaves it,because it has no node. Anything walking the chain stops there.
The one-line fix resolves 3 and the node naming, and it unblocks 1 and 2 without closing them —
callers_ofis still 0/4 afterwards, becausethis.method()is still bound to the enclosing classrather than walked up the (now complete)
INHERITSchain.Related
This issue is the complementary half: the audit there verifies that listed names are real, which
would not have caught a relevant name being absent from a list. A second check — for each
language, which grammar node kinds the parser never looks at — would have.
the
INHERITStarget is malformed; here the node does not exist. Adjacent symptom, differentcause. Note that in my reproducer the
INHERITStarget is stored as the bare nameBase, which isa further case for Design: separate base-type extraction, canonical declaration identity, and resolution for INHERITS edges #943's table.
the same area.
I did not check Kotlin, PHP or C#, which also have abstract classes. Java is not affected.