mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
fix(cfg): put embedded-script CFGs in file coordinates via lineOffset (#2195)
A Vue SFC <script> block parses at row 0 but lives at lineOffset in the .vue file. Every other worker-emitted graph node adds lineOffset to reach file coordinates, but collectFunctionCfgs built FunctionCfgs from the extracted script's raw rows and never offset them. Two consequences for .vue files: - inter-procedural taint silently resolved NOTHING — the summary-harvest join keys graph Function/Method nodes by their (offset) startLine but looked up the CFG's (unoffset) functionStartLine, missing by exactly lineOffset, so no FunctionSummary was ever produced; - persisted BasicBlock startLine/endLine (and the id's functionStartLine segment) pointed at the wrong .vue line, breaking source mapping. Thread lineOffset into collectFunctionCfgs and shift every CFG source-line field (functionStartLine/End, block start/end, statement + non-synthetic binding lines) into file coordinates at the one production chokepoint. A 0 offset returns the CFG unchanged, so .ts/.js/etc. stay byte-identical (bench --check fingerprints unchanged; worker-roundtrip + pipeline-pdg green). Unit tests for the shift + the 0-offset no-op added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8a5478c6f0
commit
194c36befc
3 changed files with 65 additions and 1 deletions
|
|
@ -30,11 +30,42 @@ export interface CollectedCfgs {
|
|||
readonly skipped: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a CFG built from an EXTRACTED sub-document's AST (script-relative
|
||||
* tree-sitter rows) into the enclosing file's coordinates by adding `offset` to
|
||||
* every source-line field. Needed for embedded scripts — a Vue SFC `<script>`
|
||||
* block parses at row 0 but lives at `lineOffset` in the `.vue` file, and every
|
||||
* other worker-emitted graph node is already file-relative; without this, the
|
||||
* CFG's `functionStartLine` would never join its Function/Method graph node
|
||||
* (inter-procedural taint silently resolves nothing) and BasicBlock source
|
||||
* lines would point at the wrong `.vue` line. A 0 offset returns the input
|
||||
* unchanged (the common case: `.ts`/`.js`/etc. parse at the file root), keeping
|
||||
* non-embedded languages byte-identical. Synthetic bindings keep `declLine` 0.
|
||||
*/
|
||||
function shiftCfgLines(cfg: FunctionCfg, offset: number): FunctionCfg {
|
||||
if (offset === 0) return cfg;
|
||||
return {
|
||||
...cfg,
|
||||
functionStartLine: cfg.functionStartLine + offset,
|
||||
functionEndLine: cfg.functionEndLine + offset,
|
||||
blocks: cfg.blocks.map((b) => ({
|
||||
...b,
|
||||
startLine: b.startLine + offset,
|
||||
endLine: b.endLine + offset,
|
||||
statements: b.statements?.map((s) => ({ ...s, line: s.line + offset })),
|
||||
})),
|
||||
bindings: cfg.bindings?.map((bd) =>
|
||||
bd.declLine > 0 ? { ...bd, declLine: bd.declLine + offset } : bd,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function collectFunctionCfgs(
|
||||
root: SyntaxNode,
|
||||
visitor: CfgVisitor<SyntaxNode>,
|
||||
filePath: string,
|
||||
maxFunctionLines = 0,
|
||||
lineOffset = 0,
|
||||
): CollectedCfgs {
|
||||
const cfgs: FunctionCfg[] = [];
|
||||
let skipped = 0;
|
||||
|
|
@ -48,7 +79,7 @@ export function collectFunctionCfgs(
|
|||
skipped++;
|
||||
} else {
|
||||
const cfg = visitor.buildFunctionCfg(node, filePath);
|
||||
if (cfg) cfgs.push(cfg);
|
||||
if (cfg) cfgs.push(shiftCfgLines(cfg, lineOffset));
|
||||
}
|
||||
}
|
||||
// Descend regardless (a skipped mega-function may still contain small
|
||||
|
|
|
|||
|
|
@ -1269,6 +1269,10 @@ const processFileGroup = (
|
|||
provider.cfgVisitor,
|
||||
file.path,
|
||||
PDG_MAX_FUNCTION_LINES,
|
||||
// Embedded scripts (Vue SFC <script>) parse at row 0 but live at
|
||||
// `lineOffset` in the file — shift the CFG into file coordinates so
|
||||
// it joins its graph node and BasicBlock lines map to source.
|
||||
lineOffset,
|
||||
);
|
||||
if (cfgs.length) withChannels = { ...withChannels, cfgSideChannel: cfgs };
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -112,6 +112,35 @@ describe('U4 — emitFileCfgs node/edge shape', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('collectFunctionCfgs — lineOffset → file coordinates (#2195 P1, Vue SFC)', () => {
|
||||
it('shifts functionStartLine + block + statement lines by lineOffset', () => {
|
||||
const code = `function f(x: number) { if (x) { a(); } else { b(); } }`;
|
||||
const base = collectFunctionCfgs(tsRoot(code), visitor(), 'x.vue').cfgs;
|
||||
const shifted = collectFunctionCfgs(tsRoot(code), visitor(), 'x.vue', 0, 5).cfgs;
|
||||
expect(base.length).toBeGreaterThan(0);
|
||||
expect(shifted).toHaveLength(base.length);
|
||||
expect(shifted[0].functionStartLine).toBe(base[0].functionStartLine + 5);
|
||||
expect(shifted[0].functionEndLine).toBe(base[0].functionEndLine + 5);
|
||||
// functionStartColumn is a COLUMN, not a line — unchanged.
|
||||
expect(shifted[0].functionStartColumn).toBe(base[0].functionStartColumn);
|
||||
for (let i = 0; i < base[0].blocks.length; i++) {
|
||||
expect(shifted[0].blocks[i].startLine).toBe(base[0].blocks[i].startLine + 5);
|
||||
expect(shifted[0].blocks[i].endLine).toBe(base[0].blocks[i].endLine + 5);
|
||||
}
|
||||
// per-statement source lines shift too (file-accurate taint/explain hops).
|
||||
const stmtLines = (cfgs: readonly FunctionCfg[]): number[] =>
|
||||
cfgs[0].blocks.flatMap((b) => (b.statements ?? []).map((s) => s.line));
|
||||
expect(stmtLines(shifted)).toEqual(stmtLines(base).map((l) => l + 5));
|
||||
});
|
||||
|
||||
it('lineOffset 0 is a byte-identical no-op (non-embedded files unchanged)', () => {
|
||||
const code = `function g() { while (true) { tick(); } }`;
|
||||
const withZero = collectFunctionCfgs(tsRoot(code), visitor(), 'g.ts', 0, 0).cfgs;
|
||||
const omitted = collectFunctionCfgs(tsRoot(code), visitor(), 'g.ts').cfgs;
|
||||
expect(JSON.stringify(withZero)).toBe(JSON.stringify(omitted));
|
||||
});
|
||||
});
|
||||
|
||||
describe('U4 — AC2: every BasicBlock is reachable from its function ENTRY', () => {
|
||||
// Fixtures deliberately contain no dead code, so the reachability closure
|
||||
// from each function's ENTRY (block index 0) must cover all of its blocks.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue