diff --git a/src/dataflow/extract.ts b/src/dataflow/extract.ts index 751ad53..d9a02f9 100644 --- a/src/dataflow/extract.ts +++ b/src/dataflow/extract.ts @@ -81,7 +81,16 @@ function emitNode(id: number, kind: GraphNode["kind"], ast: Node | null, build: const target = ast ?? build.fn; // ENTRY/EXIT carry the whole callable's span const s = build.sf.getLineAndColumnAtPos(target.getStart()); const e = build.sf.getLineAndColumnAtPos(target.getEnd()); - return { id, kind, start_line: s.line, start_column: s.column, end_line: e.line, end_column: e.column }; + return { + id, + kind, + start_line: s.line, + start_column: s.column, + end_line: e.line, + end_column: e.column, + start_offset: target.getStart(), + end_offset: target.getEnd(), + }; } function hasRestParam(build: FunctionCfgBuild): boolean { diff --git a/src/schema/graphs.ts b/src/schema/graphs.ts index 801eb5c..27e02a8 100644 --- a/src/schema/graphs.ts +++ b/src/schema/graphs.ts @@ -34,6 +34,9 @@ export interface GraphNode { start_column: number; end_line: number; end_column: number; + /** UTF-16 char offsets into the owning module's `source` (same convention as L1 `span.bytes`). */ + start_offset: number; + end_offset: number; } // ---------------------------------------------------------------------------------------------- diff --git a/src/schema/v2/dataflow.ts b/src/schema/v2/dataflow.ts index e5f7a22..f664df9 100644 Binary files a/src/schema/v2/dataflow.ts and b/src/schema/v2/dataflow.ts differ diff --git a/test/schema-v2.test.ts b/test/schema-v2.test.ts index ada1509..a33584e 100644 --- a/test/schema-v2.test.ts +++ b/test/schema-v2.test.ts @@ -344,6 +344,16 @@ describe("schema v2 — L3 intraprocedural dataflow", () => { test("no dangling endpoints at L3", () => { expect(danglingCount(dfL3)).toBe(0); }); + + test("a sampled L3 statement node is source-sliceable (span.bytes reproduces its text)", () => { + const mod = dfL3.application.symbol_table["src/flow.ts"]; + const classify = mod.functions.classify as V2Callable; + const stmt = classify.body["4:3"]; + expect(stmt?.kind).toBe("statement"); + const [s, e] = (stmt as { span: { bytes: [number, number] } }).span.bytes; + expect(e).toBeGreaterThan(s); + expect(mod.source.slice(s, e)).toBe('let label = "none";'); + }); }); describe("schema v2 — L4 interprocedural SDG", () => {