Skip to content

Commit 266d7b5

Browse files
authored
fix(neo4j): carry body_span, type parameters, leaf spans and call-site facts (#258)
The sweep that followed #255 walked every analysis.json `$def` against V2GraphProjector and found nine more fields the payload emits and the graph declares nowhere. This is the mechanical half: additive properties, no new node family, no comment model, no schema_version move. - `:JCallable` gains the body block's own offsets under a `body_` prefix (`body_start_line` … `body_end_byte`). `span` covers the whole declaration, so before this the graph could not tell a signature from the body it encloses, and a body-less declaration read the same as one whose body was not recorded. A prefix rather than a second node: a body block has no identity, it is a second pair of offsets into the same file. Absent, not zeroed, when there is no body. - `:JType` and `:JCallable` gain `type_parameters_json`. A type parameter carries a name, resolved bounds, a span and its own annotations, so it does not flatten to a scalar — same treatment as `parameters_json`, which is the precedent for exactly this shape. - The enum-constant and record-component blocks now call `putLines` and `annotate`, as `projectField` already did. Both leaf labels join the `J_ANNOTATED_BY` sources. - `J_ANNOTATED_BY` carries the application site's span, on a keyed edge. Java annotations are repeatable, so one (owner, annotation) endpoint pair legitimately occurs twice on one declaration; a plain MERGE collapsed the two applications onto one relationship and kept only the last span and argument list. `_k` is the span key. - `:JBodyNode` gains `callee_signature` (the erased signature of the resolved callee, which is what distinguishes overloads at a call site) and `arguments`. `arguments` stays body-local rather than being mapped through `globalOrdinal` the way `call_node` is, which is what #256 specified. The conformance assertion for that mapping failed on the first run: an argument id names an argument *expression*, and an expression is a body node only when it is itself a call site. On daytrader8, 537 of 3666 argument ids name a body node, so the mapping would have handed out 3129 `:JBodyNode` ids resolving to nothing. Carrying the canonical local ids verbatim keeps the fact exact and positionally aligned with `argument_expr` and `argument_types`; a consumer that wants the node for a nested call composes it the same way this projection does. Every field is asserted against the analysis.json value per node, not by presence, and the body offsets are asserted to slice `:JModule.source` to text that starts with `{` and ends with `}`. The enum, record, annotation and generics assertions run over `enum-record-bodies-test` and `generics-varargs-duplicate-signature-test`: the L4 fixture declares no enum, no record, no annotation and no generic, which is how these drops survived every existing assertion. Closes #256
1 parent 2999e44 commit 266d7b5

4 files changed

Lines changed: 419 additions & 24 deletions

File tree

‎schema.neo4j.json‎

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"base_types": "string[]",
4545
"interfaces": "string[]",
4646
"docstring": "string",
47+
"type_parameters_json": "string",
4748
"is_entrypoint": "boolean",
4849
"entrypoint_frameworks": "string[]",
4950
"start_line": "integer",
@@ -76,12 +77,19 @@
7677
"is_implicit": "boolean",
7778
"is_entrypoint": "boolean",
7879
"entrypoint_frameworks": "string[]",
80+
"type_parameters_json": "string",
7981
"start_line": "integer",
8082
"start_column": "integer",
8183
"end_line": "integer",
8284
"end_column": "integer",
8385
"start_byte": "integer",
84-
"end_byte": "integer"
86+
"end_byte": "integer",
87+
"body_start_line": "integer",
88+
"body_start_column": "integer",
89+
"body_end_line": "integer",
90+
"body_end_column": "integer",
91+
"body_start_byte": "integer",
92+
"body_end_byte": "integer"
8593
}
8694
},
8795
{
@@ -139,7 +147,13 @@
139147
"id": "string",
140148
"name": "string",
141149
"arguments": "string[]",
142-
"docstring": "string"
150+
"docstring": "string",
151+
"start_line": "integer",
152+
"start_column": "integer",
153+
"end_line": "integer",
154+
"end_column": "integer",
155+
"start_byte": "integer",
156+
"end_byte": "integer"
143157
}
144158
},
145159
{
@@ -152,7 +166,13 @@
152166
"type": "string",
153167
"modifiers": "string[]",
154168
"is_variadic": "boolean",
155-
"docstring": "string"
169+
"docstring": "string",
170+
"start_line": "integer",
171+
"start_column": "integer",
172+
"end_line": "integer",
173+
"end_column": "integer",
174+
"start_byte": "integer",
175+
"end_byte": "integer"
156176
}
157177
},
158178
{
@@ -171,6 +191,8 @@
171191
"is_static_call": "boolean",
172192
"argument_types": "string[]",
173193
"argument_expr": "string[]",
194+
"callee_signature": "string",
195+
"arguments": "string[]",
174196
"var": "string",
175197
"call_node": "string",
176198
"start_line": "integer",
@@ -392,13 +414,22 @@
392414
"from": [
393415
"JType",
394416
"JCallable",
395-
"JField"
417+
"JField",
418+
"JEnumConstant",
419+
"JRecordComponent"
396420
],
397421
"to": [
398422
"JAnnotation"
399423
],
400424
"properties": {
401-
"arguments": "string[]"
425+
"arguments": "string[]",
426+
"_k": "string",
427+
"start_line": "integer",
428+
"start_column": "integer",
429+
"end_line": "integer",
430+
"end_column": "integer",
431+
"start_byte": "integer",
432+
"end_byte": "integer"
402433
}
403434
},
404435
{

‎src/main/java/com/ibm/cldk/neo4j/V2GraphProjector.java‎

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import com.ibm.cldk.schema.JModule;
3939
import com.ibm.cldk.schema.JRecordComponent;
4040
import com.ibm.cldk.schema.JType;
41+
import com.ibm.cldk.schema.JTypeParameter;
4142
import com.ibm.cldk.schema.JVariableDeclaration;
4243
import com.ibm.cldk.schema.Span;
4344
import com.ibm.cldk.schema.V2Json;
@@ -218,6 +219,7 @@ private static void projectType(RowBuilder b, NodeRef parent, String containment
218219
p.put("base_types", type.getBaseTypes());
219220
p.put("interfaces", type.getInterfaces());
220221
p.put("docstring", docstringOf(type.getComments()));
222+
putTypeParameters(p, type.getTypeParameters());
221223
putLines(p, type.getSpan());
222224
if (type.isEntrypointClass()) {
223225
p.put("is_entrypoint", true);
@@ -252,9 +254,11 @@ private static void projectType(RowBuilder b, NodeRef parent, String containment
252254
ep.put("name", ec.getName());
253255
ep.put("arguments", ec.getArguments());
254256
ep.put("docstring", docstringOf(ec.getComments()));
257+
putLines(ep, ec.getSpan());
255258
ep.put("_module", fileKey);
256259
NodeRef er = b.node(Arrays.asList("JEnumConstant"), "id", id, RowBuilder.prune(ep));
257260
b.edge("J_HAS_ENUM_CONSTANT", ref, er);
261+
annotate(b, er, ec.getDecorators());
258262
}
259263
for (JRecordComponent rc : type.getRecordComponents()) {
260264
Map<String, Object> rp = RowBuilder.props();
@@ -267,9 +271,11 @@ private static void projectType(RowBuilder b, NodeRef parent, String containment
267271
rp.put("is_variadic", true);
268272
}
269273
rp.put("docstring", docstringOf(rc.getComments()));
274+
putLines(rp, rc.getSpan());
270275
rp.put("_module", fileKey);
271276
NodeRef rr = b.node(Arrays.asList("JRecordComponent"), "id", id, RowBuilder.prune(rp));
272277
b.edge("J_HAS_RECORD_COMPONENT", ref, rr);
278+
annotate(b, rr, rc.getDecorators());
273279
}
274280
for (Map.Entry<String, JCallable> c : type.getCallables().entrySet()) {
275281
projectCallable(b, ref, c.getKey(), c.getValue(), module, fileKey, typeIdByFqn);
@@ -332,7 +338,13 @@ private static void projectCallable(RowBuilder b, NodeRef owner, String signatur
332338
p.put("is_entrypoint", true);
333339
p.put("entrypoint_frameworks", c.getEntrypointFrameworks());
334340
}
341+
putTypeParameters(p, c.getTypeParameters());
335342
putLines(p, c.getSpan());
343+
// The body block's own offsets, under a `body_` prefix. `span` covers the whole declaration,
344+
// so without this the graph cannot tell the signature from the body it encloses -- and an
345+
// abstract or interface method, which has no body at all, reads the same as one whose body
346+
// was simply not recorded. Absent (not zeroed) when there is no body.
347+
putLines(p, c.getBodySpan(), "body_");
336348
p.put("_module", fileKey);
337349
NodeRef ref = b.node(labels, "id", c.getId(), RowBuilder.prune(p));
338350
b.edge("J_HAS_METHOD", owner, ref);
@@ -369,6 +381,15 @@ private static void projectCallable(RowBuilder b, NodeRef owner, String signatur
369381
np.put("is_static_call", n.getIsStaticCall());
370382
np.put("argument_types", n.getArgumentTypes());
371383
np.put("argument_expr", n.getArgumentExpr());
384+
np.put("callee_signature", n.getCalleeSignature());
385+
// The canonical `arguments`: one body-local `line:col` id per argument, positionally
386+
// aligned with `argument_expr` and `argument_types`. Kept local, NOT mapped to global
387+
// body-node ids the way `call_node` below is: an argument is a body node only when it is
388+
// itself a call site (`f(g(x))`), so qualifying every entry with the owning callable
389+
// would hand out `:JBodyNode` ids that mostly resolve to nothing. A consumer that wants
390+
// the node for a nested call builds it the same way this projection does -- the owner id,
391+
// an `@`, then the local id.
392+
np.put("arguments", n.getArguments());
372393
putLines(np, n.getSpan());
373394
np.put("_module", fileKey);
374395
// L4 SDG synthetic-vertex payload: absent on every non-synthetic node (prune drops nulls).
@@ -429,6 +450,36 @@ private static String globalOrdinal(String callableId, String localKey) {
429450
return localKey.startsWith("@") ? callableId + localKey : callableId + "@" + localKey;
430451
}
431452

453+
/**
454+
* A span as a MERGE discriminant. Byte offsets when the span has them, the line/column pair
455+
* otherwise, and {@code ""} for no span -- two annotations without spans collapse, which is the
456+
* pre-existing behaviour and no worse than it.
457+
*/
458+
private static String spanKey(Span span) {
459+
if (span == null) {
460+
return "";
461+
}
462+
if (span.getBytes() != null && span.getBytes().length > 1) {
463+
return span.getBytes()[0] + ":" + span.getBytes()[1];
464+
}
465+
if (span.getStart() != null && span.getStart().length > 1) {
466+
return "L" + span.getStart()[0] + ":" + span.getStart()[1];
467+
}
468+
return "";
469+
}
470+
471+
/**
472+
* The declared type parameters, serialized whole. Each carries a name, its resolved bounds, its
473+
* own span and its own annotations, so it does not flatten to a scalar -- same call as
474+
* {@code parameters_json}, and for the same reason. Absent when the declaration is not generic.
475+
*/
476+
private static void putTypeParameters(Map<String, Object> p, List<JTypeParameter> params) {
477+
if (params == null || params.isEmpty()) {
478+
return;
479+
}
480+
p.put("type_parameters_json", V2Json.compact().toJson(params));
481+
}
482+
432483
// ------------------------------------------------------------------------------------------
433484
// Repository-artifact layer: build manifests, config files, declared dependencies.
434485
// ------------------------------------------------------------------------------------------
@@ -601,7 +652,12 @@ private static void annotate(RowBuilder b, NodeRef owner, List<JDecorator> decor
601652
RowBuilder.prune(mapOf("name", d.getName())));
602653
Map<String, Object> p = RowBuilder.props();
603654
p.put("arguments", d.getArgs());
604-
b.edge("J_ANNOTATED_BY", owner, ann, RowBuilder.prune(p));
655+
putLines(p, d.getSpan());
656+
// `_k` = the application site. Annotations are repeatable in Java (`@Foo @Foo`), so the
657+
// same (owner, annotation) endpoint pair can occur more than once on one declaration;
658+
// without the discriminant a plain MERGE collapses them onto one relationship and keeps
659+
// only the last span and argument list SET.
660+
b.keyedEdge("J_ANNOTATED_BY", owner, ann, RowBuilder.prune(p), spanKey(d.getSpan()));
605661
}
606662
}
607663

@@ -733,24 +789,34 @@ private static String slice(String source, Span span) {
733789
* position (two nodes on one line) unrepresentable.
734790
*/
735791
private static void putLines(Map<String, Object> p, Span span) {
792+
putLines(p, span, "");
793+
}
794+
795+
/**
796+
* As above, under a property-name prefix, for a node carrying a second span beside its own —
797+
* {@code JCallable} carries the declaration's span unprefixed and the body block's under
798+
* {@code body_}. A prefix rather than a second node because a body block is not a thing with an
799+
* identity; it is a second pair of offsets into the same file.
800+
*/
801+
private static void putLines(Map<String, Object> p, Span span, String prefix) {
736802
if (span == null) {
737803
return;
738804
}
739805
if (span.getStart() != null && span.getStart().length > 0) {
740-
p.put("start_line", span.getStart()[0]);
806+
p.put(prefix + "start_line", span.getStart()[0]);
741807
if (span.getStart().length > 1) {
742-
p.put("start_column", span.getStart()[1]);
808+
p.put(prefix + "start_column", span.getStart()[1]);
743809
}
744810
}
745811
if (span.getEnd() != null && span.getEnd().length > 0) {
746-
p.put("end_line", span.getEnd()[0]);
812+
p.put(prefix + "end_line", span.getEnd()[0]);
747813
if (span.getEnd().length > 1) {
748-
p.put("end_column", span.getEnd()[1]);
814+
p.put(prefix + "end_column", span.getEnd()[1]);
749815
}
750816
}
751817
if (span.getBytes() != null && span.getBytes().length > 1) {
752-
p.put("start_byte", span.getBytes()[0]);
753-
p.put("end_byte", span.getBytes()[1]);
818+
p.put(prefix + "start_byte", span.getBytes()[0]);
819+
p.put(prefix + "end_byte", span.getBytes()[1]);
754820
}
755821
}
756822

‎src/main/java/com/ibm/cldk/neo4j/V2SchemaCatalog.java‎

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,19 @@ Map<String, String> done() {
9898
* are the ones they must adopt.
9999
*/
100100
private static Map<String, String> lines(P p) {
101-
return p.put("start_line", "integer").put("start_column", "integer")
102-
.put("end_line", "integer").put("end_column", "integer")
103-
.put("start_byte", "integer").put("end_byte", "integer").done();
101+
return span(p, "").done();
102+
}
103+
104+
/**
105+
* The same six properties under a name prefix, for a node carrying a second span beside its own.
106+
* {@code JCallable} is the only one: {@code body_*} is the body block, where the unprefixed span
107+
* is the whole declaration. Absent entirely on an abstract or interface method, which has no
108+
* body -- distinguishable from a body that was not recorded, which the line pair alone was not.
109+
*/
110+
private static P span(P p, String prefix) {
111+
return p.put(prefix + "start_line", "integer").put(prefix + "start_column", "integer")
112+
.put(prefix + "end_line", "integer").put(prefix + "end_column", "integer")
113+
.put(prefix + "start_byte", "integer").put(prefix + "end_byte", "integer");
104114
}
105115

106116
public static final List<NodeLabel> NODE_LABELS = buildNodeLabels();
@@ -136,18 +146,19 @@ private static List<NodeLabel> buildNodeLabels() {
136146
lines(new P().put("id", "string").put("name", "string").put("kind", "string")
137147
.put("modifiers", "string[]").put("base_types", "string[]")
138148
.put("interfaces", "string[]").put("docstring", "string")
149+
.put("type_parameters_json", "string")
139150
.put("is_entrypoint", "boolean").put("entrypoint_frameworks", "string[]"))));
140151

141152
n.add(node("JCallable", "JSymbol", "id",
142-
lines(new P().put("id", "string").put("name", "string").put("signature", "string")
153+
span(span(new P().put("id", "string").put("name", "string").put("signature", "string")
143154
.put("kind", "string").put("declaration", "string").put("return_type", "string")
144155
.put("parameters_json", "string").put("modifiers", "string[]")
145156
.put("error_channel", "string[]").put("code", "string").put("docstring", "string")
146157
.put("cyclomatic_complexity", "integer")
147158
.put("referenced_types", "string[]").put("accessed_fields", "string[]")
148159
.put("is_implicit", "boolean").put("is_entrypoint", "boolean")
149160
.put("entrypoint_frameworks", "string[]")
150-
)));
161+
.put("type_parameters_json", "string"), ""), "body_").done()));
151162

152163
n.add(node("JExternal", "JSymbol", "id",
153164
new P().put("id", "string").put("kind", "string").put("signature", "string")
@@ -163,20 +174,21 @@ private static List<NodeLabel> buildNodeLabels() {
163174
.put("initializer", "string"))));
164175

165176
n.add(node("JEnumConstant", "JEnumConstant", "id",
166-
new P().put("id", "string").put("name", "string").put("arguments", "string[]")
167-
.put("docstring", "string").done()));
177+
lines(new P().put("id", "string").put("name", "string").put("arguments", "string[]")
178+
.put("docstring", "string"))));
168179

169180
n.add(node("JRecordComponent", "JRecordComponent", "id",
170-
new P().put("id", "string").put("name", "string").put("type", "string")
181+
lines(new P().put("id", "string").put("name", "string").put("type", "string")
171182
.put("modifiers", "string[]").put("is_variadic", "boolean")
172-
.put("docstring", "string").done()));
183+
.put("docstring", "string"))));
173184

174185
n.add(node("JBodyNode", "JBodyNode", "id",
175186
lines(new P().put("id", "string").put("kind", "string").put("method_name", "string")
176187
.put("receiver_expr", "string").put("receiver_type", "string")
177188
.put("return_type", "string").put("accessibility", "string")
178189
.put("is_constructor_call", "boolean").put("is_static_call", "boolean")
179190
.put("argument_types", "string[]").put("argument_expr", "string[]")
191+
.put("callee_signature", "string").put("arguments", "string[]")
180192
// L4 SDG synthetic-vertex payload.
181193
.put("var", "string").put("call_node", "string"))));
182194

@@ -238,8 +250,14 @@ private static List<RelType> buildRelTypes() {
238250
r.add(rel("J_IMPORTS", Arrays.asList("JModule"), Arrays.asList("JModule", "JPackage"),
239251
new P().put("spellings", "string[]").put("is_static", "boolean")
240252
.put("is_wildcard", "boolean").done()));
241-
r.add(rel("J_ANNOTATED_BY", Arrays.asList("JType", "JCallable", "JField"),
242-
Arrays.asList("JAnnotation"), new P().put("arguments", "string[]").done()));
253+
// `_k` = the application site's span. Java annotations are repeatable (`@Foo @Foo`), so one
254+
// (owner, annotation) endpoint pair legitimately occurs twice on one declaration; without the
255+
// discriminant a plain MERGE collapses the two applications onto one relationship and keeps
256+
// only the last span and argument list SET.
257+
r.add(rel("J_ANNOTATED_BY",
258+
Arrays.asList("JType", "JCallable", "JField", "JEnumConstant", "JRecordComponent"),
259+
Arrays.asList("JAnnotation"),
260+
lines(new P().put("arguments", "string[]").put("_k", "string"))));
243261
// L3 CPG overlay. `_k` is the MERGE discriminant (internal, underscore-prefixed): J_CFG_NEXT
244262
// merges per `kind` (a conditional's true/false pair), J_DDG per `(var, prov)`.
245263
r.add(rel("J_CFG_NEXT", body, body, new P().put("kind", "string").put("_k", "string").done()));

0 commit comments

Comments
 (0)