[Bug]: TypeScript. abstract_class_declaration is missing from _CLASS_TYPES, so no abstract class is indexed`
code-review-graph version
2.3.8
Operating system
macOS
Python version
3.10
AI platform
claude-code
Output of code-review-graph status
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`.Steps to reproduce
src/base.ts
export abstract class Base {
handle(value: string): void { void value; }
}src/child.ts
import { Base } from './base';
export class Child extends Base {
run(): void { this.handle('x'); }
}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.runI 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:
"typescript": [
"class_declaration", "class",
"interface_declaration", "type_alias_declaration", "enum_declaration",
],
"tsx": [ … same … ],tree-sitter produces abstract_class_declaration, which is real in both grammars — checked with the
technique from #986:
>>> from tree_sitter_language_pack import get_language
>>> get_language("typescript").id_for_node_kind("abstract_class_declaration", True)
282
>>> get_language("tsx").id_for_node_kind("abstract_class_declaration", True)
295The 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_ofon an inherited method returns 0. A base method called asthis.method()from two subclasses in two files — 4 real call sites — returns nothing.- The resolver fabricates a target on the subclass. Those four
CALLSedges exist, but their targets aresubclass.ts::SubClass.method, names that exist as no node. With the owning class absent, the bare method name binds to the calling class instead. This makes the present issue an upstream cause of #984. - Inheritance chains break at an abstract intermediate.
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_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
- #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.
- #935 / #943 — inheritance through a generic base in C#/Java. There the class node exists and
the
INHERITStarget is malformed; here the node does not exist. Adjacent symptom, different cause. Note that in my reproducer theINHERITStarget is stored as the bare nameBase, which is a further case for #943's table. - #738 — indexed TS interfaces and type aliases. Abstract classes look like the remaining hole in the same area.
I did not check Kotlin, PHP or C#, which also have abstract classes. Java is not affected.
Source: tirth8205/code-review-graph