Skip to content

Commit 5abef48

Browse files
committed
Support tags in line comments (resolve #1924)
1 parent 1abd9e1 commit 5abef48

5 files changed

Lines changed: 91 additions & 26 deletions

File tree

‎packages/docs/src/content/docs/reference/jsdoc-tsdoc-tags.md‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ report an issue with Knip for false positives).
1818

1919
:::
2020

21-
JSDoc comments always start with `/**` (not `//`) and can be single or
22-
multi-line.
21+
Tags also work in regular block comments (`/* ... */`) and standalone line
22+
comments (`// ...`) before a declaration. These comments keep suppression tags
23+
out of JSDoc hover information.
24+
25+
Tags in consecutive comments are combined. Trailing `//` comments are ignored.
2326

2427
## Tags
2528

@@ -30,7 +33,7 @@ Example:
3033
/** @lintignore */
3134
export const myUnusedExport = 1;
3235

33-
/** @lintignore */
36+
// @lintignore
3437
import Unresolved from './generated/lib.js';
3538
```
3639

‎packages/knip/fixtures/tags-hints/tag-on-member/status.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@ export enum Fruit {
44
banana = 'banana',
55
/** @knipignore */
66
cherry = 'cherry',
7+
// @knipignore
8+
date = 'date',
79
}
810

911
export namespace Basket {
1012
/** @knipignore */
1113
export const size = 1;
1214
export const color = 'red';
15+
// @knipignore
16+
export const depth = 2;
1317
}
1418

1519
/** @knipignore */

‎packages/knip/fixtures/tags-hints/tags-exclude/tags.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,23 @@ export const tagged = 0;
55

66
/** @tagged */
77
export const taggedToo = 2;
8+
9+
// @lintignore
10+
export const lineTagged = 3;
11+
12+
// @unknown
13+
// @lintignore
14+
export const stackedTags = 4;
15+
16+
// @lintignore
17+
/** Documentation for consumers. */
18+
export const documented = 5;
19+
20+
// @public
21+
export const linePublic = 6;
22+
23+
/* @lintignore */
24+
export const blockTagged = 7;
25+
26+
// @lintignore
27+
import './generated.js';

‎packages/knip/src/typescript/visitors/jsdoc.ts‎

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,28 @@ export const EMPTY_TAGS: Set<string> = new Set();
44

55
export function buildJSDocTagLookup(comments: Comment[], sourceText: string) {
66
const entries: { reach: number; tags: Set<string> }[] = [];
7+
let reach = sourceText.length;
8+
9+
for (let i = comments.length - 1; i >= 0; i--) {
10+
const comment = comments[i];
11+
let end = comment.end;
12+
while (end < sourceText.length) {
13+
const ch = sourceText.charCodeAt(end);
14+
if (ch !== 32 && ch !== 9 && ch !== 10 && ch !== 13) break;
15+
end++;
16+
}
17+
if (end !== comments[i + 1]?.start) reach = end;
718

8-
for (const comment of comments) {
9-
if (comment.type !== 'Block') continue;
1019
const value = comment.value;
1120
let index = value.indexOf('@');
1221
if (index === -1) continue;
1322

23+
if (comment.type === 'Line') {
24+
let start = comment.start;
25+
while (start > 0 && (sourceText.charCodeAt(start - 1) === 32 || sourceText.charCodeAt(start - 1) === 9)) start--;
26+
if (start > 0 && sourceText.charCodeAt(start - 1) !== 10 && sourceText.charCodeAt(start - 1) !== 13) continue;
27+
}
28+
1429
let tags: Set<string> | undefined;
1530
while (index !== -1) {
1631
let end = index + 1;
@@ -29,31 +44,16 @@ export function buildJSDocTagLookup(comments: Comment[], sourceText: string) {
2944
}
3045
if (!tags) continue;
3146

32-
let reach = comment.end;
33-
for (;;) {
34-
while (reach < sourceText.length) {
35-
const ch = sourceText.charCodeAt(reach);
36-
if (ch === 32 || ch === 9 || ch === 10 || ch === 13) {
37-
reach++;
38-
continue;
39-
}
40-
break;
41-
}
42-
if (
43-
reach + 1 < sourceText.length &&
44-
sourceText.charCodeAt(reach) === 47 &&
45-
sourceText.charCodeAt(reach + 1) === 47
46-
) {
47-
const eol = sourceText.indexOf('\n', reach + 2);
48-
reach = eol === -1 ? sourceText.length : eol + 1;
49-
continue;
50-
}
51-
break;
47+
const entry = entries[entries.length - 1];
48+
if (entry?.reach === reach) {
49+
for (const tag of tags) entry.tags.add(tag);
50+
} else {
51+
entries.push({ reach, tags });
5252
}
53-
entries.push({ reach, tags });
5453
}
5554

5655
if (entries.length === 0) return () => EMPTY_TAGS;
56+
entries.reverse();
5757

5858
return function getJSDocTags(nodeStart: number): Set<string> {
5959
let lo = 0;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import assert from 'node:assert/strict';
2+
import test from 'node:test';
3+
import { parseSync } from 'oxc-parser';
4+
import { buildJSDocTagLookup } from '../../src/typescript/visitors/jsdoc.ts';
5+
6+
const cases: [string, string, string[][]][] = [
7+
['JSDoc tags', '/** @public */\nexport const value = 1;', [['@public']]],
8+
['regular block tags', '/* @public */\nexport const value = 1;', [['@public']]],
9+
['line tags', '// @public\nexport const value = 1;', [['@public']]],
10+
['indented line tags', '\t // @lintignore\nexport const value = 1;', [['@lintignore']]],
11+
['stacked line tags', '// @internal\n// @public\nexport const value = 1;', [['@internal', '@public']]],
12+
['line tags after JSDoc', '/** @internal */\n// @public\nexport const value = 1;', [['@internal', '@public']]],
13+
['line tags before JSDoc', '// @public\n/** Description */\nexport const value = 1;', [['@public']]],
14+
['intervening line comment', '// @public\n// Explanation\nexport const value = 1;', [['@public']]],
15+
['CRLF line comments', '// @public\r\n// Explanation\r\nexport const value = 1;', [['@public']]],
16+
['CR line comments', '// @public\r// Explanation\rexport const value = 1;', [['@public']]],
17+
['blank line', '// @public\n\nexport const value = 1;', [['@public']]],
18+
['trailing line tags', 'export const first = 1; // @public\nexport const second = 2;', [[], []]],
19+
['intervening code', '// @public\nconst local = 1;\nexport const value = local;', [[]]],
20+
['next declaration', '// @public\nexport const first = 1;\nexport const second = 2;', [['@public'], []]],
21+
['string contents', 'export const value = "// @public";', [[]]],
22+
['untagged comment', '// Description\nexport const value = 1;', [[]]],
23+
];
24+
25+
for (const [name, source, expected] of cases) {
26+
test(`Read comment tags: ${name}`, () => {
27+
const result = parseSync('module.ts', source);
28+
assert.deepEqual(result.errors, []);
29+
const getTags = buildJSDocTagLookup(result.comments, source);
30+
const actual = result.program.body
31+
.filter(node => node.type === 'ExportNamedDeclaration')
32+
.map(node => getTags(node.start));
33+
assert.deepEqual(
34+
actual,
35+
expected.map(tags => new Set(tags))
36+
);
37+
});
38+
}

0 commit comments

Comments
 (0)