Skip to content

Commit 8a328b4

Browse files
committed
fix(lint): report physical JSONL line numbers (blank lines no longer shift them)
1 parent f445397 commit 8a328b4

2 files changed

Lines changed: 19 additions & 12 deletions

File tree

src/commands/test.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2775,10 +2775,12 @@ describe('runLint', () => {
27752775
expect(report).toMatchObject({ checked: 1, valid: 1, issues: [] });
27762776
});
27772777

2778-
it('a JSONL file reports each bad line with its line number', async () => {
2778+
it('a JSONL file reports each bad line with its PHYSICAL line number (blank lines skipped, not renumbered)', async () => {
27792779
const dir = mkdtempSync(join(tmpdir(), 'cli-lint-jsonl-'));
27802780
const file = join(dir, 'plans.jsonl');
2781-
writeFileSync(file, `${VALID_PLAN}\nnot json at all\n${INVALID_PLAN}\n`, 'utf8');
2781+
// A blank separator line sits between entries: line 1 valid, line 2 blank,
2782+
// line 3 bad JSON, line 4 invalid plan. Reported numbers must be 3 and 4.
2783+
writeFileSync(file, `${VALID_PLAN}\n\nnot json at all\n${INVALID_PLAN}\n`, 'utf8');
27822784
const out: string[] = [];
27832785
const rejection = await runLint(
27842786
{ profile: 'default', output: 'json', debug: false, plans: file },
@@ -2787,8 +2789,10 @@ describe('runLint', () => {
27872789
expect(rejection).toMatchObject({ exitCode: 5 });
27882790
const report = JSON.parse(out.join('')) as { checked: number; issues: Array<{ file: string }> };
27892791
expect(report.checked).toBe(3);
2790-
expect(report.issues.some(issue => issue.file.endsWith(':2'))).toBe(true);
27912792
expect(report.issues.some(issue => issue.file.endsWith(':3'))).toBe(true);
2793+
expect(report.issues.some(issue => issue.file.endsWith(':4'))).toBe(true);
2794+
// The blank line itself is not an entry and never reports.
2795+
expect(report.issues.some(issue => issue.file.endsWith(':2'))).toBe(false);
27922796
});
27932797

27942798
it('requires exactly one input source', async () => {

src/commands/test.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3728,27 +3728,30 @@ export async function runLint(opts: LintOptions, deps: TestDeps = {}): Promise<C
37283728
} catch {
37293729
throw localValidationError('plans', `cannot read file: ${absolute}`);
37303730
}
3731-
const lines = content
3731+
// Index lines BEFORE dropping blanks so every reported `file:N` points at
3732+
// the PHYSICAL line in the file (a blank separator line must not shift all
3733+
// subsequent line numbers).
3734+
const numberedLines = content
37323735
.split('\n')
3733-
.map(line => line.trim())
3734-
.filter(line => line.length > 0);
3735-
if (lines.length === 0) throw localValidationError('plans', 'contains no plan lines');
3736-
lines.forEach((line, index) => {
3737-
collect(`${opts.plans}:${index + 1}`, () => {
3736+
.map((rawLine, physicalIndex) => ({ line: rawLine.trim(), lineNo: physicalIndex + 1 }))
3737+
.filter(entry => entry.line.length > 0);
3738+
if (numberedLines.length === 0) throw localValidationError('plans', 'contains no plan lines');
3739+
for (const { line, lineNo } of numberedLines) {
3740+
collect(`${opts.plans}:${lineNo}`, () => {
37383741
let parsed: unknown;
37393742
try {
37403743
parsed = JSON.parse(line);
37413744
} catch {
37423745
throw localValidationError(
37433746
'plans',
3744-
`line ${index + 1} is not valid JSON`,
3747+
`line ${lineNo} is not valid JSON`,
37453748
undefined,
37463749
'field',
37473750
);
37483751
}
3749-
assertPlanShape(parsed, { specIndex: index });
3752+
assertPlanShape(parsed, { specIndex: lineNo - 1 });
37503753
});
3751-
});
3754+
}
37523755
}
37533756

37543757
const filesWithIssues = new Set(issues.map(issue => issue.file)).size;

0 commit comments

Comments
 (0)