Skip to content

Commit 44a7181

Browse files
fix: preserve imports used in JSDoc @link tags (#111)
Co-authored-by: Anthony Fu <[email protected]>
1 parent 275d96e commit 44a7181

File tree

3 files changed

+155
-1
lines changed

3 files changed

+155
-1
lines changed

src/__test__/cases-js.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,48 @@ import { a, b } from "./utils";
77
import y from "package";
88
99
const c = a() + b + x() + y();
10+
`,
11+
},
12+
{
13+
code: `
14+
import { NoAuthenticationGuard } from "./no-authentication.guard";
15+
import { JwtAuthenticationGuard } from "./jwt-authentication.guard";
16+
17+
/**
18+
* You can reference {@link NoAuthenticationGuard} instead.
19+
* It's recommended to use the {@link JwtAuthenticationGuard}.
20+
*/
21+
const LOCAL_ENVIRONMENT_AUTHENTICATION_GUARD = JwtAuthenticationGuard;
22+
`,
23+
},
24+
{
25+
code: `
26+
import { SomeClass } from "./some-class";
27+
28+
/**
29+
* @see SomeClass
30+
*/
31+
const example = "test";
32+
`,
33+
},
34+
{
35+
code: `
36+
import { MyType } from "./types";
37+
38+
/**
39+
* @type {MyType}
40+
*/
41+
let value;
42+
`,
43+
},
44+
{
45+
code: `
46+
import { Config } from "./config";
47+
48+
/**
49+
* @param {Config} config - The configuration object
50+
*/
51+
function setup(config) {}
1052
`,
1153
},
1254
],
@@ -86,6 +128,26 @@ import y from "package";
86128
*/
87129
const c = 4;
88130
console.log(y);
131+
`,
132+
},
133+
{
134+
code: `
135+
import { UnusedClass } from "./unused";
136+
import { UsedInJSDoc } from "./used";
137+
138+
/**
139+
* Reference to {@link UsedInJSDoc}
140+
*/
141+
const example = "test";
142+
`,
143+
errors: ["'UnusedClass' is defined but never used."],
144+
output: `
145+
import { UsedInJSDoc } from "./used";
146+
147+
/**
148+
* Reference to {@link UsedInJSDoc}
149+
*/
150+
const example = "test";
89151
`,
90152
},
91153
],

src/__test__/cases-ts.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,38 @@ import y from "package";
88
import TType from "Package";
99
1010
const c: TType = a() + b + x() + y();
11+
`,
12+
},
13+
{
14+
code: `
15+
import { NoAuthenticationGuard } from "./no-authentication.guard";
16+
import { JwtAuthenticationGuard } from "./jwt-authentication.guard";
17+
18+
/**
19+
* You can reference {@link NoAuthenticationGuard} instead.
20+
* It's recommended to use the {@link JwtAuthenticationGuard}.
21+
*/
22+
const LOCAL_ENVIRONMENT_AUTHENTICATION_GUARD = JwtAuthenticationGuard;
23+
`,
24+
},
25+
{
26+
code: `
27+
import type { SomeType } from "./types";
28+
29+
/**
30+
* @see SomeType for more details
31+
*/
32+
const example = "test";
33+
`,
34+
},
35+
{
36+
code: `
37+
import type { Config } from "./config";
38+
39+
/**
40+
* @param {Config} config - The configuration object
41+
*/
42+
function setup(config: any) {}
1143
`,
1244
},
1345
],
@@ -29,6 +61,26 @@ import { a, b } from "./utils";
2961
import y from "package";
3062
3163
const c = a() + b + x() + y();
64+
`,
65+
},
66+
{
67+
code: `
68+
import type { UnusedType } from "./unused";
69+
import type { UsedInJSDoc } from "./used";
70+
71+
/**
72+
* Reference to {@link UsedInJSDoc}
73+
*/
74+
const example = "test";
75+
`,
76+
errors: ["'UnusedType' is defined but never used."],
77+
output: `
78+
import type { UsedInJSDoc } from "./used";
79+
80+
/**
81+
* Reference to {@link UsedInJSDoc}
82+
*/
83+
const example = "test";
3284
`,
3385
},
3486
],

src/rules/predicates.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,57 @@ export type Predicate = (
88
const commaFilter = { filter: (token: AST.Token) => token.value === "," };
99
const includeCommentsFilter = { includeComments: true };
1010

11+
/**
12+
* Check if an identifier is referenced in JSDoc comments
13+
* Looks for JSDoc tags like @link, @see, @type, etc.
14+
*/
15+
function isUsedInJSDoc(identifierName: string, sourceCode: SourceCode): boolean {
16+
const comments = sourceCode.getAllComments();
17+
18+
// JSDoc tags that can reference identifiers
19+
// Pattern matches: {@link Name}, {@see Name}, @type {Name}, etc.
20+
const jsdocPattern = new RegExp(
21+
// {@link Name} or @see Name
22+
`(?:@(?:link|linkcode|linkplain|see)\\s+${identifierName}\\b)|` +
23+
// {@link Name}
24+
`(?:\\{@(?:link|linkcode|linkplain)\\s+${identifierName}\\b\\})|` +
25+
// @type {Name}, @param {Name}, etc.
26+
`(?:[@{](?:type|typedef|param|returns?|template|augments|extends|implements)\\s+[^}]*\\b${identifierName}\\b)`,
27+
);
28+
29+
return comments.some((comment) => {
30+
// Only check block comments (/* ... */) as JSDoc uses block comment syntax
31+
if (comment.type !== "Block") {
32+
return false;
33+
}
34+
35+
return jsdocPattern.test(comment.value);
36+
});
37+
}
38+
1139
function makePredicate(
1240
isImport: boolean,
1341
addFixer?: (parent: any, sourceCode: SourceCode) => Partial<Rule.ReportDescriptor> | boolean,
1442
): Predicate {
1543
return (problem, context) => {
1644
const sourceCode = context.sourceCode || context.getSourceCode();
1745

18-
const { parent } =
46+
const node =
1947
(problem as any).node ??
2048
// typescript-eslint >= 7.8 sets a range instead of a node
2149
sourceCode.getNodeByRangeIndex(sourceCode.getIndexFromLoc((problem as any).loc.start));
50+
51+
const { parent } = node;
52+
53+
// Check if this is an import and if it's used in JSDoc comments
54+
if (parent && /^Import(|Default|Namespace)Specifier$/.test(parent.type) && isImport) {
55+
const identifierName = node.name;
56+
if (identifierName && isUsedInJSDoc(identifierName, sourceCode)) {
57+
// Don't report if used in JSDoc
58+
return false;
59+
}
60+
}
61+
2262
return parent
2363
? /^Import(|Default|Namespace)Specifier$/.test(parent.type) == isImport
2464
? Object.assign(problem, addFixer?.(parent, sourceCode))

0 commit comments

Comments
 (0)