Skip to content

Commit d33e375

Browse files
committed
TSNeo4jBackend (neo4j_backend.py) is now a pure read-only Cypher client
Signed-off-by: Rahul Krishna <rkrsn@ibm.com>
1 parent 0a4f6da commit d33e375

6 files changed

Lines changed: 250 additions & 155 deletions

File tree

cldk/analysis/typescript/neo4j/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@
1818

1919
from cldk.analysis.typescript.neo4j.config import Neo4jConnectionConfig
2020
from cldk.analysis.typescript.neo4j.neo4j_backend import TSNeo4jBackend
21+
from cldk.analysis.typescript.neo4j.neo4j_ingestor import TSNeo4jIngestor
2122

22-
__all__ = ["TSNeo4jBackend", "Neo4jConnectionConfig"]
23+
__all__ = ["TSNeo4jBackend", "TSNeo4jIngestor", "Neo4jConnectionConfig"]

cldk/analysis/typescript/neo4j/config.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
@dataclass
2525
class Neo4jConnectionConfig:
26-
"""How :class:`TSNeo4jBackend` reaches (and, optionally, populates) the graph database.
26+
"""How the TypeScript facade reaches (and, optionally, populates) the graph database.
2727
2828
Attributes:
2929
uri: Bolt URI of the Neo4j server (e.g. ``bolt://localhost:7687``).
@@ -33,9 +33,12 @@ class Neo4jConnectionConfig:
3333
application_name: The ``:Application`` anchor name to scope queries to. Defaults to the
3434
analyzed project directory's name (matching ``codeanalyzer-typescript``'s
3535
``--app-name`` default).
36-
build_db: If True (default), populate the database from the project on construction by
37-
running ``codeanalyzer-typescript --emit neo4j``. Set False to query a DB that is
38-
already loaded.
36+
build_db: If True (default), first populate the database from the project — via
37+
:class:`~cldk.analysis.typescript.neo4j.TSNeo4jIngestor` running
38+
``codeanalyzer-typescript --emit neo4j`` — then query it. This is a local/dev
39+
convenience that needs the analyzer binary and the sources on disk. Set False for a
40+
cloud deployment where the graph is loaded out of band and the SDK only polls it
41+
(read-only, no binary, ``project_dir`` may be None).
3942
"""
4043

4144
uri: str

cldk/analysis/typescript/neo4j/neo4j_backend.py

Lines changed: 22 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,24 @@
1414
# limitations under the License.
1515
################################################################################
1616

17-
"""Neo4j-backed TypeScript analysis backend.
17+
"""Neo4j-backed TypeScript analysis backend (read-only Cypher client).
1818
1919
A drop-in alternative to :class:`TSCodeanalyzer`: it exposes the **same query
2020
method surface** (``get_all_classes``, ``get_call_graph``, ``get_all_callers``,
2121
...) so the :class:`TypeScriptAnalysis` facade can delegate to either one, but
2222
every method answers by running **Cypher over a live Neo4j graph** instead of
2323
walking the in-memory pydantic/NetworkX structures.
2424
25+
This class is purely a **query client**: it never builds the graph and has no
26+
dependency on the ``codeanalyzer-typescript`` binary or the project sources. It
27+
assumes the database is already populated and just polls it — the shape a cloud
28+
deployment wants, where a third-party job (e.g. inside Kubernetes) loads the
29+
graph out of band and the SDK only reads it.
30+
2531
The graph is the one ``codeanalyzer-typescript`` emits with ``--emit neo4j``
26-
(schema: ``codeanalyzer-ts/schema.neo4j.json``). On construction this backend can
27-
populate the database for you by shelling out to the analyzer binary with
28-
``--emit neo4j --neo4j-uri ...`` (mirroring how :class:`TSCodeanalyzer` shells
29-
out to produce ``analysis.json``); or you can point it at an already-loaded DB
30-
with ``build_db=False``.
32+
(schema: ``codeanalyzer-ts/schema.neo4j.json``). Populating it is the job of
33+
:class:`~cldk.analysis.typescript.neo4j.TSNeo4jIngestor` (a local/dev
34+
convenience), not of this backend.
3135
3236
Identity model (must match the in-memory backend):
3337
@@ -50,17 +54,11 @@
5054
from __future__ import annotations
5155

5256
import logging
53-
import os
54-
import shlex
55-
import subprocess
5657
from collections import deque
57-
from importlib import resources
58-
from pathlib import Path
59-
from typing import Any, Dict, List, Set, Tuple, Union
58+
from typing import Any, Dict, List, Set, Tuple
6059

6160
import networkx as nx
6261

63-
from cldk.analysis import AnalysisLevel
6462
from cldk.analysis.typescript.backend import TSAnalysisBackend
6563
from cldk.analysis.typescript.neo4j import reconstruct as R
6664
from cldk.models.typescript import (
@@ -87,61 +85,38 @@
8785

8886

8987
class TSNeo4jBackend(TSAnalysisBackend):
90-
"""Build and query the application view of a TypeScript project over Neo4j (Cypher).
88+
"""Query the application view of a TypeScript project over Neo4j (Cypher), read-only.
89+
90+
The graph must already be loaded — by :class:`TSNeo4jIngestor` on a dev machine, or by a
91+
third-party job in a cloud deployment. This backend never writes and needs neither the
92+
``codeanalyzer-typescript`` binary nor the project sources on disk.
9193
9294
Args:
93-
project_dir: Root of the TypeScript project (required when ``build_db`` is True).
94-
analysis_backend_path: Directory containing the ``codeanalyzer-typescript`` binary. If
95-
None, falls back to ``$CODEANALYZER_TS_BIN``, then the ``codeanalyzer_typescript``
96-
wheel, then a bundled binary.
97-
analysis_level: ``AnalysisLevel.symbol_table`` (1) or ``AnalysisLevel.call_graph`` (2).
98-
eager_analysis: If True, force a clean rebuild of the graph even if this application's
99-
``:Application`` anchor already exists in the database.
100-
target_files: Restrict analysis to these files (incremental push).
10195
neo4j_uri: Bolt URI of the Neo4j server (e.g. ``bolt://localhost:7687``).
102-
neo4j_username / neo4j_password: Credentials.
96+
neo4j_username / neo4j_password: Credentials (read-only is sufficient).
10397
neo4j_database: Database name (None ⇒ server default).
104-
application_name: The ``:Application`` anchor name. Defaults to the project directory
105-
name, matching ``codeanalyzer-typescript``'s ``--app-name`` default.
106-
build_db: If True (default), populate the database from ``project_dir`` on construction.
107-
If False, query an already-loaded graph (``project_dir`` may be None).
98+
application_name: The ``:Application`` anchor name to scope every query to. Matches the
99+
``--app-name`` the graph was loaded with (defaults to the project directory name).
108100
"""
109101

110102
def __init__(
111103
self,
112-
project_dir: Union[str, Path, None],
113-
analysis_backend_path: Union[str, Path, None],
114-
analysis_level: str,
115-
eager_analysis: bool,
116-
target_files: List[str] | None,
117104
neo4j_uri: str,
118105
neo4j_username: str,
119106
neo4j_password: str,
120107
neo4j_database: str | None = None,
121108
application_name: str | None = None,
122-
build_db: bool = True,
123109
) -> None:
124110
try:
125111
from neo4j import GraphDatabase
126112
except ModuleNotFoundError as e: # pragma: no cover - import guard
127113
raise CodeanalyzerExecutionException("The Neo4j backend requires the 'neo4j' driver. Install it with " "`pip install neo4j` (or `pip install cldk[neo4j]`).") from e
128114

129-
self.project_dir = project_dir
130-
self.analysis_backend_path = analysis_backend_path
131-
self.analysis_level = analysis_level
132-
self.eager_analysis = eager_analysis
133-
self.target_files = target_files
134-
self.application_name = application_name or (Path(project_dir).name if project_dir else None)
135-
if not self.application_name:
136-
raise CodeanalyzerExecutionException("application_name could not be inferred; pass application_name explicitly when project_dir is None.")
115+
if not application_name:
116+
raise CodeanalyzerExecutionException("application_name is required to scope queries to an application.")
117+
self.application_name = application_name
137118
self._database = neo4j_database
138119
self._driver = GraphDatabase.driver(neo4j_uri, auth=(neo4j_username, neo4j_password))
139-
self._neo4j_conn = (neo4j_uri, neo4j_username, neo4j_password)
140-
141-
if build_db:
142-
if project_dir is None:
143-
raise CodeanalyzerExecutionException("project_dir is required when build_db=True.")
144-
self._build_graph()
145120

146121
# The application's module file_keys, used to scope every query to this app.
147122
self._modules: List[str] = self._load_module_keys()
@@ -169,88 +144,6 @@ def _load_module_keys(self) -> List[str]:
169144
)
170145
return [r["k"] for r in rows]
171146

172-
# -----[ binary resolution + DB population ]-----
173-
def _get_codeanalyzer_exec(self) -> List[str]:
174-
"""Resolve the codeanalyzer-typescript executable command (mirrors TSCodeanalyzer)."""
175-
if self.analysis_backend_path:
176-
backend = Path(self.analysis_backend_path)
177-
binary = next(
178-
(p for p in backend.rglob("codeanalyzer-typescript*") if p.is_file()),
179-
None,
180-
) or next((p for p in backend.rglob("codeanalyzer-ts*") if p.is_file()), None)
181-
if binary is None:
182-
raise CodeanalyzerExecutionException("codeanalyzer-typescript binary not found in the provided analysis_backend_path.")
183-
return [str(binary)]
184-
185-
env_bin = os.environ.get("CODEANALYZER_TS_BIN")
186-
if env_bin:
187-
return shlex.split(env_bin)
188-
189-
try:
190-
import codeanalyzer_typescript
191-
192-
return [str(codeanalyzer_typescript.bin_path())]
193-
except (ModuleNotFoundError, FileNotFoundError):
194-
pass
195-
196-
try:
197-
with resources.as_file(resources.files("cldk.analysis.typescript.codeanalyzer.bin")) as bin_dir:
198-
binary = next((p for p in bin_dir.iterdir() if p.is_file() and p.name.startswith("codeanalyzer")), None)
199-
if binary is not None:
200-
return [str(binary)]
201-
except (ModuleNotFoundError, FileNotFoundError):
202-
pass
203-
204-
raise CodeanalyzerExecutionException(
205-
"codeanalyzer-typescript binary not found. Pass analysis_backend_path=<dir containing the "
206-
"binary>, set $CODEANALYZER_TS_BIN, or bundle it under cldk/analysis/typescript/codeanalyzer/bin/."
207-
)
208-
209-
def _build_graph(self) -> None:
210-
"""Push this project's graph into Neo4j via ``--emit neo4j --neo4j-uri`` (Bolt).
211-
212-
Lazy by default: if the ``:Application`` anchor already exists and ``eager_analysis`` is
213-
False (and this is not a targeted/incremental run), the push is skipped.
214-
"""
215-
if not self.eager_analysis and not self.target_files and self._application_exists():
216-
logger.info("Neo4j already has application '%s'; skipping rebuild (lazy).", self.application_name)
217-
return
218-
219-
uri, user, password = self._neo4j_conn
220-
level = 1 if self.analysis_level == AnalysisLevel.symbol_table else 2
221-
args = self._get_codeanalyzer_exec() + [
222-
"-i",
223-
str(Path(self.project_dir)),
224-
"-a",
225-
str(level),
226-
"--emit",
227-
"neo4j",
228-
"--neo4j-uri",
229-
uri,
230-
"--neo4j-user",
231-
user,
232-
"--neo4j-password",
233-
password,
234-
"--app-name",
235-
self.application_name,
236-
]
237-
if self._database:
238-
args += ["--neo4j-database", self._database]
239-
if self.eager_analysis:
240-
args += ["--eager"]
241-
for tf in self.target_files or []:
242-
args += ["-t", str(tf).strip()]
243-
244-
try:
245-
logger.info("Running codeanalyzer-typescript (neo4j emit): %s", " ".join(args))
246-
subprocess.run(args, capture_output=True, text=True, check=True)
247-
except Exception as e: # noqa: BLE001
248-
raise CodeanalyzerExecutionException(str(e)) from e
249-
250-
def _application_exists(self) -> bool:
251-
rows = self._run("MATCH (a:Application {name: $app}) RETURN count(a) AS c", app=self.application_name)
252-
return bool(rows and rows[0]["c"] > 0)
253-
254147
# -----[ child-fetch helpers (reconstruction) ]-----
255148
def _decorators_of(self, signature: str) -> List[TSDecorator]:
256149
rows = self._run(

0 commit comments

Comments
 (0)