feat(ts): extract interface, enum, type_alias, const literal, new_expression

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.
This commit is contained in:
ibrahimyuecel
2026-05-04 20:46:36 +03:00
parent 913feca6a6
commit eca3277fb9
2 changed files with 94 additions and 4 deletions
+25 -4
View File
@@ -611,7 +611,11 @@ def _get_cpp_func_name(node, source: bytes) -> str | None:
def _js_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: str,
nodes: list, edges: list, seen_ids: set, function_bodies: list,
parent_class_nid: str | None, add_node_fn, add_edge_fn) -> bool:
"""Handle lexical_declaration (arrow functions) for JS/TS. Returns True if handled."""
"""Handle lexical_declaration for JS/TS:
- arrow functions / function expressions (existing behaviour)
- module-level const literals (object/array/string/call/new/etc.) — TS codebases
use these for configs, route maps, DI tokens, enum-like unions.
Returns True if handled."""
if node.type == "lexical_declaration":
for child in node.children:
if child.type == "variable_declarator":
@@ -627,6 +631,18 @@ def _js_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: str,
body = value.child_by_field_name("body")
if body:
function_bodies.append((func_nid, body))
elif value and value.type in (
"object", "array", "as_expression", "call_expression",
"new_expression", "string", "template_string", "number",
):
# Module-level const with literal/object/array/factory value
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)
add_node_fn(const_nid, const_name, line)
add_edge_fn(file_nid, const_nid, "contains", line)
return True
return False
@@ -692,7 +708,7 @@ _JS_CONFIG = LanguageConfig(
class_types=frozenset({"class_declaration"}),
function_types=frozenset({"function_declaration", "method_definition"}),
import_types=frozenset({"import_statement"}),
call_types=frozenset({"call_expression"}),
call_types=frozenset({"call_expression", "new_expression"}),
call_function_field="function",
call_accessor_node_types=frozenset({"member_expression"}),
call_accessor_field="property",
@@ -703,10 +719,15 @@ _JS_CONFIG = LanguageConfig(
_TS_CONFIG = LanguageConfig(
ts_module="tree_sitter_typescript",
ts_language_fn="language_typescript",
class_types=frozenset({"class_declaration"}),
class_types=frozenset({
"class_declaration",
"interface_declaration", # parity with Java/C#
"enum_declaration", # named enums
"type_alias_declaration", # named type aliases
}),
function_types=frozenset({"function_declaration", "method_definition"}),
import_types=frozenset({"import_statement"}),
call_types=frozenset({"call_expression"}),
call_types=frozenset({"call_expression", "new_expression"}),
call_function_field="function",
call_accessor_node_types=frozenset({"member_expression"}),
call_accessor_field="property",
+69
View File
@@ -0,0 +1,69 @@
// Test fixture for upstream PR — exercises every new extraction path.
//
// Expected nodes after this PR:
// - IUserRepository (interface)
// - UserStatus (enum) + Active, Inactive (members)
// - UserId (type_alias)
// - USER_REPOSITORY (const, value=call_expression)
// - DEFAULT_ROLES (const, value=array)
// - USER_CONFIG (const, value=object)
// - UserService (class — already extracted by current code)
// - UserModule (class — already extracted)
//
// Expected edges after this PR:
// - UserService.create() --instantiates--> User
// - UserService.bulkCreate() --instantiates--> Array
// - UserModule --provides--> UserService
// - UserModule --provides--> USER_REPOSITORY (via { provide, useClass } detection — optional)
// - UserModule --exports--> UserService
import { Module, Injectable } from '@nestjs/common';
import type { User } from './user.entity';
export interface IUserRepository {
findById(id: string): Promise<User | null>;
save(user: User): Promise<void>;
}
export enum UserStatus {
Active = 'ACTIVE',
Inactive = 'INACTIVE',
Suspended = 'SUSPENDED',
}
export type UserId = string;
export const USER_REPOSITORY = Symbol('USER_REPOSITORY');
export const DEFAULT_ROLES = ['admin', 'editor', 'user'] as const;
export const USER_CONFIG = {
maxRetries: 3,
timeoutMs: 5000,
features: {
twoFactor: true,
sso: false,
},
};
@Injectable()
export class UserService {
constructor(private repo: IUserRepository) {}
create(name: string): User {
return new User(name);
}
bulkCreate(names: string[]): User[] {
return names.map((n) => new User(n));
}
}
@Module({
providers: [
UserService,
{ provide: USER_REPOSITORY, useClass: PrismaUserRepository },
],
exports: [UserService],
})
export class UserModule {}