diff --git a/CHANGELOG.md b/CHANGELOG.md index b5023740..98233d7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu ## 0.7.9 (unreleased) +- Feat: TypeScript extraction parity -- interface, enum, type alias, and module-level const nodes extracted; new_expression emits calls edges; parity with Java/C# class_types - Feat: Quarto (`.qmd`) file support -- routed through existing Markdown extractor; Quarto executable code blocks (` ```{python} `) extracted as code nodes - Feat: optional Google Workspace shortcut export for headless extraction -- `graphify extract ./docs --google-workspace` converts `.gdoc`, `.gsheet`, and `.gslides` files into Markdown sidecars with the `gws` CLI before semantic extraction; account email pseudonymized via SHA256 hash; `[google]` extra adds Sheets table rendering support - Feat: AWS Bedrock backend -- `graphify extract ./docs --backend bedrock`; credentials via standard AWS provider chain (AWS_PROFILE, AWS_REGION, IAM roles, SSO); model via GRAPHIFY_BEDROCK_MODEL (default anthropic.claude-3-5-sonnet-20241022-v2:0); `[bedrock]` extra adds boto3 diff --git a/graphify/extract.py b/graphify/extract.py index 4afb6712..bfced36d 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -778,16 +778,14 @@ def _require_imports_js(node, source: bytes, file_nid: str, stem: str, edges: li 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, CJS requires) for JS/TS. - - Returns True if handled (caller should not descend further). - """ + """Handle lexical_declaration (arrow functions, CJS requires, module-level const literals) for JS/TS. Returns True if handled.""" if node.type in ("lexical_declaration", "variable_declaration"): # CJS require imports — emit edges, do not block other lexical_declaration handling require_found = _require_imports_js(node, source, file_nid, stem, edges, str_path) - # Arrow function declarations (existing behavior, lexical_declaration only) + # Arrow function declarations and module-level const literals (lexical_declaration only) arrow_found = False + const_found = False if node.type == "lexical_declaration": for child in node.children: if child.type == "variable_declarator": @@ -804,8 +802,22 @@ def _js_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: str, if body: function_bodies.append((func_nid, body)) arrow_found = True + elif value and value.type in ( + "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") + 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) + const_found = True if arrow_found: return True + if const_found: + return True if require_found: return True return False @@ -872,7 +884,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", @@ -883,10 +895,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 {}