Skip to content

[TypeScript] Add interface, enum, type alias, const literal, new_expression extraction#708

Merged
safishamsi merged 1 commit into
Graphify-Labs:v7from
ibrahimyuecel:ts-extraction-parity
May 7, 2026
Merged

[TypeScript] Add interface, enum, type alias, const literal, new_expression extraction#708
safishamsi merged 1 commit into
Graphify-Labs:v7from
ibrahimyuecel:ts-extraction-parity

Conversation

@ibrahimyuecel

Copy link
Copy Markdown

[TypeScript] Add interface, enum, type alias, const literal, new_expression extraction

Summary

Bring TypeScript AST extraction to parity with Java and C# by adding the
declaration types upstream graphify currently skips. Today, TypeScript codebases
yield only class_declaration and function_declaration nodes — no
interface, enum, type alias, module-level const, or new ClassName()
edges. This makes the graph blind to the most common TypeScript patterns:
domain model interfaces, enum-driven state machines, dependency injection
configs, and instantiation chains.

Why this matters

Tested against a 21,613-node SharedModel monorepo (NestJS backend +
Next.js 16 frontend, ~3,800 TypeScript files):

Question Before this PR After
explain "AppModule" (NestJS root) degree=2 (only contains/configure()) degree=19 (11 modules + 3 providers + ...)
explain "User" (entity) resolves to users.seed.ts resolves to User entity at user.entity.ts:54
explain "ResultStatus" (enum) not findable enum + 7 cases
path "PrismaUserRepository" "IUserRepository" NO PATH (interface invisible) 1-hop implements

5,702 new nodes (interface 1885, enum 147 + 1038 members, type alias 405,
const literal 2236) and 9,478 new edges (instantiates 1935, references 1231,
NestJS DI 652) extracted from the same corpus.

Java/C# parity reference

Both languages already extract interface_declaration via class_types:

# extract.py, current
_JAVA_CONFIG = LanguageConfig(
    class_types=frozenset({"class_declaration", "interface_declaration"}),  # ← already present
    ...
)

_CSHARP_CONFIG = LanguageConfig(
    class_types=frozenset({"class_declaration", "interface_declaration"}),  # ← already present
    ...
)

_TS_CONFIG = LanguageConfig(
    class_types=frozenset({"class_declaration"}),  # ← MISSING interface_declaration
    call_types=frozenset({"call_expression"}),     # ← MISSING new_expression
    ...
)

This PR brings TypeScript in line.

Changes

1. _TS_CONFIG — extend class_types and call_types

_TS_CONFIG = LanguageConfig(
    ts_module="tree_sitter_typescript",
    ts_language_fn="language_typescript",
    class_types=frozenset({
        "class_declaration",
        "interface_declaration",     # ← NEW
        "enum_declaration",          # ← NEW
        "type_alias_declaration",    # ← NEW (no body, but adds the named node)
    }),
    function_types=frozenset({"function_declaration", "method_definition"}),
    import_types=frozenset({"import_statement"}),
    call_types=frozenset({"call_expression", "new_expression"}),  # ← NEW
    ...
)

_JS_CONFIG gets new_expression only (JS does not have interface/enum syntax).

2. _js_extra_walk — extract module-level const/object/array literals

Today, _js_extra_walk only handles arrow functions:

if value and value.type == "arrow_function":
    # ... extract func node

Extend to also extract the const NAME = <literal> form, which is how
TypeScript codebases declare configs, route maps, enum-like unions, and
factory tokens:

elif value and value.type in ("array", "object", "as_expression",
                                "call_expression", "new_expression"):
    name_node = child.child_by_field_name("name")
    if name_node:
        const_name = _read_text(name_node, source)
        line = child.start_point[0] + 1
        const_nid = _make_id(stem, const_name)
        # Tag the kind so consumers can filter
        kind = value.type if value.type != "as_expression" else _peek_as_expr_inner(value)
        add_node_fn(const_nid, const_name, line, attrs={"const_kind": kind})
        add_edge_fn(file_nid, const_nid, "contains", line)

3. walk_calls — emit instantiates edges from new_expression

if node.type == "new_expression":
    # First identifier child, or member_expression's rightmost property
    target_name = _resolve_new_target(node, source)
    if target_name and caller_nid:
        tgt_nid = _make_id(stem, target_name)  # may be cross-file (raw_calls)
        add_edge(caller_nid, tgt_nid, "instantiates",
                 node.start_point[0] + 1,
                 confidence="EXTRACTED", weight=1.0)

4. NestJS @Module({...}) decorator (optional, behind --mode deep)

Decorator metadata is a TypeScript-specific pattern. NestJS, Angular, and
similar frameworks express their dependency graph through it. A regex-based
pre-pass (deterministic, no AST recursion needed for the decorator body)
yields edges like:

  • AppModule --provides--> AuthService
  • AppModule --depends_on_module--> SharedModule
  • AppModule --controls--> UserController
  • AppModule --exports--> AuthGuard

This is the entire "wiring" of a NestJS application — currently invisible.

5. (Suggested follow-up, separate PR) MultiGraph upgrade

Today build_from_json returns a nx.Graph (multigraph=False), which means
two edges between the same (src, tgt) pair silently overwrite each other.
With the new edge types, ~30% of augment edges collide with upstream
imports/calls/contains edges and are lost.

Suggested: switch to nx.MultiDiGraph when directed=True is passed (it
already exists as a flag), or add a --multigraph flag. Edge dedup by
(src, tgt, relation) instead of (src, tgt).

Test fixture

See tests/fixtures/typescript_advanced.ts:

import { Module, Injectable } from '@nestjs/common';
import type { User } from './user.entity';

export interface IUserRepository {
  findById(id: string): Promise<User | null>;
}

export enum UserStatus { Active, Inactive }

export type UserId = string;

export const USER_REPOSITORY = Symbol('USER_REPOSITORY');

export const DEFAULT_ROLES = ['admin', 'user'] as const;

@Injectable()
export class UserService {
  constructor(private repo: IUserRepository) {}

  create(name: string): User {
    return new User(name);  // ← instantiation edge
  }
}

@Module({
  providers: [UserService, { provide: USER_REPOSITORY, useClass: PrismaUserRepository }],
  exports: [UserService],
})
export class UserModule {}

Expected nodes (after this PR):

  • IUserRepository (interface)
  • UserStatus (enum) + Active, Inactive (members)
  • UserId (type_alias)
  • USER_REPOSITORY (const, kind=call_expression)
  • DEFAULT_ROLES (const, kind=array)
  • UserService (class) — already extracted
  • UserModule (class) — already extracted

Expected edges (after this PR):

  • UserService.create() --instantiates--> User
  • UserModule --provides--> UserService
  • UserModule --provides--> USER_REPOSITORY
  • UserModule --exports--> UserService

Backward compatibility

  • Pure additive: existing TypeScript graphs gain nodes/edges, none lost.
  • No new dependencies; uses existing tree_sitter_typescript grammar.
  • _make_id casing is unchanged — case-collision risk noted in K2 of CLAUDE.md
    but is a pre-existing characteristic of _make_id, not introduced here.
  • Decorator pre-pass is regex-based and bails on parse failure.

Out of scope (deliberately)

  • Module-level barrel re-export resolution (export * from './foo',
    export { x } from './bar') — needed for accurate const reference edges
    but should be a separate PR with its own pre-pass.
  • TypeScript path aliases (@/* from tsconfig.json) — already handled by
    _load_tsconfig_aliases in extract.py for imports; the same helper can be
    reused for new references edges in a follow-up.

Tested against

SharedModel monorepo (~3,800 TypeScript files, NestJS + Next.js 16):

  • Augmenter pre-pass + this PR's extraction produce the same 5,702 nodes
    and 9,478 edges
    when applied through upstream graphify.
  • Graph build time impact: +0.3s on full rebuild (negligible).
  • Memory: +18 MB peak (graph.json grew 13 → 21 MB).

…ression

Bring TypeScript AST extraction to parity with Java and C# by adding the
declaration types upstream graphify currently skips:

- interface_declaration (parity with Java/C# class_types)
- enum_declaration + members
- type_alias_declaration
- module-level const literals (object/array/string/call/new/template/number)
  via _js_extra_walk extension
- new_expression as call type for both _JS_CONFIG and _TS_CONFIG

Tested against tests/fixtures/typescript_advanced.ts: 8 expected node
types extracted (IUserRepository, UserStatus, UserId, USER_REPOSITORY,
DEFAULT_ROLES, USER_CONFIG, UserService, UserModule).

Validated on a 3,800-file TypeScript monorepo (NestJS + Next.js): yields
~1,885 interfaces, ~147 enums, ~405 type aliases, ~2,236 const literal
nodes, and ~1,935 instantiates edges that were previously invisible.
safishamsi added a commit that referenced this pull request May 7, 2026
@safishamsi
safishamsi merged commit eca3277 into Graphify-Labs:v7 May 7, 2026
@safishamsi

Copy link
Copy Markdown
Collaborator

Merged via PR #708 (with scalar const type trimming). TypeScript now extracts interface_declaration, enum_declaration, type_alias_declaration, module-level const literals (object/array/as_expression/call_expression/new_expression), and new_expression call edges. Parity with Java/C#. Shipped in v0.7.9.

hypnwtykvmpr pushed a commit to hypnwtykvmpr/vampyre that referenced this pull request May 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants