[Bug]: TypeScript. abstract_class_declaration is missing from _CLASS_TYPES, so no abstract class is indexed`

Author: aala-congaCreated Sep 17, 2026Updated Sep 17, 2026
Labelsbug

code-review-graph version

2.3.8

Operating system

macOS

Python version

3.10

AI platform

claude-code

Output of code-review-graph status

bash
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

typescript
export abstract class Base {
  handle(value: string): void { void value; }
}

src/child.ts

typescript
import { Base } from './base';
export class Child extends Base {
  run(): void { this.handle('x'); }
}
bash
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:

python
"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:

python
>>> 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)
295

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:

  1. 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.
  2. The resolver fabricates a target on the subclass. Those four CALLS edges exist, but their targets are subclass.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.
  3. Inheritance chains break at an abstract intermediate. Child extends AbstractMiddle extends Base: eight classes correctly show INHERITS edges to AbstractMiddle, 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 INHERITS target is malformed; here the node does not exist. Adjacent symptom, different cause. Note that in my reproducer the INHERITS target is stored as the bare name Base, 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