Skip to content

Commit a2d735d

Browse files
authored
fix(typescript): reconstruct External nodes per the slim TSExternalSymbol model (#231) (#265)
1 parent 8cf5297 commit a2d735d

2 files changed

Lines changed: 65 additions & 2 deletions

File tree

cldk/analysis/typescript/neo4j/reconstruct.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,12 @@ def enum_member(name: str, value: str | None) -> TSEnumMember:
173173

174174

175175
def external(props: Props) -> TSExternalSymbol:
176+
# Slim by design (#231): the model carries name+module only. The graph node's
177+
# ``signature`` is its merge key and becomes the ``external_symbols`` map key at the
178+
# call site; ``kind`` does not exist in the analyzer's published Neo4j schema.
176179
return TSExternalSymbol(
177-
signature=props.get("signature", ""),
178180
name=props.get("name", ""),
179181
module=props.get("module", ""),
180-
kind=props.get("kind", "unknown"),
181182
)
182183

183184

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
################################################################################
2+
# Copyright IBM Corporation 2024, 2025
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+
"""Regression tests for #231: reconstructing External (phantom) nodes from Neo4j
18+
properties must conform to the slim TSExternalSymbol model (name + module only) —
19+
the graph node legitimately carries signature/kind properties, and the
20+
reconstructor must not forward them into an extra='forbid' model."""
21+
22+
from unittest.mock import patch
23+
24+
from cldk.analysis.typescript.neo4j import reconstruct as R
25+
from cldk.analysis.typescript.neo4j.neo4j_backend import TSNeo4jBackend
26+
from cldk.models.typescript import TSExternalSymbol
27+
28+
29+
def test_external_reconstructs_from_full_graph_props():
30+
"""A real External node's property bag (signature, kind, _module included) reconstructs."""
31+
sym = R.external(
32+
{
33+
"signature": "commander.parse",
34+
"name": "parse",
35+
"module": "commander",
36+
"kind": "external",
37+
"_module": "app",
38+
}
39+
)
40+
assert isinstance(sym, TSExternalSymbol)
41+
assert sym.name == "parse"
42+
assert sym.module == "commander"
43+
44+
45+
def test_external_reconstructs_with_empty_signature():
46+
"""The reported repro: an empty-signature External node must not raise."""
47+
sym = R.external({"signature": "", "name": "", "module": "", "kind": "unknown"})
48+
assert isinstance(sym, TSExternalSymbol)
49+
50+
51+
def test_get_external_symbols_end_to_end_via_stubbed_run():
52+
"""Backend-level: get_external_symbols reconstructs every returned row, keyed by signature."""
53+
rows = [
54+
{"p": {"signature": "commander.parse", "name": "parse", "module": "commander", "kind": "external"}},
55+
{"p": {"signature": "fs.readFileSync", "name": "readFileSync", "module": "fs", "kind": "external"}},
56+
]
57+
backend = TSNeo4jBackend.__new__(TSNeo4jBackend)
58+
backend._modules = ["app"]
59+
with patch.object(TSNeo4jBackend, "_run", return_value=rows):
60+
out = backend.get_external_symbols()
61+
assert set(out) == {"commander.parse", "fs.readFileSync"}
62+
assert out["commander.parse"].module == "commander"

0 commit comments

Comments
 (0)