From a68d4c311d745ae421e477fdb6664cd6a1a1fd24 Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Tue, 7 Jul 2026 07:34:01 -0400 Subject: [PATCH] test: cover interface-extends-interface EXTENDS and entry/exit span slices (#45) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a first-party `ColoredShape extends Shape` interface to the dataflow-app hierarchy fixture and asserts the resulting Interface→Interface EXTENDS edge by node identity in the neo4j schema test — the one heritage path #33 handles but nothing previously exercised. Also asserts that L3 `@entry`/`@exit` body nodes byte-slice to the callable's whole span (per extract.ts's emitNode), alongside the existing statement-node slice test. Confirms `param` is contracted out before emission (folded onto `@entry`/`@formal_in`) and is never present as a raw body node, so no slice test is fabricated for it. --- test/fixtures/dataflow-app/src/hierarchy.ts | 7 +++- test/neo4j-schema.test.ts | 19 ++++++++++- test/schema-v2.test.ts | 37 +++++++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/test/fixtures/dataflow-app/src/hierarchy.ts b/test/fixtures/dataflow-app/src/hierarchy.ts index 79da20e..4b6eb02 100644 --- a/test/fixtures/dataflow-app/src/hierarchy.ts +++ b/test/fixtures/dataflow-app/src/hierarchy.ts @@ -1,4 +1,5 @@ -/** A minimal first-party class hierarchy — exercises the Neo4j EXTENDS/IMPLEMENTS projection. */ +/** A minimal first-party class/interface hierarchy — exercises the Neo4j EXTENDS/IMPLEMENTS + * projection, including interface→interface EXTENDS (`ColoredShape extends Shape`). */ export interface Shape { area(): number; @@ -8,6 +9,10 @@ export interface Labeled { readonly label: string; } +export interface ColoredShape extends Shape { + readonly color: string; +} + export class Rectangle implements Shape { constructor( protected readonly width: number, diff --git a/test/neo4j-schema.test.ts b/test/neo4j-schema.test.ts index 0c0d813..ccc6e83 100644 --- a/test/neo4j-schema.test.ts +++ b/test/neo4j-schema.test.ts @@ -108,7 +108,8 @@ describe(":Application node carries analyzer identity (issue #43)", () => { // ---- Class inheritance: EXTENDS/IMPLEMENTS (issue #33) ------------------------------------------ // dataflow-app's src/hierarchy.ts is a minimal, first-party heritage fixture: `Rectangle implements -// Shape`, `Square extends Rectangle implements Labeled`. +// Shape`, `Square extends Rectangle implements Labeled`, `ColoredShape extends Shape` (interface→ +// interface heritage — issue #45). describe("neo4j inheritance edges (issue #33)", () => { test("EXTENDS and IMPLEMENTS are declared in the schema catalog", () => { @@ -145,4 +146,20 @@ describe("neo4j inheritance edges (issue #33)", () => { expect(nodeValues.has(e.to.value), `dangling EXTENDS/IMPLEMENTS target ${e.to.value}`).toBe(true); } }); + + test("interface-extends-interface projects an EXTENDS edge with an Interface source AND target (issue #45)", () => { + const coloredShape = nodeBySignature("src/hierarchy.ColoredShape"); + const shape = nodeBySignature("src/hierarchy.Shape"); + expect(coloredShape, "ColoredShape node").toBeDefined(); + expect(shape, "Shape node").toBeDefined(); + expect(specificLabel(coloredShape!.labels)).toBe("Interface"); + expect(specificLabel(shape!.labels)).toBe("Interface"); + + const ext = rows.edges.filter((e) => e.type === "EXTENDS"); + expect(ext.some((e) => e.from.value === coloredShape!.value && e.to.value === shape!.value)).toBe(true); + + const nodeValues = new Set(rows.nodes.map((n) => n.value)); + expect(nodeValues.has(coloredShape!.value)).toBe(true); + expect(nodeValues.has(shape!.value)).toBe(true); + }); }); diff --git a/test/schema-v2.test.ts b/test/schema-v2.test.ts index 830f838..403e5c5 100644 --- a/test/schema-v2.test.ts +++ b/test/schema-v2.test.ts @@ -448,6 +448,43 @@ describe("schema v2 — L3 intraprocedural dataflow", () => { expect(e).toBeGreaterThan(s); expect(mod.source.slice(s, e)).toBe('let label = "none";'); }); + + test("@entry/@exit body nodes are source-sliceable and carry the whole-callable span (issue #45)", () => { + // Per extract.ts's emitNode: "ENTRY/EXIT carry the whole callable's span" — unlike statement + // nodes (which slice out just their own text), entry/exit should reproduce the callable's + // entire declaration, including its `export` modifier. + const mod = dfL3.application.symbol_table["src/flow.ts"]; + const classify = mod.functions.classify as V2Callable; + const entry = classify.body["@entry"]; + const exit = classify.body["@exit"]; + expect(entry?.kind).toBe("entry"); + expect(exit?.kind).toBe("exit"); + + const [cs, ce] = (classify.span as { bytes: [number, number] }).bytes; + const callableText = mod.source.slice(cs, ce); + expect(callableText.startsWith("export function classify")).toBe(true); + + for (const node of [entry, exit]) { + const [s, e] = (node as { span: { bytes: [number, number] } }).span.bytes; + expect(e).toBeGreaterThan(s); + const slice = mod.source.slice(s, e); + expect(slice.length).toBeGreaterThan(0); + expect(slice).toBe(callableText); // whole-callable span, byte-identical to classify's own span + } + }); + + // `param` is contracted out before emission (dataflow.ts: `if (n.kind === "param") continue;` in + // emitL3) — a param folds onto `@entry` at L3 and only reappears, synthetically, as + // `@formal_in:N` at L4 (asserted below). So there is no emitted `param`-kind body node to slice; + // slice fidelity for raw `param` is N/A at the emitted-tree level, not merely untested. + test("no raw `param`-kind body node is ever emitted (contracted onto @entry/@formal_in)", () => { + for (const c of allCallables(dfL3.application)) { + for (const node of Object.values(c.body)) expect(node.kind).not.toBe("param"); + } + for (const c of allCallables(dfL4.application)) { + for (const node of Object.values(c.body)) expect(node.kind).not.toBe("param"); + } + }); }); describe("schema v2 — L4 interprocedural SDG", () => {