From eca3277fb964f06ea1ef00dafc5d0113038d80c1 Mon Sep 17 00:00:00 2001 From: ibrahimyuecel Date: Mon, 4 May 2026 20:46:36 +0300 Subject: [PATCH 1/3] 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. --- graphify/extract.py | 29 +++++++++-- tests/fixtures/typescript_advanced.ts | 69 +++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 tests/fixtures/typescript_advanced.ts diff --git a/graphify/extract.py b/graphify/extract.py index 42fd78f7..db0209fc 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -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", diff --git a/tests/fixtures/typescript_advanced.ts b/tests/fixtures/typescript_advanced.ts new file mode 100644 index 00000000..bd271c1d --- /dev/null +++ b/tests/fixtures/typescript_advanced.ts @@ -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; + save(user: User): Promise; +} + +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 {} From 2d447889316a30e1f461eaf694df2f53b04adfdf Mon Sep 17 00:00:00 2001 From: Safi Date: Thu, 7 May 2026 11:02:11 +0100 Subject: [PATCH 2/3] add .qmd to to_obsidian_canvas safe_name regex --- graphify/export.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphify/export.py b/graphify/export.py index 7121a2c6..3599f6d0 100644 --- a/graphify/export.py +++ b/graphify/export.py @@ -884,7 +884,7 @@ def to_canvas( def safe_name(label: str) -> str: cleaned = re.sub(r'[\\/*?:"<>|#^[\]]', "", label.replace("\r\n", " ").replace("\r", " ").replace("\n", " ")).strip() - cleaned = re.sub(r"\.(md|mdx|markdown)$", "", cleaned, flags=re.IGNORECASE) + cleaned = re.sub(r"\.(md|mdx|qmd|markdown)$", "", cleaned, flags=re.IGNORECASE) return cleaned or "unnamed" # Build node_filenames if not provided (same dedup logic as to_obsidian) From 9bc79ef6e3196ac5865eb84024a0593477822a0d Mon Sep 17 00:00:00 2001 From: Safi Date: Thu, 7 May 2026 11:02:15 +0100 Subject: [PATCH 3/3] trim noisy scalar const types from js_extra_walk, collapse docstring --- graphify/extract.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/graphify/extract.py b/graphify/extract.py index db0209fc..fd9daa87 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -611,11 +611,7 @@ 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 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.""" + """Handle lexical_declaration (arrow functions and module-level const literals) for JS/TS. Returns True if handled.""" if node.type == "lexical_declaration": for child in node.children: if child.type == "variable_declarator": @@ -632,8 +628,7 @@ def _js_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: str, 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", + "object", "array", "as_expression", "call_expression", "new_expression", ): # Module-level const with literal/object/array/factory value name_node = child.child_by_field_name("name")