Skip to content

Commit d483090

Browse files
afc163claudegemini-code-assist[bot]
authored
fix: correct lint line numbers from 0 to actual positions (#65)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent bcc6305 commit d483090

2 files changed

Lines changed: 177 additions & 5 deletions

File tree

src/__tests__/commands/lint.test.ts

Lines changed: 150 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
22
import { Command } from 'commander';
3-
import { registerLintCommand } from '../../commands/lint.js';
3+
import { registerLintCommand, type LintIssue } from '../../commands/lint.js';
44
import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
55
import { join } from 'node:path';
66
import { tmpdir } from 'node:os';
@@ -14,10 +14,11 @@ function makeTmpFile(name: string, content: string): void {
1414
async function runLint(
1515
args: string[] = [],
1616
format: string = 'json',
17+
version: string = '5.20.0',
1718
): Promise<string> {
1819
const program = new Command();
1920
program.option('--format <format>', '', format);
20-
program.option('--version <version>', '', '5.20.0');
21+
program.option('--version <version>', '', version);
2122
program.option('--lang <lang>', '', 'en');
2223
registerLintCommand(program);
2324
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
@@ -455,6 +456,153 @@ const App = () => (
455456
});
456457
});
457458

459+
// --- False positive regression tests (issue #45) ---
460+
describe('deprecated false positives (#45)', () => {
461+
async function runLintV6(
462+
file: string,
463+
args: string[] = [],
464+
): Promise<{ issues: LintIssue[]; summary: any }> {
465+
return parseJson(await runLint([file, ...args], 'json', '6.0.0'));
466+
}
467+
468+
it('should not flag non-antd component props as deprecated (SendTo message)', async () => {
469+
// Alert.message is deprecated in v6, but SendTo is a custom component
470+
makeTmpFile(
471+
'false-pos-1.tsx',
472+
`import { Alert } from 'antd';
473+
474+
const SendTo = ({ message }: { message: string }) => <div>{message}</div>;
475+
476+
const App = () => (
477+
<div>
478+
<SendTo message="hello" />
479+
<Alert title="Warning" />
480+
</div>
481+
);
482+
`,
483+
);
484+
const data = await runLintV6(join(tmpDir, 'false-pos-1.tsx'), ['--only', 'deprecated']);
485+
// SendTo is not an antd component, so no deprecation should be reported
486+
const messageIssues = data.issues.filter((i: any) => i.message.includes('message'));
487+
expect(messageIssues).toHaveLength(0);
488+
});
489+
490+
it('should not flag Button type as Divider deprecated type', async () => {
491+
// Divider.type is deprecated in v6, but Button.type is NOT
492+
makeTmpFile(
493+
'false-pos-2.tsx',
494+
`import { Button, Divider } from 'antd';
495+
496+
const App = () => (
497+
<div>
498+
<Button type="dashed">Click</Button>
499+
<Button type="primary">OK</Button>
500+
<Divider />
501+
</div>
502+
);
503+
`,
504+
);
505+
const data = await runLintV6(join(tmpDir, 'false-pos-2.tsx'), ['--only', 'deprecated']);
506+
// Button.type is NOT deprecated, should not be flagged
507+
const typeIssues = data.issues.filter((i: any) => i.message.includes('type'));
508+
expect(typeIssues).toHaveLength(0);
509+
});
510+
511+
it('should correctly flag Divider deprecated type prop', async () => {
512+
// Divider.type IS deprecated in v6
513+
makeTmpFile(
514+
'false-pos-2b.tsx',
515+
`import { Divider } from 'antd';
516+
517+
const App = () => <Divider type="vertical" />;
518+
`,
519+
);
520+
const data = await runLintV6(join(tmpDir, 'false-pos-2b.tsx'), ['--only', 'deprecated']);
521+
const typeIssues = data.issues.filter((i: any) => i.message.includes('type'));
522+
expect(typeIssues).toHaveLength(1);
523+
expect(typeIssues[0].message).toContain('Divider');
524+
});
525+
526+
it('should correctly flag Alert deprecated message prop', async () => {
527+
// Alert.message IS deprecated in v6
528+
makeTmpFile(
529+
'false-pos-1b.tsx',
530+
`import { Alert } from 'antd';
531+
532+
const App = () => <Alert message="Warning" />;
533+
`,
534+
);
535+
const data = await runLintV6(join(tmpDir, 'false-pos-1b.tsx'), ['--only', 'deprecated']);
536+
const messageIssues = data.issues.filter((i: any) => i.message.includes('message'));
537+
expect(messageIssues).toHaveLength(1);
538+
expect(messageIssues[0].message).toContain('Alert');
539+
});
540+
541+
it('should not flag Space split when only JS .split() method is used', async () => {
542+
// Space.split is deprecated in v6, but JS .split() is a method call
543+
makeTmpFile(
544+
'false-pos-3.tsx',
545+
`import { Space, List } from 'antd';
546+
547+
const items = "a,b,c".split(",");
548+
const App = () => (
549+
<Space>
550+
<List split={false} />
551+
</Space>
552+
);
553+
`,
554+
);
555+
const data = await runLintV6(join(tmpDir, 'false-pos-3.tsx'), ['--only', 'deprecated']);
556+
// List.split is NOT deprecated, Space doesn't use split prop here
557+
// JS .split() method call should not be flagged
558+
const splitIssues = data.issues.filter((i: any) => i.message.includes('split'));
559+
expect(splitIssues).toHaveLength(0);
560+
});
561+
562+
it('should correctly flag Space deprecated split prop', async () => {
563+
// Space.split IS deprecated in v6
564+
makeTmpFile(
565+
'false-pos-3b.tsx',
566+
`import { Space } from 'antd';
567+
568+
const App = () => <Space split="|">items</Space>;
569+
`,
570+
);
571+
const data = await runLintV6(join(tmpDir, 'false-pos-3b.tsx'), ['--only', 'deprecated']);
572+
const splitIssues = data.issues.filter((i: any) => i.message.includes('split'));
573+
expect(splitIssues).toHaveLength(1);
574+
expect(splitIssues[0].message).toContain('Space');
575+
});
576+
577+
it('should report correct line numbers for deprecated props', async () => {
578+
makeTmpFile(
579+
'false-pos-lines.tsx',
580+
`import { Alert, Divider, Space } from 'antd';
581+
582+
const App = () => (
583+
<div>
584+
<Alert message="Warning" />
585+
<Divider type="vertical" />
586+
<Space split="|">items</Space>
587+
</div>
588+
);
589+
`,
590+
);
591+
const data = await runLintV6(join(tmpDir, 'false-pos-lines.tsx'), ['--only', 'deprecated']);
592+
expect(data.issues).toHaveLength(3);
593+
// Line numbers should be non-zero and correctly point to each element
594+
for (const issue of data.issues) {
595+
expect(issue.line).toBeGreaterThan(0);
596+
}
597+
const alertIssue = data.issues.find((i: any) => i.message.includes('Alert'));
598+
const dividerIssue = data.issues.find((i: any) => i.message.includes('Divider'));
599+
const spaceIssue = data.issues.find((i: any) => i.message.includes('Space'));
600+
expect(alertIssue!.line).toBe(5);
601+
expect(dividerIssue!.line).toBe(6);
602+
expect(spaceIssue!.line).toBe(7);
603+
});
604+
});
605+
458606
// --- Edge cases ---
459607
describe('edge cases', () => {
460608
it('skips files without antd reference', async () => {

src/commands/lint.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,24 @@ function getObjectExpressionKeys(attrs: any[], name: string): string[] {
9090
}
9191
/* v8 ignore stop */
9292

93+
/** Create a stateful offset-to-line converter that exploits monotonically increasing offsets. */
94+
function createLineMapper(source: string): (offset: number) => number {
95+
let lastOffset = 0;
96+
let lastLine = 1;
97+
return (offset: number) => {
98+
/* v8 ignore next 4 -- defensive: AST visitor offsets are monotonically increasing */
99+
if (offset < lastOffset) {
100+
lastOffset = 0;
101+
lastLine = 1;
102+
}
103+
for (let i = lastOffset; i < offset && i < source.length; i++) {
104+
if (source[i] === '\n') lastLine++;
105+
}
106+
lastOffset = offset;
107+
return lastLine;
108+
};
109+
}
110+
93111
function lintFile(
94112
filePath: string,
95113
deprecatedMap: Map<string, DeprecatedInfo[]>,
@@ -112,6 +130,12 @@ function lintFile(
112130

113131
const issues: LintIssue[] = [];
114132
const importedComponents = new Set<string>();
133+
const offsetToLine = createLineMapper(content);
134+
135+
const lineOf = (node: any): number => {
136+
if (typeof node.start === 'number') return offsetToLine(node.start);
137+
return node.loc?.start?.line ?? 0;
138+
};
115139

116140
const report = (rule: string, severity: LintIssue['severity'], line: number, message: string) => {
117141
issues.push({ file: filePath, line, rule, severity, message });
@@ -134,7 +158,7 @@ function lintFile(
134158

135159
if ((!only || only === 'performance') &&
136160
(spec.type === 'ImportDefaultSpecifier' || spec.type === 'ImportNamespaceSpecifier')) {
137-
report('performance', 'error', node.loc?.start?.line ?? 0,
161+
report('performance', 'error', lineOf(node),
138162
'Avoid wildcard import from antd. Use named imports: `import { Button } from \'antd\'`');
139163
}
140164
}
@@ -144,7 +168,7 @@ function lintFile(
144168
const compName = getJSXElementName(node.name);
145169
if (!compName) return;
146170
const attrs = node.attributes || [];
147-
const line = node.loc?.start?.line ?? 0;
171+
const line = lineOf(node);
148172

149173
// --- Deprecated checks ---
150174
if (!only || only === 'deprecated') {
@@ -167,7 +191,7 @@ function lintFile(
167191
const propName = attr.name?.name;
168192
const dep = deprecations.find((d) => d.prop === propName);
169193
if (dep) {
170-
report('deprecated', 'warning', attr.loc?.start?.line ?? line, `${compName} ${dep.message}`);
194+
report('deprecated', 'warning', lineOf(attr) || line, `${compName} ${dep.message}`);
171195
}
172196
}
173197
}

0 commit comments

Comments
 (0)