|
| 1 | +################################################################################ |
| 2 | +# Copyright IBM Corporation 2026 |
| 3 | +# |
| 4 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | +# you may not use this file except in compliance with the License. |
| 6 | +# You may obtain a copy of the License at |
| 7 | +# |
| 8 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +# |
| 10 | +# Unless required by applicable law or agreed to in writing, software |
| 11 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | +# See the License for the specific language governing permissions and |
| 14 | +# limitations under the License. |
| 15 | +################################################################################ |
| 16 | + |
| 17 | +"""The TypeScript analysis backend contract. |
| 18 | +
|
| 19 | +:class:`TypeScriptAnalysis` is a thin façade that delegates every query to a *backend*. Two |
| 20 | +interchangeable backends exist: |
| 21 | +
|
| 22 | +* :class:`~cldk.analysis.typescript.codeanalyzer.TSCodeanalyzer` — walks the in-memory pydantic |
| 23 | + ``TSApplication`` / a NetworkX call graph built from ``analysis.json``; |
| 24 | +* :class:`~cldk.analysis.typescript.neo4j.TSNeo4jBackend` — answers the *same* queries with |
| 25 | + Cypher over the graph ``codeanalyzer-typescript`` emits with ``--emit neo4j``. |
| 26 | +
|
| 27 | +This ABC formalizes the surface those two share so the façade↔backend relationship is enforced by |
| 28 | +the type system (and at instantiation time) instead of matching only by convention. Both backends |
| 29 | +subclass it; the façade is typed against it. Backend-specific lifecycle (e.g. the Neo4j driver's |
| 30 | +``close()`` / context-manager support) is intentionally *not* part of the contract. |
| 31 | +
|
| 32 | +The vocabulary mirrors :class:`~cldk.analysis.java.codeanalyzer.JCodeanalyzer` / |
| 33 | +:class:`~cldk.analysis.python.codeanalyzer.PyCodeanalyzer`, but the node kinds are TypeScript-native |
| 34 | +(interfaces, type aliases, enums, namespaces, decorators, ...). |
| 35 | +""" |
| 36 | + |
| 37 | +from __future__ import annotations |
| 38 | + |
| 39 | +from abc import ABC, abstractmethod |
| 40 | +from typing import Dict, List, Set, Tuple |
| 41 | + |
| 42 | +import networkx as nx |
| 43 | + |
| 44 | +from cldk.models.typescript import ( |
| 45 | + TSApplication, |
| 46 | + TSCallable, |
| 47 | + TSCallsite, |
| 48 | + TSClass, |
| 49 | + TSClassAttribute, |
| 50 | + TSDecorator, |
| 51 | + TSEnum, |
| 52 | + TSEnumMember, |
| 53 | + TSExport, |
| 54 | + TSExternalSymbol, |
| 55 | + TSImport, |
| 56 | + TSInterface, |
| 57 | + TSModule, |
| 58 | + TSTypeAlias, |
| 59 | + TSVariableDeclaration, |
| 60 | +) |
| 61 | + |
| 62 | + |
| 63 | +class TSAnalysisBackend(ABC): |
| 64 | + """Abstract base every TypeScript analysis backend implements. |
| 65 | +
|
| 66 | + A backend owns *all* indexing and query logic for a TypeScript application; the |
| 67 | + :class:`TypeScriptAnalysis` façade is a one-line-delegation shim over it. Implementations must |
| 68 | + return the canonical ``cldk.models.typescript`` pydantic objects (or the documented |
| 69 | + NetworkX / dict / list shapes) so the two backends are behaviorally interchangeable. |
| 70 | + """ |
| 71 | + |
| 72 | + # -----[ application / whole-program ]----- |
| 73 | + @abstractmethod |
| 74 | + def get_application(self) -> TSApplication: |
| 75 | + """The whole application view (symbol table + call graph + external symbols).""" |
| 76 | + |
| 77 | + @abstractmethod |
| 78 | + def get_symbol_table(self) -> Dict[str, TSModule]: |
| 79 | + """The per-file symbol table, keyed by module file path.""" |
| 80 | + |
| 81 | + @abstractmethod |
| 82 | + def get_modules(self) -> List[TSModule]: |
| 83 | + """All modules (compilation units).""" |
| 84 | + |
| 85 | + @abstractmethod |
| 86 | + def get_external_symbols(self) -> Dict[str, TSExternalSymbol]: |
| 87 | + """Phantom (external) call targets — imported/required library members.""" |
| 88 | + |
| 89 | + @abstractmethod |
| 90 | + def get_typescript_file(self, qualified_name: str) -> str | None: |
| 91 | + """The file path declaring the symbol with the given signature.""" |
| 92 | + |
| 93 | + @abstractmethod |
| 94 | + def get_typescript_module(self, file_path: str) -> TSModule | None: |
| 95 | + """The module for a file path.""" |
| 96 | + |
| 97 | + # -----[ call graph ]----- |
| 98 | + @abstractmethod |
| 99 | + def get_call_graph(self) -> nx.DiGraph: |
| 100 | + """NetworkX DiGraph of callable signatures (and phantom external symbols) + call edges.""" |
| 101 | + |
| 102 | + @abstractmethod |
| 103 | + def get_call_graph_json(self) -> str: |
| 104 | + """The application serialized as JSON.""" |
| 105 | + |
| 106 | + @abstractmethod |
| 107 | + def get_all_callers(self, target_class_name: str, target_method_declaration: str | None = None) -> Dict: |
| 108 | + """Callers of a method, with the connecting call-graph edge metadata.""" |
| 109 | + |
| 110 | + @abstractmethod |
| 111 | + def get_all_callees(self, source_class_name: str, source_method_declaration: str | None = None) -> Dict: |
| 112 | + """Callees of a method, with the connecting call-graph edge metadata.""" |
| 113 | + |
| 114 | + @abstractmethod |
| 115 | + def get_class_call_graph(self, qualified_class_name: str, method_signature: str | None = None) -> List[Tuple[str, str]]: |
| 116 | + """Call-graph edges reachable from a class (or one of its methods).""" |
| 117 | + |
| 118 | + @abstractmethod |
| 119 | + def get_class_hierarchy(self) -> nx.DiGraph: |
| 120 | + """Inheritance/implementation graph: an edge child → base for every base class.""" |
| 121 | + |
| 122 | + # -----[ call sites ]----- |
| 123 | + @abstractmethod |
| 124 | + def get_call_sites(self, qualified_callable_name: str) -> List[TSCallsite]: |
| 125 | + """The rich, syntactic call sites inside a callable.""" |
| 126 | + |
| 127 | + @abstractmethod |
| 128 | + def get_calling_lines(self, target_signature: str) -> List[int]: |
| 129 | + """Sorted source lines anywhere in the project where ``target_signature`` is invoked.""" |
| 130 | + |
| 131 | + @abstractmethod |
| 132 | + def get_call_targets(self, source_signature: str) -> Set[str]: |
| 133 | + """The call targets invoked from a callable, derived from its call sites.""" |
| 134 | + |
| 135 | + # -----[ classes / interfaces / enums / type-aliases ]----- |
| 136 | + @abstractmethod |
| 137 | + def get_all_classes(self) -> Dict[str, TSClass]: |
| 138 | + """Every class, keyed by signature.""" |
| 139 | + |
| 140 | + @abstractmethod |
| 141 | + def get_class(self, qualified_class_name: str) -> TSClass | None: |
| 142 | + """A single class by signature.""" |
| 143 | + |
| 144 | + @abstractmethod |
| 145 | + def get_all_interfaces(self) -> Dict[str, TSInterface]: |
| 146 | + """Every interface, keyed by signature.""" |
| 147 | + |
| 148 | + @abstractmethod |
| 149 | + def get_all_enums(self) -> Dict[str, TSEnum]: |
| 150 | + """Every enum, keyed by signature.""" |
| 151 | + |
| 152 | + @abstractmethod |
| 153 | + def get_enum_members(self, qualified_enum_name: str) -> List[TSEnumMember]: |
| 154 | + """The members of an enum.""" |
| 155 | + |
| 156 | + @abstractmethod |
| 157 | + def get_all_type_aliases(self) -> Dict[str, TSTypeAlias]: |
| 158 | + """Every type alias, keyed by signature.""" |
| 159 | + |
| 160 | + @abstractmethod |
| 161 | + def get_all_nested_classes(self, qualified_class_name: str) -> List[TSClass]: |
| 162 | + """The classes declared inside a class.""" |
| 163 | + |
| 164 | + @abstractmethod |
| 165 | + def get_all_sub_classes(self, qualified_class_name: str) -> Dict[str, TSClass]: |
| 166 | + """Classes that extend/implement the given class.""" |
| 167 | + |
| 168 | + @abstractmethod |
| 169 | + def get_extended_classes(self, qualified_class_name: str) -> List[str]: |
| 170 | + """The base types a class extends (base classes minus implemented interfaces).""" |
| 171 | + |
| 172 | + @abstractmethod |
| 173 | + def get_implemented_interfaces(self, qualified_class_name: str) -> List[str]: |
| 174 | + """The interfaces a class implements.""" |
| 175 | + |
| 176 | + # -----[ methods / functions / fields ]----- |
| 177 | + @abstractmethod |
| 178 | + def get_all_methods_in_application(self) -> Dict[str, Dict[str, TSCallable]]: |
| 179 | + """All methods grouped by their owning class/interface signature.""" |
| 180 | + |
| 181 | + @abstractmethod |
| 182 | + def get_all_methods_in_class(self, qualified_class_name: str) -> Dict[str, TSCallable]: |
| 183 | + """The methods of a class/interface, keyed by short name.""" |
| 184 | + |
| 185 | + @abstractmethod |
| 186 | + def get_method(self, qualified_class_name: str, qualified_method_name: str) -> TSCallable | None: |
| 187 | + """A single method of a class/interface.""" |
| 188 | + |
| 189 | + @abstractmethod |
| 190 | + def get_method_parameters(self, qualified_class_name: str, qualified_method_name: str) -> List[str]: |
| 191 | + """The parameter names of a method.""" |
| 192 | + |
| 193 | + @abstractmethod |
| 194 | + def get_all_constructors(self, qualified_class_name: str) -> Dict[str, TSCallable]: |
| 195 | + """The constructors of a class.""" |
| 196 | + |
| 197 | + @abstractmethod |
| 198 | + def get_all_functions(self) -> Dict[str, TSCallable]: |
| 199 | + """Top-level (module/namespace) functions, keyed by signature.""" |
| 200 | + |
| 201 | + @abstractmethod |
| 202 | + def get_all_fields(self, qualified_class_name: str) -> List[TSClassAttribute]: |
| 203 | + """The attributes/fields of a class.""" |
| 204 | + |
| 205 | + @abstractmethod |
| 206 | + def get_interface_properties(self, qualified_interface_name: str) -> List[TSClassAttribute]: |
| 207 | + """The properties of an interface.""" |
| 208 | + |
| 209 | + # -----[ imports / exports / variables ]----- |
| 210 | + @abstractmethod |
| 211 | + def get_imports(self) -> Dict[str, List[TSImport]]: |
| 212 | + """Per-file import bindings.""" |
| 213 | + |
| 214 | + @abstractmethod |
| 215 | + def get_all_exports(self) -> Dict[str, List[TSExport]]: |
| 216 | + """Per-file export bindings.""" |
| 217 | + |
| 218 | + @abstractmethod |
| 219 | + def get_all_variables(self) -> Dict[str, List[TSVariableDeclaration]]: |
| 220 | + """Per-file module-level variable declarations.""" |
| 221 | + |
| 222 | + # -----[ decorators ]----- |
| 223 | + @abstractmethod |
| 224 | + def get_decorators(self, qualified_callable_name: str) -> List[TSDecorator]: |
| 225 | + """Structured decorators applied to a callable.""" |
| 226 | + |
| 227 | + @abstractmethod |
| 228 | + def get_class_decorators(self, qualified_class_name: str) -> List[TSDecorator]: |
| 229 | + """Structured decorators applied to a class.""" |
| 230 | + |
| 231 | + @abstractmethod |
| 232 | + def get_methods_with_decorators(self, decorators: List[str]) -> Dict[str, List[str]]: |
| 233 | + """Map each requested decorator name to the signatures of callables carrying it.""" |
| 234 | + |
| 235 | + @abstractmethod |
| 236 | + def get_classes_with_decorators(self, decorators: List[str]) -> Dict[str, List[str]]: |
| 237 | + """Map each requested decorator name to the signatures of classes carrying it.""" |
0 commit comments