From 6d16382a52fe3d3dd1abb092c59bfa34a4b754d3 Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Mon, 27 Jul 2026 14:21:27 -0400 Subject: [PATCH 01/10] feat(typescript): TSCallableOverview projection model (#298) --- cldk/models/typescript/__init__.py | 2 + cldk/models/typescript/projections.py | 113 ++++++++++++++++++ tests/models/typescript/__init__.py | 0 .../models/typescript/test_ts_projections.py | 81 +++++++++++++ 4 files changed, 196 insertions(+) create mode 100644 cldk/models/typescript/projections.py create mode 100644 tests/models/typescript/__init__.py create mode 100644 tests/models/typescript/test_ts_projections.py diff --git a/cldk/models/typescript/__init__.py b/cldk/models/typescript/__init__.py index 93e82b93..319708e5 100644 --- a/cldk/models/typescript/__init__.py +++ b/cldk/models/typescript/__init__.py @@ -42,11 +42,13 @@ TSTypeParameter, TSVariableDeclaration, ) +from .projections import TSCallableOverview __all__ = [ "TSApplication", "TSCallEdge", "TSCallable", + "TSCallableOverview", "TSCallableParameter", "TSCallsite", "TSClass", diff --git a/cldk/models/typescript/projections.py b/cldk/models/typescript/projections.py new file mode 100644 index 00000000..36170c89 --- /dev/null +++ b/cldk/models/typescript/projections.py @@ -0,0 +1,113 @@ +################################################################################ +# Copyright IBM Corporation 2026 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +################################################################################ + +"""CLDK-defined projection models for the TypeScript facade. + +Unlike the rest of :mod:`cldk.models.typescript`, these are **not** part of the +``codeanalyzer-typescript`` schema — they are lightweight, field-projected views CLDK exposes so +callers can enumerate the application set-at-a-time without paying for the full per-callable +reconstruction. They map cleanly to a single Cypher ``RETURN`` on the Neo4j backend and to one +symbol-table walk in-process. +""" + +from __future__ import annotations + +from typing import List, Optional + +from pydantic import BaseModel + +from .models import TSCallable + + +class TSCallableOverview(BaseModel): + """A lightweight projection of one callable — enough to enumerate and filter without the full + :class:`~cldk.models.typescript.TSCallable` reconstruction (call-sites, inner callables, + locals). + + Returned set-at-a-time by ``TypescriptAnalysis.get_callables_overview`` / + ``TypescriptAnalysis.get_decorated_callables``. Body-inspect only the few you need afterwards + via ``TypescriptAnalysis.get_method``/``TypescriptAnalysis.get_method_bodies``. + + Attributes: + signature: The callable's unique signature (the key the call graph references). + name: The callable's short name. + owner_signature: Signature of the class/interface/namespace that declares this callable, + or ``None`` for a module-level function or arrow. + owner_kind: The owner's node kind (e.g. ``"class"``, ``"interface"``, ``"namespace"``), or + ``None`` when there is no owner. ``owner_kind`` is ``None`` iff ``owner_signature`` is + ``None``. + kind: The callable's native TS kind, passed through verbatim — one of ``function``, + ``method``, ``constructor``, ``getter``, ``setter``, ``arrow``, + ``function_expression``. Never derived; always ``TSCallable.kind`` as reported by the + analyzer. + path: Project-relative path of the declaring module. + start_line / end_line: The callable's line span. + decorators: The decorator names applied to the callable (``TSDecorator.name`` only). + is_exported: Whether the callable (or its enclosing declaration) is exported. + is_async: Whether the callable is declared ``async``. + is_static: Whether the callable is a static class member. + accessibility: The callable's declared accessibility (``public``/``protected``/``private``), + or ``None`` when unspecified. + """ + + signature: str + name: str + owner_signature: Optional[str] = None + owner_kind: Optional[str] = None + kind: str + path: str + start_line: int + end_line: int + decorators: List[str] = [] + is_exported: bool = False + is_async: bool = False + is_static: bool = False + accessibility: Optional[str] = None + + @classmethod + def from_callable( + cls, + c: TSCallable, + owner_signature: Optional[str], + owner_kind: Optional[str], + ) -> TSCallableOverview: + """Project a :class:`~cldk.models.typescript.TSCallable` into a + :class:`TSCallableOverview`. + + Args: + c: The callable to project. + owner_signature: Signature of the declaring class/interface/namespace, or ``None`` for + a module-level function or arrow. + owner_kind: The owner's node kind, or ``None`` when ``owner_signature`` is ``None``. + + Returns: + The projected overview. + """ + return cls( + signature=c.signature, + name=c.name, + owner_signature=owner_signature, + owner_kind=owner_kind, + kind=c.kind, + path=c.path, + start_line=c.start_line, + end_line=c.end_line, + decorators=[d.name for d in c.decorators], + is_exported=c.is_exported, + is_async=c.is_async, + is_static=c.is_static, + accessibility=c.accessibility, + ) diff --git a/tests/models/typescript/__init__.py b/tests/models/typescript/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/models/typescript/test_ts_projections.py b/tests/models/typescript/test_ts_projections.py new file mode 100644 index 00000000..4e448d01 --- /dev/null +++ b/tests/models/typescript/test_ts_projections.py @@ -0,0 +1,81 @@ +################################################################################ +# Copyright IBM Corporation 2026 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +################################################################################ + +"""Tests for :mod:`cldk.models.typescript.projections`.""" + +from cldk.models.typescript import TSCallable, TSDecorator +from cldk.models.typescript.projections import TSCallableOverview + + +def test_from_callable_method_case_projects_owner_pair_and_decorators(): + c = TSCallable( + name="getUser", + path="src/user.ts", + signature="src/user.UserService.getUser", + decorators=[TSDecorator(name="Get"), TSDecorator(name="Deprecated")], + start_line=10, + end_line=15, + kind="method", + accessibility="public", + is_static=False, + is_async=True, + is_exported=False, + ) + + overview = TSCallableOverview.from_callable( + c, owner_signature="src/user.UserService", owner_kind="class" + ) + + assert overview.signature == "src/user.UserService.getUser" + assert overview.name == "getUser" + assert overview.owner_signature == "src/user.UserService" + assert overview.owner_kind == "class" + assert overview.kind == "method" + assert overview.path == "src/user.ts" + assert overview.start_line == 10 + assert overview.end_line == 15 + assert overview.decorators == ["Get", "Deprecated"] + assert overview.is_exported is False + assert overview.is_async is True + assert overview.is_static is False + assert overview.accessibility == "public" + + +def test_from_callable_arrow_case_has_none_owner_pair(): + c = TSCallable( + name="handler", + path="src/handlers.ts", + signature="src/handlers.handler", + start_line=1, + end_line=3, + kind="arrow", + is_exported=True, + ) + + overview = TSCallableOverview.from_callable(c, owner_signature=None, owner_kind=None) + + assert overview.owner_signature is None + assert overview.owner_kind is None + assert overview.kind == "arrow" + assert overview.decorators == [] + assert overview.is_exported is True + assert overview.is_async is False + assert overview.is_static is False + assert overview.accessibility is None + + +def test_no_code_field(): + assert "code" not in TSCallableOverview.model_fields From e2c35a2002c20345948e4519c6066a41cea477c0 Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Mon, 27 Jul 2026 14:45:37 -0400 Subject: [PATCH 02/10] fix(typescript): close owner_kind to class|interface; require facet fields (#298) --- cldk/models/typescript/projections.py | 24 ++++++++++--------- .../models/typescript/test_ts_projections.py | 22 +++++++++++++++++ 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/cldk/models/typescript/projections.py b/cldk/models/typescript/projections.py index 36170c89..63bd7635 100644 --- a/cldk/models/typescript/projections.py +++ b/cldk/models/typescript/projections.py @@ -44,11 +44,12 @@ class TSCallableOverview(BaseModel): Attributes: signature: The callable's unique signature (the key the call graph references). name: The callable's short name. - owner_signature: Signature of the class/interface/namespace that declares this callable, - or ``None`` for a module-level function or arrow. - owner_kind: The owner's node kind (e.g. ``"class"``, ``"interface"``, ``"namespace"``), or - ``None`` when there is no owner. ``owner_kind`` is ``None`` iff ``owner_signature`` is - ``None``. + owner_signature: Signature of the class/interface that declares this callable, or ``None`` + for a module-level function, arrow, or namespace-owned function. + owner_kind: The owner's node kind — a closed two-value set, ``"class"`` or ``"interface"`` + — or ``None`` when there is no owner. ``owner_kind`` is ``None`` iff + ``owner_signature`` is ``None``. Namespace-owned functions (``TSNamespace.functions``) + carry no owner pair; their dotted signature already encodes the namespace path. kind: The callable's native TS kind, passed through verbatim — one of ``function``, ``method``, ``constructor``, ``getter``, ``setter``, ``arrow``, ``function_expression``. Never derived; always ``TSCallable.kind`` as reported by the @@ -72,9 +73,9 @@ class TSCallableOverview(BaseModel): start_line: int end_line: int decorators: List[str] = [] - is_exported: bool = False - is_async: bool = False - is_static: bool = False + is_exported: bool + is_async: bool + is_static: bool accessibility: Optional[str] = None @classmethod @@ -89,9 +90,10 @@ def from_callable( Args: c: The callable to project. - owner_signature: Signature of the declaring class/interface/namespace, or ``None`` for - a module-level function or arrow. - owner_kind: The owner's node kind, or ``None`` when ``owner_signature`` is ``None``. + owner_signature: Signature of the declaring class/interface, or ``None`` for a + module-level function, arrow, or namespace-owned function. + owner_kind: The owner's node kind (``"class"`` or ``"interface"``), or ``None`` when + ``owner_signature`` is ``None``. Returns: The projected overview. diff --git a/tests/models/typescript/test_ts_projections.py b/tests/models/typescript/test_ts_projections.py index 4e448d01..970ea844 100644 --- a/tests/models/typescript/test_ts_projections.py +++ b/tests/models/typescript/test_ts_projections.py @@ -77,5 +77,27 @@ def test_from_callable_arrow_case_has_none_owner_pair(): assert overview.accessibility is None +def test_from_callable_namespace_owned_function_has_none_owner_pair(): + """Namespace-owned functions (TSNamespace.functions) are ownerless: TS namespaces are + module-like scoping, not a class/interface owner, and the dotted signature already encodes + the namespace path — so the owner pair stays None/None, same as module-level and nested + callables.""" + c = TSCallable( + name="parse", + path="src/util.ts", + signature="src/util.Parsing.Inner.parse", + start_line=5, + end_line=8, + kind="function", + is_exported=True, + ) + + overview = TSCallableOverview.from_callable(c, owner_signature=None, owner_kind=None) + + assert overview.owner_signature is None + assert overview.owner_kind is None + assert overview.signature == "src/util.Parsing.Inner.parse" + + def test_no_code_field(): assert "code" not in TSCallableOverview.model_fields From 2c0d1c62e22dc2ce494b61045efccf1616c814ef Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Mon, 27 Jul 2026 14:53:56 -0400 Subject: [PATCH 03/10] feat(typescript): bulk accessors on the ABC, in-memory backend, and facade (#298) --- cldk/analysis/typescript/backend.py | 27 +++ .../typescript/codeanalyzer/codeanalyzer.py | 48 +++- .../typescript/typescript_analysis.py | 67 ++++++ .../test_typescript_bulk_accessors.py | 220 ++++++++++++++++++ 4 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 tests/analysis/typescript/test_typescript_bulk_accessors.py diff --git a/cldk/analysis/typescript/backend.py b/cldk/analysis/typescript/backend.py index 6b75519d..d165493c 100644 --- a/cldk/analysis/typescript/backend.py +++ b/cldk/analysis/typescript/backend.py @@ -44,6 +44,7 @@ from cldk.models.typescript import ( TSApplication, TSCallable, + TSCallableOverview, TSCallsite, TSClass, TSClassAttribute, @@ -244,3 +245,29 @@ def get_methods_with_decorators(self, decorators: List[str]) -> Dict[str, List[s @abstractmethod def get_classes_with_decorators(self, decorators: List[str]) -> Dict[str, List[str]]: """Map each requested decorator name to the signatures of classes carrying it.""" + + # -----[ bulk / projected accessors ]----- + # Set-at-a-time, field-projected reads — one round-trip on the Neo4j backend, one symbol-table + # walk in-process — for callers that enumerate the whole application and would otherwise pay the + # per-entity reconstruction of get_all_methods_in_application. + @abstractmethod + def get_callables_overview(self) -> List[TSCallableOverview]: + """A lightweight projection of every callable in the application (methods, module-level, + namespace-level, and nested/inner functions), without the full :class:`TSCallable` + reconstruction.""" + + @abstractmethod + def get_method_bodies(self, signatures: List[str]) -> Dict[str, str]: + """Source bodies for the given callable signatures, keyed by signature. Signatures with no + matching callable are omitted.""" + + @abstractmethod + def get_decorated_callables(self, markers: List[str]) -> List[TSCallableOverview]: + """Overviews of callables decorated with any of ``markers`` (matched against the decorator + names).""" + + @abstractmethod + def get_callsites_for(self, signatures: List[str]) -> Dict[str, List[TSCallsite]]: + """Call sites of the given callable signatures, keyed by owning signature. Each existing + signature gets an entry (an empty list if it has no call sites); signatures with no matching + callable are omitted.""" diff --git a/cldk/analysis/typescript/codeanalyzer/codeanalyzer.py b/cldk/analysis/typescript/codeanalyzer/codeanalyzer.py index 4c442f40..ed39f1da 100644 --- a/cldk/analysis/typescript/codeanalyzer/codeanalyzer.py +++ b/cldk/analysis/typescript/codeanalyzer/codeanalyzer.py @@ -33,7 +33,7 @@ from collections import deque from pathlib import Path from subprocess import CompletedProcess -from typing import Dict, List, Set, Tuple, Union +from typing import Dict, Iterator, List, Set, Tuple, Union import networkx as nx @@ -42,6 +42,7 @@ from cldk.models.typescript import ( TSApplication, TSCallable, + TSCallableOverview, TSCallsite, TSClass, TSClassAttribute, @@ -554,3 +555,48 @@ def get_classes_with_decorators(self, decorators: List[str]) -> Dict[str, List[s if dec.name in wanted: result[dec.name].append(sig) return result + + # -----[ bulk / projected accessors ]----- + def _iter_callables(self) -> Iterator[Tuple[TSCallable, str | None, str | None]]: + """Yield ``(callable, owner_signature, owner_kind)`` for every callable in the + application, including inner/nested callables. The owner map is built only from + ``_methods_by_class`` keyed against ``_classes``/``_interfaces``: namespace-owned + functions and module-level/nested callables are never in that map, so they correctly come + out owner-less (None, None), per the closed "class"|"interface" owner_kind set.""" + owner_of: Dict[str, Tuple[str, str]] = {} + for owner_sig, methods in self._methods_by_class.items(): + if owner_sig in self._classes: + owner_kind = "class" + elif owner_sig in self._interfaces: + owner_kind = "interface" + else: + continue + for m in methods.values(): + owner_of[m.signature] = (owner_sig, owner_kind) + for sig, c in self._callables.items(): + owner_sig, owner_kind = owner_of.get(sig, (None, None)) + yield c, owner_sig, owner_kind + + def get_callables_overview(self) -> List[TSCallableOverview]: + """Return a lightweight overview of every callable in the application (see + :meth:`TSAnalysisBackend.get_callables_overview`).""" + return [TSCallableOverview.from_callable(c, owner_sig, owner_kind) for c, owner_sig, owner_kind in self._iter_callables()] + + def get_method_bodies(self, signatures: List[str]) -> Dict[str, str]: + """Return ``{signature: code}`` for the requested signatures that exist.""" + wanted = set(signatures) + return {c.signature: c.code for c, _, _ in self._iter_callables() if c.signature in wanted} + + def get_decorated_callables(self, markers: List[str]) -> List[TSCallableOverview]: + """Return overviews of callables decorated with any of ``markers``.""" + marker_set = set(markers) + return [ + TSCallableOverview.from_callable(c, owner_sig, owner_kind) + for c, owner_sig, owner_kind in self._iter_callables() + if marker_set.intersection(d.name for d in c.decorators) + ] + + def get_callsites_for(self, signatures: List[str]) -> Dict[str, List[TSCallsite]]: + """Return ``{signature: call_sites}`` for the requested signatures that exist.""" + wanted = set(signatures) + return {c.signature: list(c.call_sites) for c, _, _ in self._iter_callables() if c.signature in wanted} diff --git a/cldk/analysis/typescript/typescript_analysis.py b/cldk/analysis/typescript/typescript_analysis.py index eef691b1..68b14232 100644 --- a/cldk/analysis/typescript/typescript_analysis.py +++ b/cldk/analysis/typescript/typescript_analysis.py @@ -36,6 +36,7 @@ from cldk.models.typescript import ( TSApplication, TSCallable, + TSCallableOverview, TSCallsite, TSClass, TSClassAttribute, @@ -297,3 +298,69 @@ def get_methods_with_decorators(self, decorators: List[str]) -> Dict[str, List[s def get_classes_with_decorators(self, decorators: List[str]) -> Dict[str, List[str]]: """Map each requested decorator name to the signatures of classes carrying it.""" return self.backend.get_classes_with_decorators(decorators) + + # -----[ bulk / projected accessors ]----- + def get_callables_overview(self) -> List[TSCallableOverview]: + """Return a lightweight overview of every callable in the project, in one bulk read. + + A field-projected alternative to :meth:`get_methods` for enumeration: each + :class:`~cldk.models.typescript.TSCallableOverview` carries the callable's signature, + owning class/interface (if any), native kind, location, and decorators — but not the full + reconstruction (call sites, inner callables, locals). On the Neo4j backend this is a single + Cypher query instead of the per-entity fan-out :meth:`get_methods` pays. Body-inspect the + few you need afterwards via :meth:`get_method` or :meth:`get_method_bodies`. + + Returns: + A flat list of :class:`~cldk.models.typescript.TSCallableOverview`, one per callable + (class/interface methods, module- and namespace-level functions, and nested/inner + callables). + + See Also: + :meth:`get_decorated_callables`: The same projection filtered by decorator. + :meth:`get_method_bodies`: Bulk source-body fetch for chosen signatures. + """ + return self.backend.get_callables_overview() + + def get_method_bodies(self, signatures: List[str]) -> Dict[str, str]: + """Return source bodies for the given callable signatures, in one bulk read. + + Args: + signatures: Callable signatures to fetch bodies for (e.g. from + :meth:`get_callables_overview`). + + Returns: + A dict mapping each signature to its source body. Signatures with no matching callable + are omitted. + """ + return self.backend.get_method_bodies(signatures) + + def get_decorated_callables(self, markers: List[str]) -> List[TSCallableOverview]: + """Return overviews of callables decorated with any of the given markers, in one bulk read. + + Args: + markers: Decorator names to match (e.g. ``["Get", "Controller"]``). + + Returns: + A list of :class:`~cldk.models.typescript.TSCallableOverview` for every callable + carrying at least one of ``markers`` as a decorator. + + See Also: + :meth:`get_callables_overview`: The unfiltered projection. + """ + return self.backend.get_decorated_callables(markers) + + def get_callsites_for(self, signatures: List[str]) -> Dict[str, List[TSCallsite]]: + """Return the call sites of the given callables, keyed by signature, in one bulk read. + + Avoids the per-callable reconstruction fan-out when you need call sites for a specific + frontier (e.g. dispatch-edge synthesis or external-reader detection). + + Args: + signatures: Callable signatures to fetch call sites for. + + Returns: + A dict mapping each existing signature to its list of + :class:`~cldk.models.typescript.TSCallsite` (empty if the callable has no call sites). + Signatures with no matching callable are omitted. + """ + return self.backend.get_callsites_for(signatures) diff --git a/tests/analysis/typescript/test_typescript_bulk_accessors.py b/tests/analysis/typescript/test_typescript_bulk_accessors.py new file mode 100644 index 00000000..4bd2e806 --- /dev/null +++ b/tests/analysis/typescript/test_typescript_bulk_accessors.py @@ -0,0 +1,220 @@ +################################################################################ +# Copyright IBM Corporation 2026 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +################################################################################ + +"""Tests for the four bulk/projected accessors (#298): +``get_callables_overview`` / ``get_method_bodies`` / ``get_decorated_callables`` / +``get_callsites_for`` — on the in-memory backend and the facade delegates. + +Built against the real sample-app fixture (``tests/resources/typescript/analysis_json/slim``) +already used elsewhere in this package. Expected sets below were derived by reading that fixture's +JSON directly (see the exploration notes in the task brief), never by running the implementation +and copying its output. +""" + +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +from cldk import CLDK +from cldk.analysis import AnalysisLevel +from cldk.analysis.commons.backend_config import CodeAnalyzerConfig +from cldk.models.typescript import TSCallableOverview + + +def _fake_run_writing_output(payload: str): + def _run(cmd, *args, **kwargs): + if "-o" in cmd: + out = Path(cmd[cmd.index("-o") + 1]) + out.mkdir(parents=True, exist_ok=True) + (out / "analysis.json").write_text(payload, encoding="utf-8") + return MagicMock(stdout=payload, returncode=0) + + return _run + + +@pytest.fixture +def ts_analysis(typescript_application, typescript_analysis_json, tmp_path, monkeypatch): + """A local-backend facade over the real sample-app fixture.""" + monkeypatch.setenv("CODEANALYZER_TS_BIN", "codeanalyzer-typescript") + with patch( + "cldk.analysis.typescript.codeanalyzer.codeanalyzer.subprocess.run", + side_effect=_fake_run_writing_output(typescript_analysis_json), + ): + return CLDK.typescript( + project_path=typescript_application, + eager=True, + analysis_level=AnalysisLevel.call_graph, + backend=CodeAnalyzerConfig(cache_dir=str(tmp_path)), + ) + + +# The fixture app has exactly 33 callables total (module functions, namespace functions, class and +# interface methods -- including nested classes' methods -- and inner/nested callables), 13 of +# which are owner-less: module-level functions, the one inner function +# (``src/util.classify.keyOf``), and the two namespace-owned functions on ``StringUtil`` +# (``repeat``/``slug``) per the ruling that namespace-owned functions carry no owner pair. +TOTAL_CALLABLES = 33 +OWNERLESS_SIGNATURES = { + "src/controllers.Controller", + "src/controllers.Get", + "src/controllers.Param", + "src/external.extensionOf", + "src/external.fingerprint", + "src/index.main", + "src/services.announce", + "src/services.makeGuestName", + "src/services.nextId", + "src/util.StringUtil.repeat", + "src/util.StringUtil.slug", + "src/util.classify", + "src/util.classify.keyOf", +} + + +def by_signature(overviews): + return {o.signature: o for o in overviews} + + +# -----[ get_callables_overview ]----- + + +def test_overview_enumerates_every_callable_including_inner(ts_analysis): + overview = ts_analysis.get_callables_overview() + assert all(isinstance(o, TSCallableOverview) for o in overview) + signatures = {o.signature for o in overview} + assert len(overview) == TOTAL_CALLABLES + assert len(signatures) == TOTAL_CALLABLES # no duplicates + assert "src/util.classify.keyOf" in signatures # inner callable is enumerated + + +def test_overview_owner_pair_is_none_for_owned_less_callables(ts_analysis): + rows = by_signature(ts_analysis.get_callables_overview()) + for sig in OWNERLESS_SIGNATURES: + assert rows[sig].owner_signature is None, sig + assert rows[sig].owner_kind is None, sig + + +def test_overview_known_method_row_full_field_tuple(ts_analysis): + rows = by_signature(ts_analysis.get_callables_overview()) + row = rows["src/models.User.recordLogin"] + assert row.signature == "src/models.User.recordLogin" + assert row.name == "recordLogin" + assert row.owner_signature == "src/models.User" + assert row.owner_kind == "class" + assert row.kind == "method" + assert row.start_line == 52 + assert row.end_line == 55 + assert row.decorators == [] + assert row.is_exported is False + assert row.is_async is True + assert row.is_static is False + assert row.accessibility is None + + +def test_overview_interface_method_owner_kind_is_interface(ts_analysis): + rows = by_signature(ts_analysis.get_callables_overview()) + row = rows["src/models.Named.describe"] + assert row.owner_signature == "src/models.Named" + assert row.owner_kind == "interface" + + +def test_overview_arrow_row_has_no_owner_and_native_kind(ts_analysis): + rows = by_signature(ts_analysis.get_callables_overview()) + row = rows["src/services.nextId"] + assert row.owner_signature is None + assert row.owner_kind is None + assert row.kind == "arrow" + + +def test_overview_namespace_owned_function_is_ownerless_with_dotted_signature(ts_analysis): + """RULING: namespace-owned functions (``TSNamespace.functions``) are enumerated but carry no + owner pair -- unlike a namespace-owned *class*'s methods, which do get an owner (the class).""" + rows = by_signature(ts_analysis.get_callables_overview()) + row = rows["src/util.StringUtil.slug"] + assert row.signature == "src/util.StringUtil.slug" + assert row.owner_signature is None + assert row.owner_kind is None + # A namespace-owned *class*'s method, by contrast, does have an owner. + builder_add = rows["src/util.StringUtil.Builder.add"] + assert builder_add.owner_signature == "src/util.StringUtil.Builder" + assert builder_add.owner_kind == "class" + + +# -----[ get_method_bodies ]----- + + +def test_method_bodies_mixes_real_interface_stub_and_unknown(ts_analysis): + bodies = ts_analysis.get_method_bodies( + [ + "src/services.UserService.create", + "src/models.Named.describe", + "src/does/not.exist", + ] + ) + assert set(bodies) == {"src/services.UserService.create", "src/models.Named.describe"} + assert bodies["src/models.Named.describe"] == "describe(): string;" + assert "this.parts.push" not in bodies["src/models.Named.describe"] + + +def test_method_bodies_empty_for_no_matches(ts_analysis): + assert ts_analysis.get_method_bodies(["nope"]) == {} + + +# -----[ get_decorated_callables ]----- + + +def test_decorated_callables_exact_set(ts_analysis): + decorated = ts_analysis.get_decorated_callables(["Get"]) + signatures = {o.signature for o in decorated} + assert signatures == {"src/controllers.UserController.show", "src/controllers.UserController.list"} + assert all(isinstance(o, TSCallableOverview) for o in decorated) + + +def test_decorated_callables_no_match_is_empty(ts_analysis): + assert ts_analysis.get_decorated_callables(["NoSuchDecorator"]) == [] + + +# -----[ get_callsites_for ]----- + + +def test_callsites_for_exact_per_signature_lists_and_empty_entry(ts_analysis): + result = ts_analysis.get_callsites_for( + [ + "src/services.UserService.create", + "src/models.Entity.constructor", + "src/does/not.exist", + ] + ) + assert set(result) == {"src/services.UserService.create", "src/models.Entity.constructor"} + # existing-but-callsite-less callable gets an empty list, not omitted + assert result["src/models.Entity.constructor"] == [] + create_targets = {cs.callee_signature or cs.method_name for cs in result["src/services.UserService.create"]} + assert create_targets == {"src/services.nextId", "src/models.User.constructor", "push"} + + +# -----[ facade delegates to the same backend objects ]----- + + +def test_facade_delegates_return_backend_objects(ts_analysis): + assert ts_analysis.get_callables_overview() == ts_analysis.backend.get_callables_overview() + assert ts_analysis.get_method_bodies(["src/services.UserService.create"]) == ts_analysis.backend.get_method_bodies( + ["src/services.UserService.create"] + ) + assert ts_analysis.get_decorated_callables(["Get"]) == ts_analysis.backend.get_decorated_callables(["Get"]) + assert ts_analysis.get_callsites_for(["src/services.UserService.create"]) == ts_analysis.backend.get_callsites_for( + ["src/services.UserService.create"] + ) From 38010432384fbf01d74b1bef238b1f8e53e35ffd Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Mon, 27 Jul 2026 15:00:54 -0400 Subject: [PATCH 04/10] feat(typescript): bulk accessors on the Neo4j backend (#298) --- .../typescript/neo4j/neo4j_backend.py | 64 ++++ cldk/analysis/typescript/neo4j/reconstruct.py | 40 +++ .../typescript/test_typescript_neo4j_bulk.py | 313 ++++++++++++++++++ 3 files changed, 417 insertions(+) create mode 100644 tests/analysis/typescript/test_typescript_neo4j_bulk.py diff --git a/cldk/analysis/typescript/neo4j/neo4j_backend.py b/cldk/analysis/typescript/neo4j/neo4j_backend.py index 3025e1ca..02891c7d 100644 --- a/cldk/analysis/typescript/neo4j/neo4j_backend.py +++ b/cldk/analysis/typescript/neo4j/neo4j_backend.py @@ -63,6 +63,7 @@ from cldk.models.typescript import ( TSApplication, TSCallable, + TSCallableOverview, TSCallEdge, TSCallsite, TSClass, @@ -710,3 +711,66 @@ def get_classes_with_decorators(self, decorators: List[str]) -> Dict[str, List[s for r in rows: result[r["dn"]].append(r["sig"]) return result + + # -----[ bulk / projected accessors ]----- + # Field-projected RETURNs that sidestep the per-entity reconstruction fan-out: each is a single + # Cypher statement (one round trip), not the child-fetch walk _callable_full pays. + # + # Owner leg: (o:Symbol)-[:HAS_METHOD]->(c) only ever connects a Class/Interface owner to one of + # its methods, so it naturally has no match for module-level, namespace-owned, or nested + # callables -- they fall out owner-less (None/None) with no separate namespace leg needed. + _OVERVIEW_RETURN = ( + "OPTIONAL MATCH (o:Symbol)-[:HAS_METHOD]->(c) " + "OPTIONAL MATCH (c)-[:DECORATED_BY]->(d:Decorator) " + "RETURN c.signature AS signature, c.name AS name, c.kind AS kind, c.path AS path, " + "c.start_line AS start_line, c.end_line AS end_line, " + "c.is_exported AS is_exported, c.is_async AS is_async, c.is_static AS is_static, " + "c.accessibility AS accessibility, " + "o.signature AS owner_signature, labels(o) AS owner_labels, " + "collect(DISTINCT d.name) AS decorators" + ) + + def get_callables_overview(self) -> List[TSCallableOverview]: + rows = self._run( + "MATCH (c:Callable) WHERE c._module IN $mods " + self._OVERVIEW_RETURN, + mods=self._modules, + ) + return [R.overview(r) for r in rows] + + def get_method_bodies(self, signatures: List[str]) -> Dict[str, str]: + rows = self._run( + "MATCH (c:Callable) WHERE c._module IN $mods AND c.signature IN $sigs AND c.code IS NOT NULL " + "RETURN c.signature AS signature, c.code AS code", + mods=self._modules, + sigs=list(signatures), + ) + return {r["signature"]: r["code"] for r in rows} + + def get_decorated_callables(self, markers: List[str]) -> List[TSCallableOverview]: + rows = self._run( + "MATCH (c:Callable)-[:DECORATED_BY]->(marker:Decorator) " + "WHERE c._module IN $mods AND marker.name IN $markers " + "WITH DISTINCT c " + self._OVERVIEW_RETURN, + mods=self._modules, + markers=list(markers), + ) + return [R.overview(r) for r in rows] + + def get_callsites_for(self, signatures: List[str]) -> Dict[str, List[TSCallsite]]: + # OPTIONAL MATCH so a requested callable with no call sites still yields a row (p is null), + # giving it an empty-list entry -- parity with the in-process backend, which keys every + # existing signature. ORDER mirrors _callsites_of's call-site ordering. + rows = self._run( + "MATCH (c:Callable) WHERE c._module IN $mods AND c.signature IN $sigs " + "OPTIONAL MATCH (c)-[:HAS_CALLSITE]->(cs:CallSite) " + "RETURN c.signature AS owner, properties(cs) AS p " + "ORDER BY cs.start_line, cs.start_column", + mods=self._modules, + sigs=list(signatures), + ) + out: Dict[str, List[TSCallsite]] = {} + for r in rows: + sites = out.setdefault(r["owner"], []) + if r["p"] is not None: + sites.append(R.callsite(r["p"])) + return out diff --git a/cldk/analysis/typescript/neo4j/reconstruct.py b/cldk/analysis/typescript/neo4j/reconstruct.py index 8c687652..f815ca95 100644 --- a/cldk/analysis/typescript/neo4j/reconstruct.py +++ b/cldk/analysis/typescript/neo4j/reconstruct.py @@ -40,6 +40,7 @@ from cldk.models.typescript import ( TSCallable, + TSCallableOverview, TSCallableParameter, TSCallsite, TSClass, @@ -191,6 +192,45 @@ def synthesized(props: Props) -> TSSynthesizedCallable: ) +def overview(row: Props) -> TSCallableOverview: + """Build a :class:`TSCallableOverview` from a projected callable row (a flat ``RETURN`` + projection, not a node's ``properties()``): ``signature``/``name``/``kind``/``path``/ + ``start_line``/``end_line``/``is_exported``/``is_async``/``is_static``/``accessibility`` plus + ``owner_signature``/``owner_labels`` (the ``HAS_METHOD`` owner leg — absent, i.e. both null, + for module-level/namespace-owned/nested callables) and ``decorators`` (collected decorator + names). + + ``owner_kind`` is derived from ``owner_labels`` rather than stored directly: ``"class"`` if the + owner node carries the ``Class`` label, ``"interface"`` if it carries ``Interface``, else + ``None`` — matching the closed two-value ``owner_kind`` set the in-memory backend produces. + """ + owner_signature = row.get("owner_signature") + owner_labels = row.get("owner_labels") or [] + if owner_signature is None: + owner_kind = None + elif "Class" in owner_labels: + owner_kind = "class" + elif "Interface" in owner_labels: + owner_kind = "interface" + else: + owner_kind = None + return TSCallableOverview( + signature=row.get("signature", ""), + name=row.get("name", ""), + owner_signature=owner_signature, + owner_kind=owner_kind, + kind=row.get("kind", "function"), + path=row.get("path", ""), + start_line=row.get("start_line", -1), + end_line=row.get("end_line", -1), + decorators=[d for d in (row.get("decorators") or []) if d is not None], + is_exported=bool(row.get("is_exported", False)), + is_async=bool(row.get("is_async", False)), + is_static=bool(row.get("is_static", False)), + accessibility=row.get("accessibility"), + ) + + # ---------------------------------------------------------------------------------------------- # declaration nodes (children supplied by the backend) # ---------------------------------------------------------------------------------------------- diff --git a/tests/analysis/typescript/test_typescript_neo4j_bulk.py b/tests/analysis/typescript/test_typescript_neo4j_bulk.py new file mode 100644 index 00000000..c9cf2342 --- /dev/null +++ b/tests/analysis/typescript/test_typescript_neo4j_bulk.py @@ -0,0 +1,313 @@ +################################################################################ +# Copyright IBM Corporation 2026 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +################################################################################ + +"""Stub tests for the four bulk/projected accessors (#298) on :class:`TSNeo4jBackend`: +``get_callables_overview`` / ``get_method_bodies`` / ``get_decorated_callables`` / +``get_callsites_for``. + +``_run`` is the single seam every query method goes through, so it is stubbed here with canned +rows keyed on a distinguishing fragment of the query text -- no live Neo4j needed (mirrors the +house pattern in ``test_typescript_get_method_functions.py`` / ``test_typescript_external_reconstruct.py``). +Semantics must match Task 2's in-memory impls (``TSCodeanalyzer._iter_callables`` et al.) exactly: +owner pair derived from the owner node's labels (Class/Interface), namespace/nested callables get +no owner leg match at all (None/None falls out naturally), null-code bodies are omitted, and every +requested-and-existing signature gets a callsites entry (empty list if it has none). +""" + +from unittest.mock import patch + +from cldk.analysis.typescript.neo4j.neo4j_backend import TSNeo4jBackend +from cldk.models.typescript import TSCallableOverview, TSCallsite + + +def _backend(modules=("app.ts",)) -> TSNeo4jBackend: + """A TSNeo4jBackend with __init__ (and its real driver connection) bypassed.""" + backend = object.__new__(TSNeo4jBackend) + backend.application_name = "test-app" + backend._database = None + backend._modules = list(modules) + return backend + + +def _run_keyed(rows_by_fragment: dict): + """A stub ``_run`` returning the canned rows for the first query fragment found in the text.""" + + def _run(query: str, **params): + for fragment, result in rows_by_fragment.items(): + if fragment in query: + return result + raise AssertionError(f"no canned rows for query: {query!r} (params={params})") + + return _run + + +# -----[ get_callables_overview ]----- + + +def test_overview_builds_row_with_class_owner_from_labels(): + row = { + "signature": "src/models.User.recordLogin", + "name": "recordLogin", + "kind": "method", + "path": "src/models.ts", + "start_line": 52, + "end_line": 55, + "is_exported": False, + "is_async": True, + "is_static": False, + "accessibility": None, + "owner_signature": "src/models.User", + "owner_labels": ["Symbol", "Class"], + "decorators": [], + } + backend = _backend() + with patch.object(TSNeo4jBackend, "_run", side_effect=_run_keyed({"MATCH (c:Callable) WHERE c._module IN $mods ": [row]})): + overview = backend.get_callables_overview() + assert len(overview) == 1 + assert isinstance(overview[0], TSCallableOverview) + o = overview[0] + assert o.signature == "src/models.User.recordLogin" + assert o.owner_signature == "src/models.User" + assert o.owner_kind == "class" + assert o.kind == "method" + assert o.is_async is True + assert o.is_exported is False + + +def test_overview_builds_row_with_interface_owner_from_labels(): + row = { + "signature": "src/models.Named.describe", + "name": "describe", + "kind": "method", + "path": "src/models.ts", + "start_line": 1, + "end_line": 1, + "is_exported": False, + "is_async": False, + "is_static": False, + "accessibility": None, + "owner_signature": "src/models.Named", + "owner_labels": ["Symbol", "Interface"], + "decorators": [], + } + backend = _backend() + with patch.object(TSNeo4jBackend, "_run", side_effect=_run_keyed({"MATCH (c:Callable) WHERE c._module IN $mods ": [row]})): + overview = backend.get_callables_overview() + assert overview[0].owner_kind == "interface" + assert overview[0].owner_signature == "src/models.Named" + + +def test_overview_namespace_or_module_owned_function_has_no_owner_leg_match(): + """RULING: namespace-owned (and module-level / nested) functions never match the HAS_METHOD + owner leg at all -- None/None falls straight out of the row; there is no separate namespace + owner leg to add.""" + row = { + "signature": "src/util.StringUtil.slug", + "name": "slug", + "kind": "function", + "path": "src/util.ts", + "start_line": 10, + "end_line": 12, + "is_exported": False, + "is_async": False, + "is_static": False, + "accessibility": None, + "owner_signature": None, + "owner_labels": None, + "decorators": [], + } + backend = _backend() + with patch.object(TSNeo4jBackend, "_run", side_effect=_run_keyed({"MATCH (c:Callable) WHERE c._module IN $mods ": [row]})): + overview = backend.get_callables_overview() + assert overview[0].owner_signature is None + assert overview[0].owner_kind is None + + +def test_overview_collects_decorator_names(): + row = { + "signature": "src/controllers.UserController.show", + "name": "show", + "kind": "method", + "path": "src/controllers.ts", + "start_line": 1, + "end_line": 1, + "is_exported": False, + "is_async": False, + "is_static": False, + "accessibility": None, + "owner_signature": "src/controllers.UserController", + "owner_labels": ["Symbol", "Class"], + "decorators": ["Get"], + } + backend = _backend() + with patch.object(TSNeo4jBackend, "_run", side_effect=_run_keyed({"MATCH (c:Callable) WHERE c._module IN $mods ": [row]})): + overview = backend.get_callables_overview() + assert overview[0].decorators == ["Get"] + + +def test_overview_scopes_query_to_this_backends_modules(): + captured = {} + + def _run(query, **params): + captured["mods"] = params.get("mods") + return [] + + backend = _backend(modules=["a.ts", "b.ts"]) + with patch.object(TSNeo4jBackend, "_run", side_effect=_run): + assert backend.get_callables_overview() == [] + assert captured["mods"] == ["a.ts", "b.ts"] + + +# -----[ get_method_bodies ]----- + + +def test_method_bodies_keyed_by_signature_unknowns_omitted(): + rows = [ + {"signature": "src/services.UserService.create", "code": "create() { ... }"}, + {"signature": "src/models.Named.describe", "code": "describe(): string;"}, + ] + backend = _backend() + with patch.object(TSNeo4jBackend, "_run", side_effect=_run_keyed({"c.code IS NOT NULL": rows})): + bodies = backend.get_method_bodies( + [ + "src/services.UserService.create", + "src/models.Named.describe", + "src/does/not.exist", + ] + ) + assert bodies == { + "src/services.UserService.create": "create() { ... }", + "src/models.Named.describe": "describe(): string;", + } + + +def test_method_bodies_empty_for_no_matches(): + backend = _backend() + with patch.object(TSNeo4jBackend, "_run", side_effect=_run_keyed({"c.code IS NOT NULL": []})): + assert backend.get_method_bodies(["nope"]) == {} + + +def test_method_bodies_query_filters_null_code_and_scopes_sigs(): + captured = {} + + def _run(query, **params): + captured["query"] = query + captured["sigs"] = params.get("sigs") + return [] + + backend = _backend() + with patch.object(TSNeo4jBackend, "_run", side_effect=_run): + backend.get_method_bodies(["sig-a", "sig-b"]) + assert "c.code IS NOT NULL" in captured["query"] + assert "c.signature IN $sigs" in captured["query"] + assert captured["sigs"] == ["sig-a", "sig-b"] + + +# -----[ get_decorated_callables ]----- + + +def test_decorated_callables_matches_marker_and_returns_overview(): + row = { + "signature": "src/controllers.UserController.show", + "name": "show", + "kind": "method", + "path": "src/controllers.ts", + "start_line": 1, + "end_line": 1, + "is_exported": False, + "is_async": False, + "is_static": False, + "accessibility": None, + "owner_signature": "src/controllers.UserController", + "owner_labels": ["Symbol", "Class"], + "decorators": ["Get"], + } + backend = _backend() + with patch.object(TSNeo4jBackend, "_run", side_effect=_run_keyed({"DECORATED_BY]->(marker:Decorator)": [row]})): + decorated = backend.get_decorated_callables(["Get"]) + assert len(decorated) == 1 + assert isinstance(decorated[0], TSCallableOverview) + assert decorated[0].signature == "src/controllers.UserController.show" + assert decorated[0].owner_kind == "class" + assert decorated[0].decorators == ["Get"] + + +def test_decorated_callables_no_match_is_empty(): + backend = _backend() + with patch.object(TSNeo4jBackend, "_run", side_effect=_run_keyed({"DECORATED_BY]->(marker:Decorator)": []})): + assert backend.get_decorated_callables(["NoSuchDecorator"]) == [] + + +def test_decorated_callables_passes_markers_param(): + captured = {} + + def _run(query, **params): + captured["markers"] = params.get("markers") + return [] + + backend = _backend() + with patch.object(TSNeo4jBackend, "_run", side_effect=_run): + backend.get_decorated_callables(["Get", "Post"]) + assert captured["markers"] == ["Get", "Post"] + + +# -----[ get_callsites_for ]----- + + +def test_callsites_for_groups_by_owner_and_keeps_empty_entry(): + rows = [ + { + "owner": "src/services.UserService.create", + "p": {"method_name": "nextId", "callee_signature": "src/services.nextId", "start_line": 1, "start_column": 1}, + }, + { + "owner": "src/services.UserService.create", + "p": {"method_name": "push", "callee_signature": None, "start_line": 2, "start_column": 1}, + }, + {"owner": "src/models.Entity.constructor", "p": None}, + ] + backend = _backend() + with patch.object(TSNeo4jBackend, "_run", side_effect=_run_keyed({"HAS_CALLSITE": rows})): + result = backend.get_callsites_for( + [ + "src/services.UserService.create", + "src/models.Entity.constructor", + "src/does/not.exist", + ] + ) + assert set(result) == {"src/services.UserService.create", "src/models.Entity.constructor"} + # existing-but-callsite-less callable gets an empty list, not omitted + assert result["src/models.Entity.constructor"] == [] + assert all(isinstance(cs, TSCallsite) for cs in result["src/services.UserService.create"]) + create_targets = {cs.callee_signature or cs.method_name for cs in result["src/services.UserService.create"]} + assert create_targets == {"src/services.nextId", "push"} + + +def test_callsites_for_scopes_sigs_and_mods(): + captured = {} + + def _run(query, **params): + captured["query"] = query + captured["sigs"] = params.get("sigs") + captured["mods"] = params.get("mods") + return [] + + backend = _backend(modules=["a.ts"]) + with patch.object(TSNeo4jBackend, "_run", side_effect=_run): + assert backend.get_callsites_for(["sig-a"]) == {} + assert "HAS_CALLSITE" in captured["query"] + assert captured["sigs"] == ["sig-a"] + assert captured["mods"] == ["a.ts"] From 6446c87bda4cb2675aec035d4768d1c260a4c5c9 Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Mon, 27 Jul 2026 15:03:50 -0400 Subject: [PATCH 05/10] fix(typescript): get_method_bodies omits code-less callables (#298) --- cldk/analysis/typescript/backend.py | 4 +++- .../typescript/codeanalyzer/codeanalyzer.py | 7 +++++-- cldk/analysis/typescript/typescript_analysis.py | 3 ++- .../typescript/test_typescript_bulk_accessors.py | 16 ++++++++++++++++ 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/cldk/analysis/typescript/backend.py b/cldk/analysis/typescript/backend.py index d165493c..080ef089 100644 --- a/cldk/analysis/typescript/backend.py +++ b/cldk/analysis/typescript/backend.py @@ -259,7 +259,9 @@ def get_callables_overview(self) -> List[TSCallableOverview]: @abstractmethod def get_method_bodies(self, signatures: List[str]) -> Dict[str, str]: """Source bodies for the given callable signatures, keyed by signature. Signatures with no - matching callable are omitted.""" + matching callable are omitted, as are callables whose ``code`` is ``None`` (e.g. implicit + constructors the analyzer synthesizes with no source text) — every returned value is a + real ``str``.""" @abstractmethod def get_decorated_callables(self, markers: List[str]) -> List[TSCallableOverview]: diff --git a/cldk/analysis/typescript/codeanalyzer/codeanalyzer.py b/cldk/analysis/typescript/codeanalyzer/codeanalyzer.py index ed39f1da..d534d303 100644 --- a/cldk/analysis/typescript/codeanalyzer/codeanalyzer.py +++ b/cldk/analysis/typescript/codeanalyzer/codeanalyzer.py @@ -583,9 +583,12 @@ def get_callables_overview(self) -> List[TSCallableOverview]: return [TSCallableOverview.from_callable(c, owner_sig, owner_kind) for c, owner_sig, owner_kind in self._iter_callables()] def get_method_bodies(self, signatures: List[str]) -> Dict[str, str]: - """Return ``{signature: code}`` for the requested signatures that exist.""" + """Return ``{signature: code}`` for the requested signatures that exist and have a body + (omits callables whose ``code`` is ``None``, e.g. implicit constructors).""" wanted = set(signatures) - return {c.signature: c.code for c, _, _ in self._iter_callables() if c.signature in wanted} + return { + c.signature: c.code for c, _, _ in self._iter_callables() if c.signature in wanted and c.code is not None + } def get_decorated_callables(self, markers: List[str]) -> List[TSCallableOverview]: """Return overviews of callables decorated with any of ``markers``.""" diff --git a/cldk/analysis/typescript/typescript_analysis.py b/cldk/analysis/typescript/typescript_analysis.py index 68b14232..a49357d0 100644 --- a/cldk/analysis/typescript/typescript_analysis.py +++ b/cldk/analysis/typescript/typescript_analysis.py @@ -330,7 +330,8 @@ def get_method_bodies(self, signatures: List[str]) -> Dict[str, str]: Returns: A dict mapping each signature to its source body. Signatures with no matching callable - are omitted. + are omitted, as are callables whose ``code`` is ``None`` (e.g. implicit constructors + the analyzer synthesizes with no source text) — every returned value is a real ``str``. """ return self.backend.get_method_bodies(signatures) diff --git a/tests/analysis/typescript/test_typescript_bulk_accessors.py b/tests/analysis/typescript/test_typescript_bulk_accessors.py index 4bd2e806..5c151772 100644 --- a/tests/analysis/typescript/test_typescript_bulk_accessors.py +++ b/tests/analysis/typescript/test_typescript_bulk_accessors.py @@ -174,6 +174,22 @@ def test_method_bodies_empty_for_no_matches(ts_analysis): assert ts_analysis.get_method_bodies(["nope"]) == {} +def test_method_bodies_omits_code_less_callables(ts_analysis): + """The implicit ``Builder`` constructor exists (it's a real callable in the symbol table) but + the analyzer never synthesized source text for it -- ``code`` is ``None``. It must be omitted + from the result, not surfaced as ``{sig: None}``, so every returned value is a real ``str``.""" + bodies = ts_analysis.get_method_bodies( + [ + "src/services.UserService.create", + "src/util.StringUtil.Builder.constructor", + "src/does/not.exist", + ] + ) + assert set(bodies) == {"src/services.UserService.create"} + assert "src/util.StringUtil.Builder.constructor" not in bodies + assert all(isinstance(v, str) for v in bodies.values()) + + # -----[ get_decorated_callables ]----- From cd0516d6376921f09c0cf0ed8c82c7e24ca7ed9f Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Mon, 27 Jul 2026 15:07:01 -0400 Subject: [PATCH 06/10] test(typescript): pin overview Cypher shape in Neo4j bulk stub tests (#298) --- .../typescript/test_typescript_neo4j_bulk.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/analysis/typescript/test_typescript_neo4j_bulk.py b/tests/analysis/typescript/test_typescript_neo4j_bulk.py index c9cf2342..40c6003c 100644 --- a/tests/analysis/typescript/test_typescript_neo4j_bulk.py +++ b/tests/analysis/typescript/test_typescript_neo4j_bulk.py @@ -171,6 +171,26 @@ def _run(query, **params): assert captured["mods"] == ["a.ts", "b.ts"] +def test_overview_query_shape_has_owner_leg_and_module_scoping(): + """Pins the Cypher shape itself: a `labels(c)`-for-`labels(o)` typo, or a silently dropped + HAS_METHOD/DECORATED_BY OPTIONAL MATCH leg, would still pass the row-construction tests above + (they hand `owner_labels` straight to the reconstructor) -- only asserting on the actual query + text catches that class of bug.""" + captured = {} + + def _run(query, **params): + captured["query"] = query + return [] + + backend = _backend() + with patch.object(TSNeo4jBackend, "_run", side_effect=_run): + assert backend.get_callables_overview() == [] + assert "c._module IN $mods" in captured["query"] + assert "HAS_METHOD" in captured["query"] + assert "labels(o)" in captured["query"] + assert "DECORATED_BY" in captured["query"] + + # -----[ get_method_bodies ]----- @@ -264,6 +284,28 @@ def _run(query, **params): assert captured["markers"] == ["Get", "Post"] +def test_decorated_callables_query_shape_has_marker_leg_owner_leg_and_module_scoping(): + """Same pinning concern as the overview query-shape test above: the marker-match leg + (`DECORATED_BY]->(marker:Decorator)` + `marker.name IN $markers`) and the reused overview + projection (owner leg via `labels(o)`, the separate `d:Decorator` collection leg) must both + actually be in the Cypher, not just implied by the canned rows.""" + captured = {} + + def _run(query, **params): + captured["query"] = query + return [] + + backend = _backend() + with patch.object(TSNeo4jBackend, "_run", side_effect=_run): + assert backend.get_decorated_callables(["Get"]) == [] + assert "c._module IN $mods" in captured["query"] + assert "DECORATED_BY]->(marker:Decorator)" in captured["query"] + assert "marker.name IN $markers" in captured["query"] + assert "HAS_METHOD" in captured["query"] + assert "labels(o)" in captured["query"] + assert "DECORATED_BY]->(d:Decorator)" in captured["query"] + + # -----[ get_callsites_for ]----- From 5046c9007ba20dcd84c4f783719a278b57dfa148 Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Mon, 27 Jul 2026 15:18:13 -0400 Subject: [PATCH 07/10] test(typescript): dual-backend parity for the bulk accessors (#298) --- .../test_typescript_bulk_parity_live.py | 199 ++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 tests/analysis/typescript/test_typescript_bulk_parity_live.py diff --git a/tests/analysis/typescript/test_typescript_bulk_parity_live.py b/tests/analysis/typescript/test_typescript_bulk_parity_live.py new file mode 100644 index 00000000..066e5a32 --- /dev/null +++ b/tests/analysis/typescript/test_typescript_bulk_parity_live.py @@ -0,0 +1,199 @@ +################################################################################ +# Copyright IBM Corporation 2026 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +################################################################################ + +"""Live dual-backend parity for the four bulk/projected accessors (#298): +``get_callables_overview`` / ``get_method_bodies`` / ``get_decorated_callables`` / +``get_callsites_for``. + +This is the acceptance bar the spec names for this feature: the in-memory backend +(:class:`TSCodeanalyzer`) and the read-only Neo4j backend (:class:`TSNeo4jBackend`) must answer +these four queries identically over the *same* tracked sample app +(``tests/resources/typescript/application``) — never the slim ``analysis.json`` fixture used by +the rest of this package's (mocked-subprocess) tests, so the emit and the in-memory reference +describe the exact same code. + +Reuses the live harness idiom of ``test_typescript_neo4j_backend.py`` verbatim: same env-var +gating, same ``_populate_neo4j`` out-of-band loader (``codeanalyzer-typescript --emit neo4j`` over +Bolt), same tracked sample app / app name. The whole module is skipped unless a Neo4j server is +reachable. Point the tests at one with: + + CLDK_TEST_NEO4J_URI=bolt://localhost:7687 \ + CLDK_TEST_NEO4J_USER=neo4j \ + CLDK_TEST_NEO4J_PASSWORD=test \ + pytest tests/analysis/typescript/test_typescript_bulk_parity_live.py + +(e.g. `podman run -d -p 7687:7687 -e NEO4J_AUTH=neo4j/test neo4j:5`). +""" + +import logging + +import pytest + +from cldk import CLDK +from cldk.analysis import AnalysisLevel +from cldk.analysis.commons.backend_config import CodeAnalyzerConfig, Neo4jConnectionConfig + +from .test_typescript_neo4j_backend import ( + APP_NAME, + NEO4J_PASSWORD, + NEO4J_URI, + NEO4J_USER, + _neo4j_reachable, + _populate_neo4j, +) + +logging.getLogger("neo4j").setLevel(logging.ERROR) + +pytestmark = pytest.mark.skipif( + not _neo4j_reachable(), + reason=f"no Neo4j reachable at {NEO4J_URI} (set CLDK_TEST_NEO4J_URI / _USER / _PASSWORD)", +) + + +def _overview_tuple(o): + """A hashable, order-independent projection of one ``TSCallableOverview`` row. + + ``decorators`` is compared as a *sorted* tuple, not as-is: the Neo4j side collects decorator + names with ``collect(DISTINCT d.name)``, which carries no row-order guarantee, while the + in-memory side preserves declaration order — so decorator order is deliberately not part of + the parity contract, only the set of names is. Every other field is a plain scalar, so the + remaining tuple positions already compare exactly. + """ + return ( + o.signature, + o.name, + o.owner_signature, + o.owner_kind, + o.kind, + o.path, + o.start_line, + o.end_line, + tuple(sorted(o.decorators)), + o.is_exported, + o.is_async, + o.is_static, + o.accessibility, + ) + + +@pytest.fixture(scope="module") +def ts_dual(typescript_application, tmp_path_factory): + """``(ref, neo)``: the in-memory backend and a Neo4j backend, both over the SAME tracked + sample app (``tests/resources/typescript/application``) — the emit and the in-memory reference + must describe identical code, so neither side may fall back to the slim fixture JSON. + """ + _populate_neo4j(typescript_application) + + cache_dir = tmp_path_factory.mktemp("ts_bulk_parity_cache") + ref = CLDK.typescript( + project_path=typescript_application, + eager=True, + analysis_level=AnalysisLevel.call_graph, + backend=CodeAnalyzerConfig(cache_dir=str(cache_dir)), + ) + + neo = CLDK.typescript( + project_path=typescript_application, + analysis_level=AnalysisLevel.call_graph, + backend=Neo4jConnectionConfig( + uri=NEO4J_URI, + username=NEO4J_USER, + password=NEO4J_PASSWORD, + application_name=APP_NAME, + ), + ) + yield ref, neo + neo.backend.close() + + +def test_callables_overview_parity(ts_dual): + ref, neo = ts_dual + ref_rows = {_overview_tuple(o) for o in ref.get_callables_overview()} + neo_rows = {_overview_tuple(o) for o in neo.get_callables_overview()} + assert ref_rows, "sample app should have at least one callable" + assert ref_rows == neo_rows + + +def test_method_bodies_parity(ts_dual): + ref, neo = ts_dual + sigs = [o.signature for o in ref.get_callables_overview()] + assert ref.get_method_bodies(sigs) == neo.get_method_bodies(sigs) + # unknown signatures are omitted identically on both backends + assert ref.get_method_bodies(["nope.not.here"]) == neo.get_method_bodies(["nope.not.here"]) == {} + + +def test_decorated_callables_parity(ts_dual): + ref, neo = ts_dual + markers = sorted({d for o in ref.get_callables_overview() for d in o.decorators}) + assert markers, "sample app fixture should carry at least one decorator (e.g. Controller/Get)" + + ref_rows = {_overview_tuple(o) for o in ref.get_decorated_callables(markers)} + neo_rows = {_overview_tuple(o) for o in neo.get_decorated_callables(markers)} + assert ref_rows, "at least one callable should match the markers actually in use" + assert ref_rows == neo_rows + + # a marker nothing carries yields an identical empty result on both backends + assert ref.get_decorated_callables(["__no_such_decorator__"]) == neo.get_decorated_callables(["__no_such_decorator__"]) == [] + + +def _callsite_tuple(cs): + """A hashable, fully-fielded projection of one ``TSCallsite`` -- used to compare call-site + lists content-for-content, order-independent (see the comment on ``test_callsites_parity`` + for why order is deliberately not part of this comparison). + """ + return ( + cs.method_name, + cs.receiver_expr, + cs.receiver_type, + tuple(cs.argument_types), + tuple(cs.type_arguments), + cs.return_type, + cs.callee_signature, + cs.is_constructor_call, + cs.is_optional_chain, + cs.start_line, + cs.start_column, + cs.end_line, + cs.end_column, + ) + + +def test_callsites_parity(ts_dual): + ref, neo = ts_dual + sigs = [o.signature for o in ref.get_callables_overview()] + cs_ref = ref.get_callsites_for(sigs) + cs_neo = neo.get_callsites_for(sigs) + assert set(cs_ref) == set(cs_neo) + for sig in cs_ref: + # Content equality as a multiset, NOT list-order equality. `TSAnalysisBackend.get_callsites_for` + # (backend.py) never contracts an order beyond "each existing signature gets an entry" -- + # and a live run surfaced a real case where order genuinely differs: for a receiver chain + # (sample app's `builder.add("a").add("b").build()`), the outer call's span *starts* at the + # same (start_line, start_column) as its own receiver sub-expression's call, so + # TSNeo4jBackend's `ORDER BY cs.start_line, cs.start_column` cannot disambiguate them and + # returns the tied pair in the opposite relative order from the in-memory backend (which + # preserves the analyzer's own analysis.json array order). No CallSite property (graph or + # JSON) carries a stable ordinal to reconstruct the "true" order for such ties, so recovering + # it would need an upstream codeanalyzer-typescript emitter change (out of scope here) -- + # matching the existing precedent in test_typescript_neo4j_bulk.py + # (test_callsites_for_groups_by_owner_and_keeps_empty_entry), which also only ever asserts + # callsite *sets*, never list order. + ref_multiset = [_callsite_tuple(c) for c in cs_ref[sig]] + neo_multiset = [_callsite_tuple(c) for c in cs_neo[sig]] + assert sorted(ref_multiset, key=str) == sorted(neo_multiset, key=str), f"call sites for {sig} differ" + + # unknown signatures are omitted identically on both backends + assert ref.get_callsites_for(["nope.not.here"]) == neo.get_callsites_for(["nope.not.here"]) == {} From 81f307b2ea932ef2718103415ec1cba3804ea977 Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Mon, 27 Jul 2026 15:33:00 -0400 Subject: [PATCH 08/10] test(typescript): track the blessed slim analysis fixture (#298) tests/resources/typescript/analysis_json/slim/analysis.json was caught by the blanket *.json gitignore rule and never committed, even though the TS bulk accessor tests assert exact-set constants against it and it was hand-built from a src/external.ts that was itself never committed -- so a fresh clone could neither run the suite nor regenerate the fixture. Add a narrow .gitignore exception and track the file. --- .gitignore | 6 + .../analysis_json/slim/analysis.json | 3769 +++++++++++++++++ 2 files changed, 3775 insertions(+) create mode 100644 tests/resources/typescript/analysis_json/slim/analysis.json diff --git a/.gitignore b/.gitignore index b7fe8d82..9214263b 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,12 @@ scratch* *.json !devcontainer.json +# Blessed TS unit-test fixture: hand-built from a sample app whose source (incl. src/external.ts) +# was never committed, so it cannot be regenerated by running codeanalyzer-typescript again. The +# bulk-accessor tests assert exact-set constants (signature counts, ownerless sets) against this +# exact file -- losing it breaks the suite for every fresh clone (#298). +!tests/resources/typescript/analysis_json/slim/analysis.json + # Python compiled files and env __pycache__/ diff --git a/tests/resources/typescript/analysis_json/slim/analysis.json b/tests/resources/typescript/analysis_json/slim/analysis.json new file mode 100644 index 00000000..7e59faae --- /dev/null +++ b/tests/resources/typescript/analysis_json/slim/analysis.json @@ -0,0 +1,3769 @@ +{ + "symbol_table": { + "src/controllers.ts": { + "file_path": "src/controllers.ts", + "module_name": "src/controllers", + "imports": [ + { + "module": "./services", + "name": "UserService", + "alias": null, + "is_type_only": false, + "import_kind": "named", + "start_line": 1, + "end_line": 1, + "start_column": 1, + "end_column": 42 + } + ], + "exports": [], + "comments": [ + { + "content": "// Minimal decorator factories (NestJS/Angular-flavored) to exercise structured TSDecorator capture.", + "is_docstring": false, + "start_line": 3, + "end_line": 3, + "start_column": 1, + "end_column": 101 + } + ], + "classes": { + "src/controllers.UserController": { + "name": "UserController", + "signature": "src/controllers.UserController", + "comments": [], + "code": "@Controller(\"/users\")\nexport class UserController {\n constructor(private readonly service: UserService) {}\n\n @Get(\"/:id\")\n show(@Param(\"id\") id: string): string {\n const user = this.service.create(id);\n return user.describe();\n }\n\n @Get(\"/\")\n list(): string[] {\n return this.service.describeAll();\n }\n}", + "decorators": [ + { + "name": "Controller", + "qualified_name": "Controller", + "positional_arguments": [ + "\"/users\"" + ], + "keyword_arguments": {}, + "start_line": 14, + "end_line": 14, + "start_column": 1, + "end_column": 22 + } + ], + "base_classes": [], + "implements_types": [], + "type_parameters": [], + "methods": { + "src/controllers.UserController.show": { + "name": "show", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/controllers.ts", + "signature": "src/controllers.UserController.show", + "comments": [], + "decorators": [ + { + "name": "Get", + "qualified_name": "Get", + "positional_arguments": [ + "\"/:id\"" + ], + "keyword_arguments": {}, + "start_line": 18, + "end_line": 18, + "start_column": 3, + "end_column": 15 + } + ], + "parameters": [ + { + "name": "id", + "type": "string", + "default_value": null, + "is_optional": false, + "is_rest": false, + "is_readonly": false, + "accessibility": null, + "decorators": [ + { + "name": "Param", + "qualified_name": "Param", + "positional_arguments": [ + "\"id\"" + ], + "keyword_arguments": {}, + "start_line": 19, + "end_line": 19, + "start_column": 8, + "end_column": 20 + } + ], + "start_line": 19, + "end_line": 19, + "start_column": 8, + "end_column": 31 + } + ], + "type_parameters": [], + "return_type": "string", + "code": "@Get(\"/:id\")\n show(@Param(\"id\") id: string): string {\n const user = this.service.create(id);\n return user.describe();\n }", + "start_line": 18, + "end_line": 22, + "code_start_line": 18, + "accessed_symbols": [], + "call_sites": [ + { + "method_name": "create", + "receiver_expr": "this.service", + "receiver_type": "UserService", + "argument_types": [ + "string" + ], + "type_arguments": [], + "return_type": "import(\"./models\").User", + "callee_signature": "src/services.UserService.create", + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 20, + "end_line": 20, + "start_column": 18, + "end_column": 41 + }, + { + "method_name": "describe", + "receiver_expr": "user", + "receiver_type": "import(\"./models\").User", + "argument_types": [], + "type_arguments": [], + "return_type": "string", + "callee_signature": "src/models.User.describe", + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 21, + "end_line": 21, + "start_column": 12, + "end_column": 27 + } + ], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [ + { + "name": "user", + "type": "import(\"./models\").User", + "initializer": "this.service.create(id)", + "value": null, + "scope": "function", + "declaration_kind": "const", + "is_readonly": true, + "is_exported": false, + "start_line": 20, + "end_line": 20, + "start_column": 11, + "end_column": 41 + } + ], + "cyclomatic_complexity": 1, + "kind": "method", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": false, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + }, + "src/controllers.UserController.list": { + "name": "list", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/controllers.ts", + "signature": "src/controllers.UserController.list", + "comments": [], + "decorators": [ + { + "name": "Get", + "qualified_name": "Get", + "positional_arguments": [ + "\"/\"" + ], + "keyword_arguments": {}, + "start_line": 24, + "end_line": 24, + "start_column": 3, + "end_column": 12 + } + ], + "parameters": [], + "type_parameters": [], + "return_type": "string[]", + "code": "@Get(\"/\")\n list(): string[] {\n return this.service.describeAll();\n }", + "start_line": 24, + "end_line": 27, + "code_start_line": 24, + "accessed_symbols": [], + "call_sites": [ + { + "method_name": "describeAll", + "receiver_expr": "this.service", + "receiver_type": "UserService", + "argument_types": [], + "type_arguments": [], + "return_type": "string[]", + "callee_signature": "src/services.UserService.describeAll", + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 26, + "end_line": 26, + "start_column": 12, + "end_column": 38 + } + ], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [], + "cyclomatic_complexity": 1, + "kind": "method", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": false, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + }, + "src/controllers.UserController.constructor": { + "name": "constructor", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/controllers.ts", + "signature": "src/controllers.UserController.constructor", + "comments": [], + "decorators": [], + "parameters": [ + { + "name": "service", + "type": "UserService", + "default_value": null, + "is_optional": false, + "is_rest": false, + "is_readonly": true, + "accessibility": "private", + "decorators": [], + "start_line": 16, + "end_line": 16, + "start_column": 15, + "end_column": 52 + } + ], + "type_parameters": [], + "return_type": null, + "code": "constructor(private readonly service: UserService) {}", + "start_line": 16, + "end_line": 16, + "code_start_line": 16, + "accessed_symbols": [], + "call_sites": [], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [], + "cyclomatic_complexity": 1, + "kind": "constructor", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": false, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + } + }, + "attributes": { + "service": { + "name": "service", + "type": "UserService", + "comments": [], + "decorators": [], + "initializer": null, + "accessibility": "private", + "is_static": false, + "is_readonly": true, + "is_optional": false, + "is_abstract": false, + "start_line": 16, + "end_line": 16 + } + }, + "inner_classes": {}, + "is_abstract": false, + "is_exported": true, + "is_ambient": false, + "start_line": 14, + "end_line": 28, + "entrypoints": [] + } + }, + "interfaces": {}, + "enums": {}, + "type_aliases": {}, + "functions": { + "src/controllers.Controller": { + "name": "Controller", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/controllers.ts", + "signature": "src/controllers.Controller", + "comments": [], + "decorators": [], + "parameters": [ + { + "name": "prefix", + "type": "string", + "default_value": null, + "is_optional": false, + "is_rest": false, + "is_readonly": false, + "accessibility": null, + "decorators": [], + "start_line": 4, + "end_line": 4, + "start_column": 21, + "end_column": 35 + } + ], + "type_parameters": [], + "return_type": "ClassDecorator", + "code": "function Controller(prefix: string): ClassDecorator {\n return () => undefined;\n}", + "start_line": 4, + "end_line": 6, + "code_start_line": 4, + "accessed_symbols": [], + "call_sites": [], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [], + "cyclomatic_complexity": 1, + "kind": "function", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": false, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + }, + "src/controllers.Get": { + "name": "Get", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/controllers.ts", + "signature": "src/controllers.Get", + "comments": [], + "decorators": [], + "parameters": [ + { + "name": "path", + "type": "string", + "default_value": null, + "is_optional": false, + "is_rest": false, + "is_readonly": false, + "accessibility": null, + "decorators": [], + "start_line": 7, + "end_line": 7, + "start_column": 14, + "end_column": 26 + } + ], + "type_parameters": [], + "return_type": "MethodDecorator", + "code": "function Get(path: string): MethodDecorator {\n return () => undefined;\n}", + "start_line": 7, + "end_line": 9, + "code_start_line": 7, + "accessed_symbols": [], + "call_sites": [], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [], + "cyclomatic_complexity": 1, + "kind": "function", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": false, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + }, + "src/controllers.Param": { + "name": "Param", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/controllers.ts", + "signature": "src/controllers.Param", + "comments": [], + "decorators": [], + "parameters": [ + { + "name": "name", + "type": "string", + "default_value": null, + "is_optional": false, + "is_rest": false, + "is_readonly": false, + "accessibility": null, + "decorators": [], + "start_line": 10, + "end_line": 10, + "start_column": 16, + "end_column": 28 + } + ], + "type_parameters": [], + "return_type": "ParameterDecorator", + "code": "function Param(name: string): ParameterDecorator {\n return () => undefined;\n}", + "start_line": 10, + "end_line": 12, + "code_start_line": 10, + "accessed_symbols": [], + "call_sites": [], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [], + "cyclomatic_complexity": 1, + "kind": "function", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": false, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + } + }, + "namespaces": {}, + "variables": [], + "is_tsx": false, + "is_declaration_file": false, + "content_hash": "d42b8f615d51575e3436409b9fce70862182b6071da40471b4be9b95f7c9ee4c", + "last_modified": 1780493392399.7122, + "file_size": 699 + }, + "src/external.ts": { + "file_path": "src/external.ts", + "module_name": "src/external", + "imports": [ + { + "module": "node:crypto", + "name": "createHash", + "alias": null, + "is_type_only": false, + "import_kind": "named", + "start_line": 2, + "end_line": 2, + "start_column": 1, + "end_column": 54 + }, + { + "module": "node:crypto", + "name": "randomUUID", + "alias": null, + "is_type_only": false, + "import_kind": "named", + "start_line": 2, + "end_line": 2, + "start_column": 1, + "end_column": 54 + }, + { + "module": "node:path", + "name": "*", + "alias": "path", + "is_type_only": false, + "import_kind": "namespace", + "start_line": 3, + "end_line": 3, + "start_column": 1, + "end_column": 35 + } + ], + "exports": [], + "comments": [ + { + "content": "// Exercises phantom (external) nodes: calls into Node builtins via named + namespace imports.", + "is_docstring": false, + "start_line": 1, + "end_line": 1, + "start_column": 1, + "end_column": 95 + }, + { + "content": "/** Bare named-import calls \u2192 phantom node:crypto.randomUUID / node:crypto.createHash. */", + "is_docstring": true, + "start_line": 5, + "end_line": 5, + "start_column": 1, + "end_column": 90 + }, + { + "content": "/** Namespace-import member call \u2192 phantom node:path.extname. */", + "is_docstring": true, + "start_line": 13, + "end_line": 13, + "start_column": 1, + "end_column": 65 + } + ], + "classes": {}, + "interfaces": {}, + "enums": {}, + "type_aliases": {}, + "functions": { + "src/external.fingerprint": { + "name": "fingerprint", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/external.ts", + "signature": "src/external.fingerprint", + "comments": [ + { + "content": "Bare named-import calls \u2192 phantom node:crypto.randomUUID / node:crypto.createHash.", + "is_docstring": true, + "start_line": 5, + "end_line": 5, + "start_column": 1, + "end_column": 90 + } + ], + "decorators": [], + "parameters": [ + { + "name": "name", + "type": "string", + "default_value": null, + "is_optional": false, + "is_rest": false, + "is_readonly": false, + "accessibility": null, + "decorators": [], + "start_line": 6, + "end_line": 6, + "start_column": 29, + "end_column": 41 + } + ], + "type_parameters": [], + "return_type": "string", + "code": "export function fingerprint(name: string): string {\n const id = randomUUID();\n return createHash(\"sha256\")\n .update(name + id)\n .digest(\"hex\");\n}", + "start_line": 5, + "end_line": 11, + "code_start_line": 6, + "accessed_symbols": [], + "call_sites": [ + { + "method_name": "randomUUID", + "receiver_expr": null, + "receiver_type": null, + "argument_types": [], + "type_arguments": [], + "return_type": "any", + "callee_signature": "node:crypto.randomUUID", + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 7, + "end_line": 7, + "start_column": 14, + "end_column": 26 + }, + { + "method_name": "digest", + "receiver_expr": "createHash(\"sha256\")\n .update(name + id)", + "receiver_type": "any", + "argument_types": [ + "\"hex\"" + ], + "type_arguments": [], + "return_type": "any", + "callee_signature": null, + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 8, + "end_line": 10, + "start_column": 10, + "end_column": 19 + }, + { + "method_name": "update", + "receiver_expr": "createHash(\"sha256\")", + "receiver_type": "any", + "argument_types": [ + "string" + ], + "type_arguments": [], + "return_type": "any", + "callee_signature": null, + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 8, + "end_line": 9, + "start_column": 10, + "end_column": 23 + }, + { + "method_name": "createHash", + "receiver_expr": null, + "receiver_type": null, + "argument_types": [ + "\"sha256\"" + ], + "type_arguments": [], + "return_type": "any", + "callee_signature": "node:crypto.createHash", + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 8, + "end_line": 8, + "start_column": 10, + "end_column": 30 + } + ], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [ + { + "name": "id", + "type": "any", + "initializer": "randomUUID()", + "value": null, + "scope": "function", + "declaration_kind": "const", + "is_readonly": true, + "is_exported": false, + "start_line": 7, + "end_line": 7, + "start_column": 9, + "end_column": 26 + } + ], + "cyclomatic_complexity": 1, + "kind": "function", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": true, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + }, + "src/external.extensionOf": { + "name": "extensionOf", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/external.ts", + "signature": "src/external.extensionOf", + "comments": [ + { + "content": "Namespace-import member call \u2192 phantom node:path.extname.", + "is_docstring": true, + "start_line": 13, + "end_line": 13, + "start_column": 1, + "end_column": 65 + } + ], + "decorators": [], + "parameters": [ + { + "name": "file", + "type": "string", + "default_value": null, + "is_optional": false, + "is_rest": false, + "is_readonly": false, + "accessibility": null, + "decorators": [], + "start_line": 14, + "end_line": 14, + "start_column": 29, + "end_column": 41 + } + ], + "type_parameters": [], + "return_type": "string", + "code": "export function extensionOf(file: string): string {\n return path.extname(file);\n}", + "start_line": 13, + "end_line": 16, + "code_start_line": 14, + "accessed_symbols": [], + "call_sites": [ + { + "method_name": "extname", + "receiver_expr": "path", + "receiver_type": "any", + "argument_types": [ + "string" + ], + "type_arguments": [], + "return_type": "any", + "callee_signature": "node:path.extname", + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 15, + "end_line": 15, + "start_column": 10, + "end_column": 28 + } + ], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [], + "cyclomatic_complexity": 1, + "kind": "function", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": true, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + } + }, + "namespaces": {}, + "variables": [], + "is_tsx": false, + "is_declaration_file": false, + "content_hash": "049a7393ffc7647fcab8ddeadb0eb82f74c57a385195aec4a4f0700ebf9c88c0", + "last_modified": 1780591453016.1892, + "file_size": 582 + }, + "src/index.ts": { + "file_path": "src/index.ts", + "module_name": "src/index", + "imports": [ + { + "module": "./controllers", + "name": "UserController", + "alias": null, + "is_type_only": false, + "import_kind": "named", + "start_line": 1, + "end_line": 1, + "start_column": 1, + "end_column": 48 + }, + { + "module": "./models", + "name": "Robot", + "alias": null, + "is_type_only": false, + "import_kind": "named", + "start_line": 2, + "end_line": 2, + "start_column": 1, + "end_column": 46 + }, + { + "module": "./models", + "name": "Role", + "alias": null, + "is_type_only": false, + "import_kind": "named", + "start_line": 2, + "end_line": 2, + "start_column": 1, + "end_column": 46 + }, + { + "module": "./models", + "name": "User", + "alias": null, + "is_type_only": false, + "import_kind": "named", + "start_line": 2, + "end_line": 2, + "start_column": 1, + "end_column": 46 + }, + { + "module": "./services", + "name": "UserService", + "alias": null, + "is_type_only": false, + "import_kind": "named", + "start_line": 3, + "end_line": 3, + "start_column": 1, + "end_column": 52 + }, + { + "module": "./services", + "name": "announce", + "alias": null, + "is_type_only": false, + "import_kind": "named", + "start_line": 3, + "end_line": 3, + "start_column": 1, + "end_column": 52 + }, + { + "module": "./util", + "name": "StringUtil", + "alias": null, + "is_type_only": false, + "import_kind": "named", + "start_line": 4, + "end_line": 4, + "start_column": 1, + "end_column": 37 + } + ], + "exports": [], + "comments": [], + "classes": {}, + "interfaces": {}, + "enums": {}, + "type_aliases": {}, + "functions": { + "src/index.main": { + "name": "main", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/index.ts", + "signature": "src/index.main", + "comments": [], + "decorators": [], + "parameters": [], + "type_parameters": [], + "return_type": "void", + "code": "export function main(): void {\n const service = new UserService(100);\n service.create(\"Ada\", Role.Admin);\n service.createGuest();\n\n const controller = new UserController(service);\n controller.list();\n controller.show(\"42\");\n\n // interface-typed dispatch \u2014 RTA should expand announce -> {User,Robot}.describe\n announce(new User(1, \"Ada\", Role.Admin));\n announce(new Robot(\"r2d2\"));\n\n const slug = StringUtil.repeat(\"hello world\", 2);\n const builder = new StringUtil.Builder();\n builder.add(\"a\").add(\"b\").build();\n console.log(slug);\n}", + "start_line": 6, + "end_line": 23, + "code_start_line": 6, + "accessed_symbols": [], + "call_sites": [ + { + "method_name": "UserService", + "receiver_expr": null, + "receiver_type": null, + "argument_types": [ + "100" + ], + "type_arguments": [], + "return_type": "UserService", + "callee_signature": "src/services.UserService.constructor", + "is_constructor_call": true, + "is_optional_chain": false, + "start_line": 7, + "end_line": 7, + "start_column": 19, + "end_column": 39 + }, + { + "method_name": "create", + "receiver_expr": "service", + "receiver_type": "UserService", + "argument_types": [ + "\"Ada\"", + "Role.Admin" + ], + "type_arguments": [], + "return_type": "User", + "callee_signature": "src/services.UserService.create", + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 8, + "end_line": 8, + "start_column": 3, + "end_column": 36 + }, + { + "method_name": "createGuest", + "receiver_expr": "service", + "receiver_type": "UserService", + "argument_types": [], + "type_arguments": [], + "return_type": "User", + "callee_signature": "src/services.UserService.createGuest", + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 9, + "end_line": 9, + "start_column": 3, + "end_column": 24 + }, + { + "method_name": "UserController", + "receiver_expr": null, + "receiver_type": null, + "argument_types": [ + "UserService" + ], + "type_arguments": [], + "return_type": "UserController", + "callee_signature": "src/controllers.UserController.constructor", + "is_constructor_call": true, + "is_optional_chain": false, + "start_line": 11, + "end_line": 11, + "start_column": 22, + "end_column": 49 + }, + { + "method_name": "list", + "receiver_expr": "controller", + "receiver_type": "UserController", + "argument_types": [], + "type_arguments": [], + "return_type": "string[]", + "callee_signature": "src/controllers.UserController.list", + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 12, + "end_line": 12, + "start_column": 3, + "end_column": 20 + }, + { + "method_name": "show", + "receiver_expr": "controller", + "receiver_type": "UserController", + "argument_types": [ + "\"42\"" + ], + "type_arguments": [], + "return_type": "string", + "callee_signature": "src/controllers.UserController.show", + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 13, + "end_line": 13, + "start_column": 3, + "end_column": 24 + }, + { + "method_name": "announce", + "receiver_expr": null, + "receiver_type": null, + "argument_types": [ + "User" + ], + "type_arguments": [], + "return_type": "string", + "callee_signature": "src/services.announce", + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 16, + "end_line": 16, + "start_column": 3, + "end_column": 43 + }, + { + "method_name": "User", + "receiver_expr": null, + "receiver_type": null, + "argument_types": [ + "1", + "\"Ada\"", + "Role.Admin" + ], + "type_arguments": [], + "return_type": "User", + "callee_signature": "src/models.User.constructor", + "is_constructor_call": true, + "is_optional_chain": false, + "start_line": 16, + "end_line": 16, + "start_column": 12, + "end_column": 42 + }, + { + "method_name": "announce", + "receiver_expr": null, + "receiver_type": null, + "argument_types": [ + "Robot" + ], + "type_arguments": [], + "return_type": "string", + "callee_signature": "src/services.announce", + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 17, + "end_line": 17, + "start_column": 3, + "end_column": 30 + }, + { + "method_name": "Robot", + "receiver_expr": null, + "receiver_type": null, + "argument_types": [ + "\"r2d2\"" + ], + "type_arguments": [], + "return_type": "Robot", + "callee_signature": "src/models.Robot.constructor", + "is_constructor_call": true, + "is_optional_chain": false, + "start_line": 17, + "end_line": 17, + "start_column": 12, + "end_column": 29 + }, + { + "method_name": "repeat", + "receiver_expr": "StringUtil", + "receiver_type": "typeof StringUtil", + "argument_types": [ + "\"hello world\"", + "2" + ], + "type_arguments": [], + "return_type": "string", + "callee_signature": "src/util.StringUtil.repeat", + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 19, + "end_line": 19, + "start_column": 16, + "end_column": 51 + }, + { + "method_name": "Builder", + "receiver_expr": "StringUtil", + "receiver_type": "typeof StringUtil", + "argument_types": [], + "type_arguments": [], + "return_type": "StringUtil.Builder", + "callee_signature": "src/util.StringUtil.Builder.constructor", + "is_constructor_call": true, + "is_optional_chain": false, + "start_line": 20, + "end_line": 20, + "start_column": 19, + "end_column": 43 + }, + { + "method_name": "build", + "receiver_expr": "builder.add(\"a\").add(\"b\")", + "receiver_type": "StringUtil.Builder", + "argument_types": [], + "type_arguments": [], + "return_type": "string", + "callee_signature": "src/util.StringUtil.Builder.build", + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 21, + "end_line": 21, + "start_column": 3, + "end_column": 36 + }, + { + "method_name": "add", + "receiver_expr": "builder.add(\"a\")", + "receiver_type": "StringUtil.Builder", + "argument_types": [ + "\"b\"" + ], + "type_arguments": [], + "return_type": "StringUtil.Builder", + "callee_signature": "src/util.StringUtil.Builder.add", + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 21, + "end_line": 21, + "start_column": 3, + "end_column": 28 + }, + { + "method_name": "add", + "receiver_expr": "builder", + "receiver_type": "StringUtil.Builder", + "argument_types": [ + "\"a\"" + ], + "type_arguments": [], + "return_type": "StringUtil.Builder", + "callee_signature": "src/util.StringUtil.Builder.add", + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 21, + "end_line": 21, + "start_column": 3, + "end_column": 19 + }, + { + "method_name": "log", + "receiver_expr": "console", + "receiver_type": "Console", + "argument_types": [ + "string" + ], + "type_arguments": [], + "return_type": "void", + "callee_signature": null, + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 22, + "end_line": 22, + "start_column": 3, + "end_column": 20 + } + ], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [ + { + "name": "service", + "type": "UserService", + "initializer": "new UserService(100)", + "value": null, + "scope": "function", + "declaration_kind": "const", + "is_readonly": true, + "is_exported": false, + "start_line": 7, + "end_line": 7, + "start_column": 9, + "end_column": 39 + }, + { + "name": "controller", + "type": "UserController", + "initializer": "new UserController(service)", + "value": null, + "scope": "function", + "declaration_kind": "const", + "is_readonly": true, + "is_exported": false, + "start_line": 11, + "end_line": 11, + "start_column": 9, + "end_column": 49 + }, + { + "name": "slug", + "type": "string", + "initializer": "StringUtil.repeat(\"hello world\", 2)", + "value": null, + "scope": "function", + "declaration_kind": "const", + "is_readonly": true, + "is_exported": false, + "start_line": 19, + "end_line": 19, + "start_column": 9, + "end_column": 51 + }, + { + "name": "builder", + "type": "StringUtil.Builder", + "initializer": "new StringUtil.Builder()", + "value": null, + "scope": "function", + "declaration_kind": "const", + "is_readonly": true, + "is_exported": false, + "start_line": 20, + "end_line": 20, + "start_column": 9, + "end_column": 43 + } + ], + "cyclomatic_complexity": 1, + "kind": "function", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": true, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + } + }, + "namespaces": {}, + "variables": [], + "is_tsx": false, + "is_declaration_file": false, + "content_hash": "6de6f6ff5a01a5c7c750a93d5e10bae7b15817f386421f45c7527cfb7259d9b3", + "last_modified": 1780497690533.4756, + "file_size": 742 + }, + "src/models.ts": { + "file_path": "src/models.ts", + "module_name": "src/models", + "imports": [], + "exports": [], + "comments": [ + { + "content": "/** Domain models for the sample app. */", + "is_docstring": true, + "start_line": 1, + "end_line": 1, + "start_column": 1, + "end_column": 41 + }, + { + "content": "/** A user of the system. */", + "is_docstring": true, + "start_line": 25, + "end_line": 25, + "start_column": 1, + "end_column": 29 + }, + { + "content": "/** A second, unrelated implementer of Named \u2014 drives RTA subtype expansion. */", + "is_docstring": true, + "start_line": 58, + "end_line": 58, + "start_column": 1, + "end_column": 80 + } + ], + "classes": { + "src/models.Entity": { + "name": "Entity", + "signature": "src/models.Entity", + "comments": [ + { + "content": "A user of the system.", + "is_docstring": true, + "start_line": 25, + "end_line": 25, + "start_column": 1, + "end_column": 29 + } + ], + "code": "export abstract class Entity implements Identifiable {\n constructor(public readonly id: ID) {}\n abstract describe(): string;\n}", + "decorators": [], + "base_classes": [ + "src/models.Identifiable" + ], + "implements_types": [ + "src/models.Identifiable" + ], + "type_parameters": [ + { + "name": "ID", + "constraint": null, + "default": "string" + } + ], + "methods": { + "src/models.Entity.describe": { + "name": "describe", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/models.ts", + "signature": "src/models.Entity.describe", + "comments": [], + "decorators": [], + "parameters": [], + "type_parameters": [], + "return_type": "string", + "code": "abstract describe(): string;", + "start_line": 28, + "end_line": 28, + "code_start_line": 28, + "accessed_symbols": [], + "call_sites": [], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [], + "cyclomatic_complexity": 0, + "kind": "method", + "accessibility": null, + "is_static": false, + "is_abstract": true, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": false, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [ + { + "parameters": [], + "return_type": "string", + "type_parameters": [], + "start_line": 28, + "end_line": 28 + } + ], + "entrypoints": [] + }, + "src/models.Entity.constructor": { + "name": "constructor", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/models.ts", + "signature": "src/models.Entity.constructor", + "comments": [], + "decorators": [], + "parameters": [ + { + "name": "id", + "type": "ID", + "default_value": null, + "is_optional": false, + "is_rest": false, + "is_readonly": true, + "accessibility": "public", + "decorators": [], + "start_line": 27, + "end_line": 27, + "start_column": 15, + "end_column": 37 + } + ], + "type_parameters": [], + "return_type": null, + "code": "constructor(public readonly id: ID) {}", + "start_line": 27, + "end_line": 27, + "code_start_line": 27, + "accessed_symbols": [], + "call_sites": [], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [], + "cyclomatic_complexity": 1, + "kind": "constructor", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": false, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + } + }, + "attributes": { + "id": { + "name": "id", + "type": "ID", + "comments": [], + "decorators": [], + "initializer": null, + "accessibility": "public", + "is_static": false, + "is_readonly": true, + "is_optional": false, + "is_abstract": false, + "start_line": 27, + "end_line": 27 + } + }, + "inner_classes": {}, + "is_abstract": true, + "is_exported": true, + "is_ambient": false, + "start_line": 25, + "end_line": 29, + "entrypoints": [] + }, + "src/models.User": { + "name": "User", + "signature": "src/models.User", + "comments": [], + "code": "export class User extends Entity implements Named {\n private loginCount = 0;\n static instances = 0;\n\n constructor(\n id: UserId,\n public name: string,\n private role: Role = Role.Member,\n ) {\n super(id);\n User.instances++;\n }\n\n get isAdmin(): boolean {\n return this.role === Role.Admin;\n }\n\n describe(): string {\n return `${this.name} (${this.role})`;\n }\n\n async recordLogin(): Promise {\n this.loginCount += 1;\n return this.loginCount;\n }\n}", + "decorators": [], + "base_classes": [ + "src/models.Entity", + "src/models.Named" + ], + "implements_types": [ + "src/models.Named" + ], + "type_parameters": [], + "methods": { + "src/models.User.describe": { + "name": "describe", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/models.ts", + "signature": "src/models.User.describe", + "comments": [], + "decorators": [], + "parameters": [], + "type_parameters": [], + "return_type": "string", + "code": "describe(): string {\n return `${this.name} (${this.role})`;\n }", + "start_line": 48, + "end_line": 50, + "code_start_line": 48, + "accessed_symbols": [], + "call_sites": [], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [], + "cyclomatic_complexity": 1, + "kind": "method", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": false, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + }, + "src/models.User.recordLogin": { + "name": "recordLogin", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/models.ts", + "signature": "src/models.User.recordLogin", + "comments": [], + "decorators": [], + "parameters": [], + "type_parameters": [], + "return_type": "Promise", + "code": "async recordLogin(): Promise {\n this.loginCount += 1;\n return this.loginCount;\n }", + "start_line": 52, + "end_line": 55, + "code_start_line": 52, + "accessed_symbols": [], + "call_sites": [], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [], + "cyclomatic_complexity": 1, + "kind": "method", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": true, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": false, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + }, + "src/models.User.constructor": { + "name": "constructor", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/models.ts", + "signature": "src/models.User.constructor", + "comments": [], + "decorators": [], + "parameters": [ + { + "name": "id", + "type": "UserId", + "default_value": null, + "is_optional": false, + "is_rest": false, + "is_readonly": false, + "accessibility": null, + "decorators": [], + "start_line": 36, + "end_line": 36, + "start_column": 5, + "end_column": 15 + }, + { + "name": "name", + "type": "string", + "default_value": null, + "is_optional": false, + "is_rest": false, + "is_readonly": false, + "accessibility": "public", + "decorators": [], + "start_line": 37, + "end_line": 37, + "start_column": 5, + "end_column": 24 + }, + { + "name": "role", + "type": "Role", + "default_value": "Role.Member", + "is_optional": true, + "is_rest": false, + "is_readonly": false, + "accessibility": "private", + "decorators": [], + "start_line": 38, + "end_line": 38, + "start_column": 5, + "end_column": 37 + } + ], + "type_parameters": [], + "return_type": null, + "code": "constructor(\n id: UserId,\n public name: string,\n private role: Role = Role.Member,\n ) {\n super(id);\n User.instances++;\n }", + "start_line": 35, + "end_line": 42, + "code_start_line": 35, + "accessed_symbols": [], + "call_sites": [ + { + "method_name": "super", + "receiver_expr": null, + "receiver_type": null, + "argument_types": [ + "UserId" + ], + "type_arguments": [], + "return_type": "void", + "callee_signature": "src/models.Entity.constructor", + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 40, + "end_line": 40, + "start_column": 5, + "end_column": 14 + } + ], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [], + "cyclomatic_complexity": 1, + "kind": "constructor", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": false, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + }, + "src/models.User.isAdmin#get": { + "name": "isAdmin", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/models.ts", + "signature": "src/models.User.isAdmin", + "comments": [], + "decorators": [], + "parameters": [], + "type_parameters": [], + "return_type": "boolean", + "code": "get isAdmin(): boolean {\n return this.role === Role.Admin;\n }", + "start_line": 44, + "end_line": 46, + "code_start_line": 44, + "accessed_symbols": [], + "call_sites": [], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [], + "cyclomatic_complexity": 1, + "kind": "getter", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": false, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": "getter", + "overload_signatures": [], + "entrypoints": [] + } + }, + "attributes": { + "loginCount": { + "name": "loginCount", + "type": "number", + "comments": [], + "decorators": [], + "initializer": "0", + "accessibility": "private", + "is_static": false, + "is_readonly": false, + "is_optional": false, + "is_abstract": false, + "start_line": 32, + "end_line": 32 + }, + "instances": { + "name": "instances", + "type": "number", + "comments": [], + "decorators": [], + "initializer": "0", + "accessibility": null, + "is_static": true, + "is_readonly": false, + "is_optional": false, + "is_abstract": false, + "start_line": 33, + "end_line": 33 + }, + "name": { + "name": "name", + "type": "string", + "comments": [], + "decorators": [], + "initializer": null, + "accessibility": "public", + "is_static": false, + "is_readonly": false, + "is_optional": false, + "is_abstract": false, + "start_line": 37, + "end_line": 37 + }, + "role": { + "name": "role", + "type": "Role", + "comments": [], + "decorators": [], + "initializer": null, + "accessibility": "private", + "is_static": false, + "is_readonly": false, + "is_optional": true, + "is_abstract": false, + "start_line": 38, + "end_line": 38 + } + }, + "inner_classes": {}, + "is_abstract": false, + "is_exported": true, + "is_ambient": false, + "start_line": 31, + "end_line": 56, + "entrypoints": [] + }, + "src/models.Robot": { + "name": "Robot", + "signature": "src/models.Robot", + "comments": [ + { + "content": "A second, unrelated implementer of Named \u2014 drives RTA subtype expansion.", + "is_docstring": true, + "start_line": 58, + "end_line": 58, + "start_column": 1, + "end_column": 80 + } + ], + "code": "export class Robot implements Named {\n constructor(public name: string) {}\n describe(): string {\n return `robot:${this.name}`;\n }\n}", + "decorators": [], + "base_classes": [ + "src/models.Named" + ], + "implements_types": [ + "src/models.Named" + ], + "type_parameters": [], + "methods": { + "src/models.Robot.describe": { + "name": "describe", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/models.ts", + "signature": "src/models.Robot.describe", + "comments": [], + "decorators": [], + "parameters": [], + "type_parameters": [], + "return_type": "string", + "code": "describe(): string {\n return `robot:${this.name}`;\n }", + "start_line": 61, + "end_line": 63, + "code_start_line": 61, + "accessed_symbols": [], + "call_sites": [], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [], + "cyclomatic_complexity": 1, + "kind": "method", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": false, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + }, + "src/models.Robot.constructor": { + "name": "constructor", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/models.ts", + "signature": "src/models.Robot.constructor", + "comments": [], + "decorators": [], + "parameters": [ + { + "name": "name", + "type": "string", + "default_value": null, + "is_optional": false, + "is_rest": false, + "is_readonly": false, + "accessibility": "public", + "decorators": [], + "start_line": 60, + "end_line": 60, + "start_column": 15, + "end_column": 34 + } + ], + "type_parameters": [], + "return_type": null, + "code": "constructor(public name: string) {}", + "start_line": 60, + "end_line": 60, + "code_start_line": 60, + "accessed_symbols": [], + "call_sites": [], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [], + "cyclomatic_complexity": 1, + "kind": "constructor", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": false, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + } + }, + "attributes": { + "name": { + "name": "name", + "type": "string", + "comments": [], + "decorators": [], + "initializer": null, + "accessibility": "public", + "is_static": false, + "is_readonly": false, + "is_optional": false, + "is_abstract": false, + "start_line": 60, + "end_line": 60 + } + }, + "inner_classes": {}, + "is_abstract": false, + "is_exported": true, + "is_ambient": false, + "start_line": 58, + "end_line": 64, + "entrypoints": [] + } + }, + "interfaces": { + "src/models.Identifiable": { + "name": "Identifiable", + "signature": "src/models.Identifiable", + "comments": [ + { + "content": "Domain models for the sample app.", + "is_docstring": true, + "start_line": 1, + "end_line": 1, + "start_column": 1, + "end_column": 41 + } + ], + "code": "export interface Identifiable {\n readonly id: T;\n}", + "base_classes": [], + "type_parameters": [ + { + "name": "T", + "constraint": null, + "default": "string" + } + ], + "methods": {}, + "properties": { + "id": { + "name": "id", + "type": "T", + "comments": [], + "decorators": [], + "initializer": null, + "accessibility": null, + "is_static": false, + "is_readonly": true, + "is_optional": false, + "is_abstract": false, + "start_line": 4, + "end_line": 4 + } + }, + "call_signatures": [], + "index_signatures": [], + "is_exported": true, + "is_ambient": false, + "start_line": 1, + "end_line": 5 + }, + "src/models.Named": { + "name": "Named", + "signature": "src/models.Named", + "comments": [], + "code": "export interface Named {\n name: string;\n describe(): string;\n}", + "base_classes": [], + "type_parameters": [], + "methods": { + "src/models.Named.describe": { + "name": "describe", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/models.ts", + "signature": "src/models.Named.describe", + "comments": [], + "decorators": [], + "parameters": [], + "type_parameters": [], + "return_type": "string", + "code": "describe(): string;", + "start_line": 9, + "end_line": 9, + "code_start_line": 9, + "accessed_symbols": [], + "call_sites": [], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [], + "cyclomatic_complexity": 0, + "kind": "method", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": false, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + } + }, + "properties": { + "name": { + "name": "name", + "type": "string", + "comments": [], + "decorators": [], + "initializer": null, + "accessibility": null, + "is_static": false, + "is_readonly": false, + "is_optional": false, + "is_abstract": false, + "start_line": 8, + "end_line": 8 + } + }, + "call_signatures": [], + "index_signatures": [], + "is_exported": true, + "is_ambient": false, + "start_line": 7, + "end_line": 10 + } + }, + "enums": { + "src/models.Role": { + "name": "Role", + "signature": "src/models.Role", + "comments": [], + "code": "export enum Role {\n Admin = \"admin\",\n Member = \"member\",\n Guest = \"guest\",\n}", + "members": [ + { + "name": "Admin", + "value": "admin", + "start_line": 15, + "end_line": 15 + }, + { + "name": "Member", + "value": "member", + "start_line": 16, + "end_line": 16 + }, + { + "name": "Guest", + "value": "guest", + "start_line": 17, + "end_line": 17 + } + ], + "is_const": false, + "is_exported": true, + "is_ambient": false, + "start_line": 14, + "end_line": 18 + }, + "src/models.Flag": { + "name": "Flag", + "signature": "src/models.Flag", + "comments": [], + "code": "export const enum Flag {\n None = 0,\n Active = 1,\n}", + "members": [ + { + "name": "None", + "value": "0", + "start_line": 21, + "end_line": 21 + }, + { + "name": "Active", + "value": "1", + "start_line": 22, + "end_line": 22 + } + ], + "is_const": true, + "is_exported": true, + "is_ambient": false, + "start_line": 20, + "end_line": 23 + } + }, + "type_aliases": { + "src/models.UserId": { + "name": "UserId", + "signature": "src/models.UserId", + "comments": [], + "code": "export type UserId = string | number;", + "aliased_type": "string | number", + "type_parameters": [], + "is_exported": true, + "is_ambient": false, + "start_line": 12, + "end_line": 12 + } + }, + "functions": {}, + "namespaces": {}, + "variables": [], + "is_tsx": false, + "is_declaration_file": false, + "content_hash": "d39ff571ccfda4b7c46923586b6b94d0e818a51be966f1ba40305650fd0ffc91", + "last_modified": 1780497676166.356, + "file_size": 1237 + }, + "src/services.ts": { + "file_path": "src/services.ts", + "module_name": "src/services", + "imports": [ + { + "module": "./models", + "name": "Named", + "alias": null, + "is_type_only": true, + "import_kind": "named", + "start_line": 1, + "end_line": 1, + "start_column": 1, + "end_column": 64 + }, + { + "module": "./models", + "name": "Role", + "alias": null, + "is_type_only": false, + "import_kind": "named", + "start_line": 1, + "end_line": 1, + "start_column": 1, + "end_column": 64 + }, + { + "module": "./models", + "name": "User", + "alias": null, + "is_type_only": false, + "import_kind": "named", + "start_line": 1, + "end_line": 1, + "start_column": 1, + "end_column": 64 + }, + { + "module": "./models", + "name": "UserId", + "alias": null, + "is_type_only": true, + "import_kind": "named", + "start_line": 1, + "end_line": 1, + "start_column": 1, + "end_column": 64 + } + ], + "exports": [], + "comments": [ + { + "content": "/** Pure helper \u2014 top-level function. */", + "is_docstring": true, + "start_line": 3, + "end_line": 3, + "start_column": 1, + "end_column": 41 + }, + { + "content": "/**\n * Calls describe() on an interface-typed receiver. Under RTA this expands to every instantiated\n * concrete implementer of Named (User, Robot, ...).\n */", + "is_docstring": true, + "start_line": 8, + "end_line": 11, + "start_column": 1, + "end_column": 4 + }, + { + "content": "/** Arrow function bound to a const (function_expression-style callable). */", + "is_docstring": true, + "start_line": 16, + "end_line": 16, + "start_column": 1, + "end_column": 77 + } + ], + "classes": { + "src/services.UserService": { + "name": "UserService", + "signature": "src/services.UserService", + "comments": [], + "code": "export class UserService {\n private users: User[] = [];\n\n constructor(private readonly startId: number = 0) {}\n\n create(name: string, role: Role = Role.Member): User {\n const id = nextId(this.users.length + this.startId);\n const user = new User(id, name, role);\n this.users.push(user);\n return user;\n }\n\n createGuest(): User {\n const name = makeGuestName(this.users.length);\n return this.create(name, Role.Guest);\n }\n\n describeAll(): string[] {\n return this.users.map((u) => u.describe());\n }\n\n async loginAll(): Promise {\n let total = 0;\n for (const u of this.users) {\n total += await u.recordLogin();\n }\n return total;\n }\n}", + "decorators": [], + "base_classes": [], + "implements_types": [], + "type_parameters": [], + "methods": { + "src/services.UserService.create": { + "name": "create", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/services.ts", + "signature": "src/services.UserService.create", + "comments": [], + "decorators": [], + "parameters": [ + { + "name": "name", + "type": "string", + "default_value": null, + "is_optional": false, + "is_rest": false, + "is_readonly": false, + "accessibility": null, + "decorators": [], + "start_line": 24, + "end_line": 24, + "start_column": 10, + "end_column": 22 + }, + { + "name": "role", + "type": "Role", + "default_value": "Role.Member", + "is_optional": true, + "is_rest": false, + "is_readonly": false, + "accessibility": null, + "decorators": [], + "start_line": 24, + "end_line": 24, + "start_column": 24, + "end_column": 48 + } + ], + "type_parameters": [], + "return_type": "User", + "code": "create(name: string, role: Role = Role.Member): User {\n const id = nextId(this.users.length + this.startId);\n const user = new User(id, name, role);\n this.users.push(user);\n return user;\n }", + "start_line": 24, + "end_line": 29, + "code_start_line": 24, + "accessed_symbols": [], + "call_sites": [ + { + "method_name": "nextId", + "receiver_expr": null, + "receiver_type": null, + "argument_types": [ + "number" + ], + "type_arguments": [], + "return_type": "UserId", + "callee_signature": "src/services.nextId", + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 25, + "end_line": 25, + "start_column": 16, + "end_column": 56 + }, + { + "method_name": "User", + "receiver_expr": null, + "receiver_type": null, + "argument_types": [ + "UserId", + "string", + "Role" + ], + "type_arguments": [], + "return_type": "User", + "callee_signature": "src/models.User.constructor", + "is_constructor_call": true, + "is_optional_chain": false, + "start_line": 26, + "end_line": 26, + "start_column": 18, + "end_column": 42 + }, + { + "method_name": "push", + "receiver_expr": "this.users", + "receiver_type": "User[]", + "argument_types": [ + "User" + ], + "type_arguments": [], + "return_type": "number", + "callee_signature": null, + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 27, + "end_line": 27, + "start_column": 5, + "end_column": 26 + } + ], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [ + { + "name": "id", + "type": "UserId", + "initializer": "nextId(this.users.length + this.startId)", + "value": null, + "scope": "function", + "declaration_kind": "const", + "is_readonly": true, + "is_exported": false, + "start_line": 25, + "end_line": 25, + "start_column": 11, + "end_column": 56 + }, + { + "name": "user", + "type": "User", + "initializer": "new User(id, name, role)", + "value": null, + "scope": "function", + "declaration_kind": "const", + "is_readonly": true, + "is_exported": false, + "start_line": 26, + "end_line": 26, + "start_column": 11, + "end_column": 42 + } + ], + "cyclomatic_complexity": 1, + "kind": "method", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": false, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + }, + "src/services.UserService.createGuest": { + "name": "createGuest", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/services.ts", + "signature": "src/services.UserService.createGuest", + "comments": [], + "decorators": [], + "parameters": [], + "type_parameters": [], + "return_type": "User", + "code": "createGuest(): User {\n const name = makeGuestName(this.users.length);\n return this.create(name, Role.Guest);\n }", + "start_line": 31, + "end_line": 34, + "code_start_line": 31, + "accessed_symbols": [], + "call_sites": [ + { + "method_name": "makeGuestName", + "receiver_expr": null, + "receiver_type": null, + "argument_types": [ + "number" + ], + "type_arguments": [], + "return_type": "string", + "callee_signature": "src/services.makeGuestName", + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 32, + "end_line": 32, + "start_column": 18, + "end_column": 50 + }, + { + "method_name": "create", + "receiver_expr": "this", + "receiver_type": "this", + "argument_types": [ + "string", + "Role.Guest" + ], + "type_arguments": [], + "return_type": "User", + "callee_signature": "src/services.UserService.create", + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 33, + "end_line": 33, + "start_column": 12, + "end_column": 41 + } + ], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [ + { + "name": "name", + "type": "string", + "initializer": "makeGuestName(this.users.length)", + "value": null, + "scope": "function", + "declaration_kind": "const", + "is_readonly": true, + "is_exported": false, + "start_line": 32, + "end_line": 32, + "start_column": 11, + "end_column": 50 + } + ], + "cyclomatic_complexity": 1, + "kind": "method", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": false, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + }, + "src/services.UserService.describeAll": { + "name": "describeAll", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/services.ts", + "signature": "src/services.UserService.describeAll", + "comments": [], + "decorators": [], + "parameters": [], + "type_parameters": [], + "return_type": "string[]", + "code": "describeAll(): string[] {\n return this.users.map((u) => u.describe());\n }", + "start_line": 36, + "end_line": 38, + "code_start_line": 36, + "accessed_symbols": [], + "call_sites": [ + { + "method_name": "map", + "receiver_expr": "this.users", + "receiver_type": "User[]", + "argument_types": [ + "(u: User) => string" + ], + "type_arguments": [], + "return_type": "string[]", + "callee_signature": null, + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 37, + "end_line": 37, + "start_column": 12, + "end_column": 47 + }, + { + "method_name": "describe", + "receiver_expr": "u", + "receiver_type": "User", + "argument_types": [], + "type_arguments": [], + "return_type": "string", + "callee_signature": "src/models.User.describe", + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 37, + "end_line": 37, + "start_column": 34, + "end_column": 46 + } + ], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [], + "cyclomatic_complexity": 1, + "kind": "method", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": false, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + }, + "src/services.UserService.loginAll": { + "name": "loginAll", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/services.ts", + "signature": "src/services.UserService.loginAll", + "comments": [], + "decorators": [], + "parameters": [], + "type_parameters": [], + "return_type": "Promise", + "code": "async loginAll(): Promise {\n let total = 0;\n for (const u of this.users) {\n total += await u.recordLogin();\n }\n return total;\n }", + "start_line": 40, + "end_line": 46, + "code_start_line": 40, + "accessed_symbols": [], + "call_sites": [ + { + "method_name": "recordLogin", + "receiver_expr": "u", + "receiver_type": "User", + "argument_types": [], + "type_arguments": [], + "return_type": "Promise", + "callee_signature": "src/models.User.recordLogin", + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 43, + "end_line": 43, + "start_column": 22, + "end_column": 37 + } + ], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [ + { + "name": "total", + "type": "number", + "initializer": "0", + "value": null, + "scope": "function", + "declaration_kind": "let", + "is_readonly": false, + "is_exported": false, + "start_line": 41, + "end_line": 41, + "start_column": 9, + "end_column": 18 + }, + { + "name": "u", + "type": "User", + "initializer": null, + "value": null, + "scope": "function", + "declaration_kind": "unknown", + "is_readonly": false, + "is_exported": false, + "start_line": 42, + "end_line": 42, + "start_column": 16, + "end_column": 17 + } + ], + "cyclomatic_complexity": 2, + "kind": "method", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": true, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": false, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + }, + "src/services.UserService.constructor": { + "name": "constructor", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/services.ts", + "signature": "src/services.UserService.constructor", + "comments": [], + "decorators": [], + "parameters": [ + { + "name": "startId", + "type": "number", + "default_value": "0", + "is_optional": true, + "is_rest": false, + "is_readonly": true, + "accessibility": "private", + "decorators": [], + "start_line": 22, + "end_line": 22, + "start_column": 15, + "end_column": 51 + } + ], + "type_parameters": [], + "return_type": null, + "code": "constructor(private readonly startId: number = 0) {}", + "start_line": 22, + "end_line": 22, + "code_start_line": 22, + "accessed_symbols": [], + "call_sites": [], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [], + "cyclomatic_complexity": 1, + "kind": "constructor", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": false, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + } + }, + "attributes": { + "users": { + "name": "users", + "type": "User[]", + "comments": [], + "decorators": [], + "initializer": "[]", + "accessibility": "private", + "is_static": false, + "is_readonly": false, + "is_optional": false, + "is_abstract": false, + "start_line": 20, + "end_line": 20 + }, + "startId": { + "name": "startId", + "type": "number", + "comments": [], + "decorators": [], + "initializer": null, + "accessibility": "private", + "is_static": false, + "is_readonly": true, + "is_optional": true, + "is_abstract": false, + "start_line": 22, + "end_line": 22 + } + }, + "inner_classes": {}, + "is_abstract": false, + "is_exported": true, + "is_ambient": false, + "start_line": 19, + "end_line": 47, + "entrypoints": [] + } + }, + "interfaces": {}, + "enums": {}, + "type_aliases": {}, + "functions": { + "src/services.makeGuestName": { + "name": "makeGuestName", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/services.ts", + "signature": "src/services.makeGuestName", + "comments": [ + { + "content": "Pure helper \u2014 top-level function.", + "is_docstring": true, + "start_line": 3, + "end_line": 3, + "start_column": 1, + "end_column": 41 + } + ], + "decorators": [], + "parameters": [ + { + "name": "seed", + "type": "number", + "default_value": null, + "is_optional": false, + "is_rest": false, + "is_readonly": false, + "accessibility": null, + "decorators": [], + "start_line": 4, + "end_line": 4, + "start_column": 31, + "end_column": 43 + } + ], + "type_parameters": [], + "return_type": "string", + "code": "export function makeGuestName(seed: number): string {\n return `guest-${seed}`;\n}", + "start_line": 3, + "end_line": 6, + "code_start_line": 4, + "accessed_symbols": [], + "call_sites": [], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [], + "cyclomatic_complexity": 1, + "kind": "function", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": true, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + }, + "src/services.announce": { + "name": "announce", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/services.ts", + "signature": "src/services.announce", + "comments": [ + { + "content": "Calls describe() on an interface-typed receiver. Under RTA this expands to every instantiated\nconcrete implementer of Named (User, Robot, ...).", + "is_docstring": true, + "start_line": 8, + "end_line": 11, + "start_column": 1, + "end_column": 4 + } + ], + "decorators": [], + "parameters": [ + { + "name": "thing", + "type": "Named", + "default_value": null, + "is_optional": false, + "is_rest": false, + "is_readonly": false, + "accessibility": null, + "decorators": [], + "start_line": 12, + "end_line": 12, + "start_column": 26, + "end_column": 38 + } + ], + "type_parameters": [], + "return_type": "string", + "code": "export function announce(thing: Named): string {\n return thing.describe();\n}", + "start_line": 8, + "end_line": 14, + "code_start_line": 12, + "accessed_symbols": [], + "call_sites": [ + { + "method_name": "describe", + "receiver_expr": "thing", + "receiver_type": "Named", + "argument_types": [], + "type_arguments": [], + "return_type": "string", + "callee_signature": "src/models.Named.describe", + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 13, + "end_line": 13, + "start_column": 10, + "end_column": 26 + } + ], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [], + "cyclomatic_complexity": 1, + "kind": "function", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": true, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + }, + "src/services.nextId": { + "name": "nextId", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/services.ts", + "signature": "src/services.nextId", + "comments": [], + "decorators": [], + "parameters": [ + { + "name": "n", + "type": "number", + "default_value": null, + "is_optional": false, + "is_rest": false, + "is_readonly": false, + "accessibility": null, + "decorators": [], + "start_line": 17, + "end_line": 17, + "start_column": 24, + "end_column": 33 + } + ], + "type_parameters": [], + "return_type": "UserId", + "code": "nextId = (n: number): UserId => n + 1", + "start_line": 17, + "end_line": 17, + "code_start_line": 17, + "accessed_symbols": [], + "call_sites": [], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [], + "cyclomatic_complexity": 1, + "kind": "arrow", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": true, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + } + }, + "namespaces": {}, + "variables": [], + "is_tsx": false, + "is_declaration_file": false, + "content_hash": "1bda3666dab48f8e686fb8ffc2143f2e422af62be46698053cfeea4184b55617", + "last_modified": 1780497684064.8262, + "file_size": 1240 + }, + "src/util.ts": { + "file_path": "src/util.ts", + "module_name": "src/util", + "imports": [], + "exports": [], + "comments": [ + { + "content": "/** A namespace with nested declarations, to exercise the namespaces{} collection + nested signatures. */", + "is_docstring": true, + "start_line": 1, + "end_line": 1, + "start_column": 1, + "end_column": 106 + }, + { + "content": "/** Generic top-level function with a nested helper, to exercise inner_callables + generics. */", + "is_docstring": true, + "start_line": 23, + "end_line": 23, + "start_column": 1, + "end_column": 96 + } + ], + "classes": {}, + "interfaces": {}, + "enums": {}, + "type_aliases": {}, + "functions": { + "src/util.classify": { + "name": "classify", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/util.ts", + "signature": "src/util.classify", + "comments": [ + { + "content": "Generic top-level function with a nested helper, to exercise inner_callables + generics.", + "is_docstring": true, + "start_line": 23, + "end_line": 23, + "start_column": 1, + "end_column": 96 + } + ], + "decorators": [], + "parameters": [ + { + "name": "items", + "type": "T[]", + "default_value": null, + "is_optional": false, + "is_rest": false, + "is_readonly": false, + "accessibility": null, + "decorators": [], + "start_line": 24, + "end_line": 24, + "start_column": 54, + "end_column": 64 + } + ], + "type_parameters": [ + { + "name": "T", + "constraint": "{ name: string }", + "default": null + } + ], + "return_type": "Record", + "code": "export function classify(items: T[]): Record {\n function keyOf(item: T): string {\n return item.name.charAt(0);\n }\n const out: Record = {};\n for (const item of items) {\n const k = keyOf(item);\n (out[k] ??= []).push(item);\n }\n return out;\n}", + "start_line": 23, + "end_line": 34, + "code_start_line": 24, + "accessed_symbols": [], + "call_sites": [ + { + "method_name": "keyOf", + "receiver_expr": null, + "receiver_type": null, + "argument_types": [ + "T" + ], + "type_arguments": [], + "return_type": "string", + "callee_signature": "src/util.classify.keyOf", + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 30, + "end_line": 30, + "start_column": 15, + "end_column": 26 + }, + { + "method_name": "push", + "receiver_expr": "(out[k] ??= [])", + "receiver_type": "T[]", + "argument_types": [ + "T" + ], + "type_arguments": [], + "return_type": "number", + "callee_signature": null, + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 31, + "end_line": 31, + "start_column": 5, + "end_column": 31 + } + ], + "inner_callables": { + "src/util.classify.keyOf": { + "name": "keyOf", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/util.ts", + "signature": "src/util.classify.keyOf", + "comments": [], + "decorators": [], + "parameters": [ + { + "name": "item", + "type": "T", + "default_value": null, + "is_optional": false, + "is_rest": false, + "is_readonly": false, + "accessibility": null, + "decorators": [], + "start_line": 25, + "end_line": 25, + "start_column": 18, + "end_column": 25 + } + ], + "type_parameters": [], + "return_type": "string", + "code": "function keyOf(item: T): string {\n return item.name.charAt(0);\n }", + "start_line": 25, + "end_line": 27, + "code_start_line": 25, + "accessed_symbols": [], + "call_sites": [ + { + "method_name": "charAt", + "receiver_expr": "item.name", + "receiver_type": "string", + "argument_types": [ + "0" + ], + "type_arguments": [], + "return_type": "string", + "callee_signature": null, + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 26, + "end_line": 26, + "start_column": 12, + "end_column": 31 + } + ], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [], + "cyclomatic_complexity": 1, + "kind": "function", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": false, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + } + }, + "inner_classes": {}, + "local_variables": [ + { + "name": "out", + "type": "Record", + "initializer": "{}", + "value": null, + "scope": "function", + "declaration_kind": "const", + "is_readonly": true, + "is_exported": false, + "start_line": 28, + "end_line": 28, + "start_column": 9, + "end_column": 38 + }, + { + "name": "item", + "type": "T", + "initializer": null, + "value": null, + "scope": "function", + "declaration_kind": "unknown", + "is_readonly": false, + "is_exported": false, + "start_line": 29, + "end_line": 29, + "start_column": 14, + "end_column": 18 + }, + { + "name": "k", + "type": "string", + "initializer": "keyOf(item)", + "value": null, + "scope": "function", + "declaration_kind": "const", + "is_readonly": true, + "is_exported": false, + "start_line": 30, + "end_line": 30, + "start_column": 11, + "end_column": 26 + } + ], + "cyclomatic_complexity": 2, + "kind": "function", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": true, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + } + }, + "namespaces": { + "src/util.StringUtil": { + "name": "StringUtil", + "signature": "src/util.StringUtil", + "comments": [ + { + "content": "A namespace with nested declarations, to exercise the namespaces{} collection + nested signatures.", + "is_docstring": true, + "start_line": 1, + "end_line": 1, + "start_column": 1, + "end_column": 106 + } + ], + "classes": { + "src/util.StringUtil.Builder": { + "name": "Builder", + "signature": "src/util.StringUtil.Builder", + "comments": [], + "code": "export class Builder {\n private parts: string[] = [];\n add(part: string): this {\n this.parts.push(slug(part));\n return this;\n }\n build(): string {\n return this.parts.join(\"/\");\n }\n }", + "decorators": [], + "base_classes": [], + "implements_types": [], + "type_parameters": [], + "methods": { + "src/util.StringUtil.Builder.add": { + "name": "add", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/util.ts", + "signature": "src/util.StringUtil.Builder.add", + "comments": [], + "decorators": [], + "parameters": [ + { + "name": "part", + "type": "string", + "default_value": null, + "is_optional": false, + "is_rest": false, + "is_readonly": false, + "accessibility": null, + "decorators": [], + "start_line": 13, + "end_line": 13, + "start_column": 9, + "end_column": 21 + } + ], + "type_parameters": [], + "return_type": "this", + "code": "add(part: string): this {\n this.parts.push(slug(part));\n return this;\n }", + "start_line": 13, + "end_line": 16, + "code_start_line": 13, + "accessed_symbols": [], + "call_sites": [ + { + "method_name": "push", + "receiver_expr": "this.parts", + "receiver_type": "string[]", + "argument_types": [ + "string" + ], + "type_arguments": [], + "return_type": "number", + "callee_signature": null, + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 14, + "end_line": 14, + "start_column": 7, + "end_column": 34 + }, + { + "method_name": "slug", + "receiver_expr": null, + "receiver_type": null, + "argument_types": [ + "string" + ], + "type_arguments": [], + "return_type": "string", + "callee_signature": "src/util.StringUtil.slug", + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 14, + "end_line": 14, + "start_column": 23, + "end_column": 33 + } + ], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [], + "cyclomatic_complexity": 1, + "kind": "method", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": false, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + }, + "src/util.StringUtil.Builder.build": { + "name": "build", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/util.ts", + "signature": "src/util.StringUtil.Builder.build", + "comments": [], + "decorators": [], + "parameters": [], + "type_parameters": [], + "return_type": "string", + "code": "build(): string {\n return this.parts.join(\"/\");\n }", + "start_line": 17, + "end_line": 19, + "code_start_line": 17, + "accessed_symbols": [], + "call_sites": [ + { + "method_name": "join", + "receiver_expr": "this.parts", + "receiver_type": "string[]", + "argument_types": [ + "\"/\"" + ], + "type_arguments": [], + "return_type": "string", + "callee_signature": null, + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 18, + "end_line": 18, + "start_column": 14, + "end_column": 34 + } + ], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [], + "cyclomatic_complexity": 1, + "kind": "method", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": false, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + }, + "src/util.StringUtil.Builder.constructor": { + "name": "constructor", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/util.ts", + "signature": "src/util.StringUtil.Builder.constructor", + "comments": [], + "decorators": [], + "parameters": [], + "type_parameters": [], + "return_type": null, + "code": null, + "start_line": -1, + "end_line": -1, + "code_start_line": -1, + "accessed_symbols": [], + "call_sites": [], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [], + "cyclomatic_complexity": 0, + "kind": "constructor", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": false, + "is_ambient": false, + "is_implicit": true, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + } + }, + "attributes": { + "parts": { + "name": "parts", + "type": "string[]", + "comments": [], + "decorators": [], + "initializer": "[]", + "accessibility": "private", + "is_static": false, + "is_readonly": false, + "is_optional": false, + "is_abstract": false, + "start_line": 12, + "end_line": 12 + } + }, + "inner_classes": {}, + "is_abstract": false, + "is_exported": true, + "is_ambient": false, + "start_line": 11, + "end_line": 20, + "entrypoints": [] + } + }, + "interfaces": {}, + "enums": {}, + "type_aliases": {}, + "functions": { + "src/util.StringUtil.repeat": { + "name": "repeat", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/util.ts", + "signature": "src/util.StringUtil.repeat", + "comments": [], + "decorators": [], + "parameters": [ + { + "name": "s", + "type": "string", + "default_value": null, + "is_optional": false, + "is_rest": false, + "is_readonly": false, + "accessibility": null, + "decorators": [], + "start_line": 3, + "end_line": 3, + "start_column": 26, + "end_column": 35 + }, + { + "name": "n", + "type": "number", + "default_value": null, + "is_optional": false, + "is_rest": false, + "is_readonly": false, + "accessibility": null, + "decorators": [], + "start_line": 3, + "end_line": 3, + "start_column": 37, + "end_column": 46 + } + ], + "type_parameters": [], + "return_type": "string", + "code": "export function repeat(s: string, n: number): string {\n return slug(s).repeat(n);\n }", + "start_line": 3, + "end_line": 5, + "code_start_line": 3, + "accessed_symbols": [], + "call_sites": [ + { + "method_name": "repeat", + "receiver_expr": "slug(s)", + "receiver_type": "string", + "argument_types": [ + "number" + ], + "type_arguments": [], + "return_type": "string", + "callee_signature": null, + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 4, + "end_line": 4, + "start_column": 12, + "end_column": 29 + }, + { + "method_name": "slug", + "receiver_expr": null, + "receiver_type": null, + "argument_types": [ + "string" + ], + "type_arguments": [], + "return_type": "string", + "callee_signature": "src/util.StringUtil.slug", + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 4, + "end_line": 4, + "start_column": 12, + "end_column": 19 + } + ], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [], + "cyclomatic_complexity": 1, + "kind": "function", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": true, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + }, + "src/util.StringUtil.slug": { + "name": "slug", + "path": "/Users/rkrsn/workspace/codellm-devkit/codeanalyzer-ts/test/fixtures/sample-app/src/util.ts", + "signature": "src/util.StringUtil.slug", + "comments": [], + "decorators": [], + "parameters": [ + { + "name": "s", + "type": "string", + "default_value": null, + "is_optional": false, + "is_rest": false, + "is_readonly": false, + "accessibility": null, + "decorators": [], + "start_line": 7, + "end_line": 7, + "start_column": 24, + "end_column": 33 + } + ], + "type_parameters": [], + "return_type": "string", + "code": "export function slug(s: string): string {\n return s.toLowerCase().replace(/\\s+/g, \"-\");\n }", + "start_line": 7, + "end_line": 9, + "code_start_line": 7, + "accessed_symbols": [], + "call_sites": [ + { + "method_name": "replace", + "receiver_expr": "s.toLowerCase()", + "receiver_type": "string", + "argument_types": [ + "RegExp", + "\"-\"" + ], + "type_arguments": [], + "return_type": "string", + "callee_signature": null, + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 8, + "end_line": 8, + "start_column": 12, + "end_column": 48 + }, + { + "method_name": "toLowerCase", + "receiver_expr": "s", + "receiver_type": "string", + "argument_types": [], + "type_arguments": [], + "return_type": "string", + "callee_signature": null, + "is_constructor_call": false, + "is_optional_chain": false, + "start_line": 8, + "end_line": 8, + "start_column": 12, + "end_column": 27 + } + ], + "inner_callables": {}, + "inner_classes": {}, + "local_variables": [], + "cyclomatic_complexity": 1, + "kind": "function", + "accessibility": null, + "is_static": false, + "is_abstract": false, + "is_async": false, + "is_generator": false, + "is_optional": false, + "is_readonly": false, + "is_exported": true, + "is_ambient": false, + "is_implicit": false, + "accessor_kind": null, + "overload_signatures": [], + "entrypoints": [] + } + }, + "namespaces": {}, + "variables": [], + "is_exported": true, + "is_ambient": false, + "start_line": 1, + "end_line": 21 + } + }, + "variables": [], + "is_tsx": false, + "is_declaration_file": false, + "content_hash": "54886cff7e11ccadc987571f508b2aef2f324b14e63d8ad52100499851d130fa", + "last_modified": 1780493397133.355, + "file_size": 949 + } + }, + "call_graph": [ + { + "source": "src/controllers.UserController.show", + "target": "src/services.UserService.create", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": {} + }, + { + "source": "src/controllers.UserController.show", + "target": "src/models.User.describe", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": {} + }, + { + "source": "src/controllers.UserController.list", + "target": "src/services.UserService.describeAll", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": {} + }, + { + "source": "src/external.fingerprint", + "target": "node:crypto.randomUUID", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "import" + ], + "tags": { + "ts.external": "true", + "ts.module": "node:crypto" + } + }, + { + "source": "src/external.fingerprint", + "target": "node:crypto.createHash", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "import" + ], + "tags": { + "ts.external": "true", + "ts.module": "node:crypto" + } + }, + { + "source": "src/external.extensionOf", + "target": "node:path.extname", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "import" + ], + "tags": { + "ts.external": "true", + "ts.module": "node:path" + } + }, + { + "source": "src/index.main", + "target": "src/services.UserService.constructor", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": {} + }, + { + "source": "src/index.main", + "target": "src/services.UserService.create", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": {} + }, + { + "source": "src/index.main", + "target": "src/services.UserService.createGuest", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": {} + }, + { + "source": "src/index.main", + "target": "src/controllers.UserController.constructor", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": {} + }, + { + "source": "src/index.main", + "target": "src/controllers.UserController.list", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": {} + }, + { + "source": "src/index.main", + "target": "src/controllers.UserController.show", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": {} + }, + { + "source": "src/index.main", + "target": "src/services.announce", + "type": "CALL_DEP", + "weight": 2, + "provenance": [ + "tsc" + ], + "tags": {} + }, + { + "source": "src/index.main", + "target": "src/models.User.constructor", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": {} + }, + { + "source": "src/index.main", + "target": "src/models.Robot.constructor", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": {} + }, + { + "source": "src/index.main", + "target": "src/util.StringUtil.repeat", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": {} + }, + { + "source": "src/index.main", + "target": "src/util.StringUtil.Builder.constructor", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": {} + }, + { + "source": "src/index.main", + "target": "src/util.StringUtil.Builder.build", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": {} + }, + { + "source": "src/index.main", + "target": "src/util.StringUtil.Builder.add", + "type": "CALL_DEP", + "weight": 2, + "provenance": [ + "tsc" + ], + "tags": {} + }, + { + "source": "src/models.User.constructor", + "target": "src/models.Entity.constructor", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": {} + }, + { + "source": "src/services.announce", + "target": "src/models.Named.describe", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": {} + }, + { + "source": "src/services.announce", + "target": "src/models.User.describe", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": { + "ts.dispatch": "rta" + } + }, + { + "source": "src/services.announce", + "target": "src/models.Robot.describe", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": { + "ts.dispatch": "rta" + } + }, + { + "source": "src/services.UserService.create", + "target": "src/services.nextId", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": {} + }, + { + "source": "src/services.UserService.create", + "target": "src/models.User.constructor", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": {} + }, + { + "source": "src/services.UserService.createGuest", + "target": "src/services.makeGuestName", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": {} + }, + { + "source": "src/services.UserService.createGuest", + "target": "src/services.UserService.create", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": {} + }, + { + "source": "src/services.UserService.describeAll", + "target": "src/models.User.describe", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": {} + }, + { + "source": "src/services.UserService.loginAll", + "target": "src/models.User.recordLogin", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": {} + }, + { + "source": "src/util.classify", + "target": "src/util.classify.keyOf", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": {} + }, + { + "source": "src/util.StringUtil.repeat", + "target": "src/util.StringUtil.slug", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": {} + }, + { + "source": "src/util.StringUtil.Builder.add", + "target": "src/util.StringUtil.slug", + "type": "CALL_DEP", + "weight": 1, + "provenance": [ + "tsc" + ], + "tags": {} + } + ], + "external_symbols": { + "node:crypto.randomUUID": { + "name": "randomUUID", + "module": "node:crypto" + }, + "node:crypto.createHash": { + "name": "createHash", + "module": "node:crypto" + }, + "node:path.extname": { + "name": "extname", + "module": "node:path" + } + } +} \ No newline at end of file From 074615f18b74c2c81a709ba8052239ef57a4ef21 Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Mon, 27 Jul 2026 15:33:05 -0400 Subject: [PATCH 09/10] docs(typescript): note accessor-pair limitation; file parity issue (#298) get x()/set x() pairs share one TSCallable.signature, so the in-memory backend's last-writer-wins _callables map collapses the pair to a single row while the Neo4j backend (one node per accessor) surfaces two -- and duplicate decorator names diverge the same way via collect(DISTINCT ...). Invisible on the current sample-app fixture (unpaired getter only), but the live parity suite would catch it on any app with a real paired accessor. Filed as codellm-devkit/python-sdk#300; note it on the ABC and facade get_callables_overview docstrings, and record that decorator order isn't part of the cross-backend contract. --- cldk/analysis/typescript/backend.py | 6 +++++- cldk/analysis/typescript/typescript_analysis.py | 5 +++++ cldk/models/typescript/projections.py | 5 ++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/cldk/analysis/typescript/backend.py b/cldk/analysis/typescript/backend.py index 080ef089..2ba00044 100644 --- a/cldk/analysis/typescript/backend.py +++ b/cldk/analysis/typescript/backend.py @@ -254,7 +254,11 @@ def get_classes_with_decorators(self, decorators: List[str]) -> Dict[str, List[s def get_callables_overview(self) -> List[TSCallableOverview]: """A lightweight projection of every callable in the application (methods, module-level, namespace-level, and nested/inner functions), without the full :class:`TSCallable` - reconstruction.""" + reconstruction. + + Known limitation: a ``get x()``/``set x()`` accessor pair shares one ``signature``, so + this (and the other bulk accessors) can diverge between backends on a paired accessor — + see `#300 `_.""" @abstractmethod def get_method_bodies(self, signatures: List[str]) -> Dict[str, str]: diff --git a/cldk/analysis/typescript/typescript_analysis.py b/cldk/analysis/typescript/typescript_analysis.py index a49357d0..c6216ebb 100644 --- a/cldk/analysis/typescript/typescript_analysis.py +++ b/cldk/analysis/typescript/typescript_analysis.py @@ -318,6 +318,11 @@ def get_callables_overview(self) -> List[TSCallableOverview]: See Also: :meth:`get_decorated_callables`: The same projection filtered by decorator. :meth:`get_method_bodies`: Bulk source-body fetch for chosen signatures. + + Note: + A ``get x()``/``set x()`` accessor pair shares one ``signature``, so this projection + (and the other bulk accessors) can diverge between the local and Neo4j backends on a + paired accessor — see `#300 `_. """ return self.backend.get_callables_overview() diff --git a/cldk/models/typescript/projections.py b/cldk/models/typescript/projections.py index 63bd7635..a49d57b6 100644 --- a/cldk/models/typescript/projections.py +++ b/cldk/models/typescript/projections.py @@ -56,7 +56,10 @@ class TSCallableOverview(BaseModel): analyzer. path: Project-relative path of the declaring module. start_line / end_line: The callable's line span. - decorators: The decorator names applied to the callable (``TSDecorator.name`` only). + decorators: The decorator names applied to the callable (``TSDecorator.name`` only). Order + is not part of the cross-backend contract — the in-memory backend preserves source + order, the Neo4j backend aggregates with ``collect(DISTINCT ...)``, which does not + guarantee order (and also dedupes duplicate names; see #300). is_exported: Whether the callable (or its enclosing declaration) is exported. is_async: Whether the callable is declared ``async``. is_static: Whether the callable is a static class member. From 8fc8feb0330a961b6b87369429da09d674b4d4c1 Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Mon, 27 Jul 2026 15:33:10 -0400 Subject: [PATCH 10/10] chore(typescript): exact ownerless set, direct lookups, delegation test, decorator-order note (#298) - test_overview_owner_pair_is_none_for_ownerless_callables: fix the "owned_less" typo and assert the ownerless signature set exactly, not just membership. - get_method_bodies / get_callsites_for: replace the O(all callables) walk via _iter_callables() with direct _callables.get(sig) lookups keyed on the requested signatures; omission/empty-entry semantics unchanged (covering tests stay green). - test_facade_delegates_to_backend: replace the same-fixture double-call (which only proved the fixture is deterministic) with a MagicMock-backed facade asserting each bulk accessor calls the identical backend method with the identical arguments and returns its exact object. --- .../typescript/codeanalyzer/codeanalyzer.py | 18 +++++--- .../test_typescript_bulk_accessors.py | 41 ++++++++++++++----- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/cldk/analysis/typescript/codeanalyzer/codeanalyzer.py b/cldk/analysis/typescript/codeanalyzer/codeanalyzer.py index d534d303..d173ec91 100644 --- a/cldk/analysis/typescript/codeanalyzer/codeanalyzer.py +++ b/cldk/analysis/typescript/codeanalyzer/codeanalyzer.py @@ -585,10 +585,12 @@ def get_callables_overview(self) -> List[TSCallableOverview]: def get_method_bodies(self, signatures: List[str]) -> Dict[str, str]: """Return ``{signature: code}`` for the requested signatures that exist and have a body (omits callables whose ``code`` is ``None``, e.g. implicit constructors).""" - wanted = set(signatures) - return { - c.signature: c.code for c, _, _ in self._iter_callables() if c.signature in wanted and c.code is not None - } + result: Dict[str, str] = {} + for sig in signatures: + c = self._callables.get(sig) + if c is not None and c.code is not None: + result[sig] = c.code + return result def get_decorated_callables(self, markers: List[str]) -> List[TSCallableOverview]: """Return overviews of callables decorated with any of ``markers``.""" @@ -601,5 +603,9 @@ def get_decorated_callables(self, markers: List[str]) -> List[TSCallableOverview def get_callsites_for(self, signatures: List[str]) -> Dict[str, List[TSCallsite]]: """Return ``{signature: call_sites}`` for the requested signatures that exist.""" - wanted = set(signatures) - return {c.signature: list(c.call_sites) for c, _, _ in self._iter_callables() if c.signature in wanted} + result: Dict[str, List[TSCallsite]] = {} + for sig in signatures: + c = self._callables.get(sig) + if c is not None: + result[sig] = list(c.call_sites) + return result diff --git a/tests/analysis/typescript/test_typescript_bulk_accessors.py b/tests/analysis/typescript/test_typescript_bulk_accessors.py index 5c151772..9a8fdf0d 100644 --- a/tests/analysis/typescript/test_typescript_bulk_accessors.py +++ b/tests/analysis/typescript/test_typescript_bulk_accessors.py @@ -32,6 +32,7 @@ from cldk import CLDK from cldk.analysis import AnalysisLevel from cldk.analysis.commons.backend_config import CodeAnalyzerConfig +from cldk.analysis.typescript.typescript_analysis import TypeScriptAnalysis from cldk.models.typescript import TSCallableOverview @@ -101,8 +102,10 @@ def test_overview_enumerates_every_callable_including_inner(ts_analysis): assert "src/util.classify.keyOf" in signatures # inner callable is enumerated -def test_overview_owner_pair_is_none_for_owned_less_callables(ts_analysis): - rows = by_signature(ts_analysis.get_callables_overview()) +def test_overview_owner_pair_is_none_for_ownerless_callables(ts_analysis): + overview = ts_analysis.get_callables_overview() + assert {o.signature for o in overview if o.owner_signature is None} == OWNERLESS_SIGNATURES + rows = by_signature(overview) for sig in OWNERLESS_SIGNATURES: assert rows[sig].owner_signature is None, sig assert rows[sig].owner_kind is None, sig @@ -225,12 +228,28 @@ def test_callsites_for_exact_per_signature_lists_and_empty_entry(ts_analysis): # -----[ facade delegates to the same backend objects ]----- -def test_facade_delegates_return_backend_objects(ts_analysis): - assert ts_analysis.get_callables_overview() == ts_analysis.backend.get_callables_overview() - assert ts_analysis.get_method_bodies(["src/services.UserService.create"]) == ts_analysis.backend.get_method_bodies( - ["src/services.UserService.create"] - ) - assert ts_analysis.get_decorated_callables(["Get"]) == ts_analysis.backend.get_decorated_callables(["Get"]) - assert ts_analysis.get_callsites_for(["src/services.UserService.create"]) == ts_analysis.backend.get_callsites_for( - ["src/services.UserService.create"] - ) +def test_facade_delegates_to_backend(): + """The facade is a thin pass-through: each bulk accessor must call the identical backend + method with the identical arguments and return its exact object, unmodified.""" + facade = object.__new__(TypeScriptAnalysis) + facade.backend = MagicMock() + + overview_sentinel = object() + facade.backend.get_callables_overview.return_value = overview_sentinel + assert facade.get_callables_overview() is overview_sentinel + facade.backend.get_callables_overview.assert_called_once_with() + + bodies_sentinel = object() + facade.backend.get_method_bodies.return_value = bodies_sentinel + assert facade.get_method_bodies(["src/services.UserService.create"]) is bodies_sentinel + facade.backend.get_method_bodies.assert_called_once_with(["src/services.UserService.create"]) + + decorated_sentinel = object() + facade.backend.get_decorated_callables.return_value = decorated_sentinel + assert facade.get_decorated_callables(["Get"]) is decorated_sentinel + facade.backend.get_decorated_callables.assert_called_once_with(["Get"]) + + callsites_sentinel = object() + facade.backend.get_callsites_for.return_value = callsites_sentinel + assert facade.get_callsites_for(["src/services.UserService.create"]) is callsites_sentinel + facade.backend.get_callsites_for.assert_called_once_with(["src/services.UserService.create"])