Skip to content

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

Description

@aala-conga

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.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:

"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)
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 call targets are invented when they cannot be worked out, and the invented ones look exactly like real ones #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

I did not check Kotlin, PHP or C#, which also have abstract classes. Java is not affected.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions