Skip to content

Commit 962d99d

Browse files
committedJul 13, 2024·
feat(check-template-names): add rule; fixes #1120
1 parent 7e311ab commit 962d99d

File tree

6 files changed

+471
-0
lines changed

6 files changed

+471
-0
lines changed
 

‎.README/rules/check-template-names.md

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# `check-template-names`
2+
3+
Checks that any `@template` names are actually used in the connected
4+
`@typedef` or type alias.
5+
6+
Currently checks `TSTypeAliasDeclaration` such as:
7+
8+
```ts
9+
/**
10+
* @template D
11+
* @template V
12+
*/
13+
export type Pairs<D, V> = [D, V | undefined];
14+
```
15+
16+
or
17+
18+
```js
19+
/**
20+
* @template D
21+
* @template V
22+
* @typedef {[D, V | undefined]} Pairs
23+
*/
24+
```
25+
26+
|||
27+
|---|---|
28+
|Context|everywhere|
29+
|Tags|`@template`|
30+
|Recommended|false|
31+
|Settings||
32+
|Options||
33+
34+
## Failing examples
35+
36+
<!-- assertions-failing checkTemplateNames -->
37+
38+
## Passing examples
39+
40+
<!-- assertions-passing checkTemplateNames -->

‎docs/rules/check-template-names.md

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<a name="user-content-check-template-names"></a>
2+
<a name="check-template-names"></a>
3+
# <code>check-template-names</code>
4+
5+
Checks that any `@template` names are actually used in the connected
6+
`@typedef` or type alias.
7+
8+
Currently checks `TSTypeAliasDeclaration` such as:
9+
10+
```ts
11+
/**
12+
* @template D
13+
* @template V
14+
*/
15+
export type Pairs<D, V> = [D, V | undefined];
16+
```
17+
18+
or
19+
20+
```js
21+
/**
22+
* @template D
23+
* @template V
24+
* @typedef {[D, V | undefined]} Pairs
25+
*/
26+
```
27+
28+
|||
29+
|---|---|
30+
|Context|everywhere|
31+
|Tags|`@template`|
32+
|Recommended|false|
33+
|Settings||
34+
|Options||
35+
36+
<a name="user-content-check-template-names-failing-examples"></a>
37+
<a name="check-template-names-failing-examples"></a>
38+
## Failing examples
39+
40+
The following patterns are considered problems:
41+
42+
````js
43+
/**
44+
* @template D
45+
* @template V
46+
*/
47+
type Pairs<X, Y> = [X, Y | undefined];
48+
// Message: @template D not in use
49+
50+
/**
51+
* @template D
52+
* @template V
53+
*/
54+
export type Pairs<X, Y> = [X, Y | undefined];
55+
// Message: @template D not in use
56+
57+
/**
58+
* @template D
59+
* @template V
60+
* @typedef {[X, Y | undefined]} Pairs
61+
*/
62+
// Message: @template D not in use
63+
64+
/**
65+
* @template D
66+
* @template V
67+
*/
68+
export type Pairs = [number, undefined];
69+
// Message: @template D not in use
70+
71+
/**
72+
* @template D
73+
* @template V
74+
* @typedef {[undefined]} Pairs
75+
*/
76+
// Settings: {"jsdoc":{"mode":"permissive"}}
77+
// Message: @template D not in use
78+
79+
/**
80+
* @template D, U, V
81+
*/
82+
export type Extras<D, U> = [D, U | undefined];
83+
// Message: @template V not in use
84+
85+
/**
86+
* @template D, U, V
87+
* @typedef {[D, U | undefined]} Extras
88+
*/
89+
// Message: @template V not in use
90+
````
91+
92+
93+
94+
<a name="user-content-check-template-names-passing-examples"></a>
95+
<a name="check-template-names-passing-examples"></a>
96+
## Passing examples
97+
98+
The following patterns are not considered problems:
99+
100+
````js
101+
/**
102+
* @template D
103+
* @template V
104+
*/
105+
export type Pairs<D, V> = [D, V | undefined];
106+
107+
/**
108+
* @template D
109+
* @template V
110+
* @typedef {[D, V | undefined]} Pairs
111+
*/
112+
113+
/**
114+
* @template D, U, V
115+
*/
116+
export type Extras<D, U, V> = [D, U, V | undefined];
117+
118+
/**
119+
* @template D, U, V
120+
* @typedef {[D, U, V | undefined]} Extras
121+
*/
122+
123+
/**
124+
* @template X
125+
* @typedef {[D, U, V | undefined]} Extras
126+
* @typedef {[D, U, V | undefined]} Extras
127+
*/
128+
````
129+

‎src/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import checkParamNames from './rules/checkParamNames.js';
77
import checkPropertyNames from './rules/checkPropertyNames.js';
88
import checkSyntax from './rules/checkSyntax.js';
99
import checkTagNames from './rules/checkTagNames.js';
10+
import checkTemplateNames from './rules/checkTemplateNames.js';
1011
import checkTypes from './rules/checkTypes.js';
1112
import checkValues from './rules/checkValues.js';
1213
import convertToJsdocComments from './rules/convertToJsdocComments.js';
@@ -81,6 +82,7 @@ const index = {
8182
'check-property-names': checkPropertyNames,
8283
'check-syntax': checkSyntax,
8384
'check-tag-names': checkTagNames,
85+
'check-template-names': checkTemplateNames,
8486
'check-types': checkTypes,
8587
'check-values': checkValues,
8688
'convert-to-jsdoc-comments': convertToJsdocComments,
@@ -155,6 +157,7 @@ const createRecommendedRuleset = (warnOrError, flatName) => {
155157
'jsdoc/check-property-names': warnOrError,
156158
'jsdoc/check-syntax': 'off',
157159
'jsdoc/check-tag-names': warnOrError,
160+
'jsdoc/check-template-names': 'off',
158161
'jsdoc/check-types': warnOrError,
159162
'jsdoc/check-values': warnOrError,
160163
'jsdoc/convert-to-jsdoc-comments': 'off',

‎src/rules/checkTemplateNames.js

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import {
2+
parse as parseType,
3+
traverse,
4+
tryParse as tryParseType,
5+
} from '@es-joy/jsdoccomment';
6+
import iterateJsdoc from '../iterateJsdoc.js';
7+
8+
export default iterateJsdoc(({
9+
context,
10+
utils,
11+
node,
12+
settings,
13+
report,
14+
}) => {
15+
const {
16+
mode
17+
} = settings;
18+
19+
const templateTags = utils.getTags('template');
20+
21+
const usedNames = new Set();
22+
/**
23+
* @param {import('@typescript-eslint/types').TSESTree.TSTypeAliasDeclaration} aliasDeclaration
24+
*/
25+
const checkParameters = (aliasDeclaration) => {
26+
/* c8 ignore next -- Guard */
27+
const {params} = aliasDeclaration.typeParameters ?? {params: []};
28+
for (const {name: {name}} of params) {
29+
usedNames.add(name);
30+
}
31+
for (const tag of templateTags) {
32+
const {name} = tag;
33+
const names = name.split(/,\s*/);
34+
for (const name of names) {
35+
if (!usedNames.has(name)) {
36+
report(`@template ${name} not in use`, null, tag);
37+
}
38+
}
39+
}
40+
};
41+
42+
const handleTypeAliases = () => {
43+
const nde = /** @type {import('@typescript-eslint/types').TSESTree.Node} */ (
44+
node
45+
);
46+
if (!nde) {
47+
return;
48+
}
49+
switch (nde.type) {
50+
case 'ExportNamedDeclaration':
51+
if (nde.declaration?.type === 'TSTypeAliasDeclaration') {
52+
checkParameters(nde.declaration);
53+
}
54+
break;
55+
case 'TSTypeAliasDeclaration':
56+
checkParameters(nde);
57+
break;
58+
}
59+
};
60+
61+
const typedefTags = utils.getTags('typedef');
62+
if (!typedefTags.length || typedefTags.length >= 2) {
63+
handleTypeAliases();
64+
return;
65+
}
66+
67+
const potentialType = typedefTags[0].type;
68+
const parsedType = mode === 'permissive' ?
69+
tryParseType(/** @type {string} */ (potentialType)) :
70+
parseType(/** @type {string} */ (potentialType), mode)
71+
72+
traverse(parsedType, (nde) => {
73+
const {
74+
type,
75+
value,
76+
} = /** @type {import('jsdoc-type-pratt-parser').NameResult} */ (nde);
77+
if (type === 'JsdocTypeName' && (/^[A-Z]$/).test(value)) {
78+
usedNames.add(value);
79+
}
80+
});
81+
82+
for (const tag of templateTags) {
83+
const {name} = tag;
84+
const names = name.split(/,\s*/);
85+
for (const name of names) {
86+
if (!usedNames.has(name)) {
87+
report(`@template ${name} not in use`, null, tag);
88+
}
89+
}
90+
}
91+
}, {
92+
iterateAllJsdocs: true,
93+
meta: {
94+
docs: {
95+
description: 'Checks that any `@template` names are actually used in the connected `@typedef` or type alias.',
96+
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-template.md#repos-sticky-header',
97+
},
98+
schema: [],
99+
type: 'suggestion',
100+
},
101+
});
+197
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import {parser as typescriptEslintParser} from 'typescript-eslint';
2+
3+
export default {
4+
invalid: [
5+
{
6+
code: `
7+
/**
8+
* @template D
9+
* @template V
10+
*/
11+
type Pairs<X, Y> = [X, Y | undefined];
12+
`,
13+
errors: [
14+
{
15+
line: 3,
16+
message: '@template D not in use',
17+
},
18+
{
19+
line: 4,
20+
message: '@template V not in use',
21+
},
22+
],
23+
languageOptions: {
24+
parser: typescriptEslintParser
25+
},
26+
},
27+
{
28+
code: `
29+
/**
30+
* @template D
31+
* @template V
32+
*/
33+
export type Pairs<X, Y> = [X, Y | undefined];
34+
`,
35+
errors: [
36+
{
37+
line: 3,
38+
message: '@template D not in use',
39+
},
40+
{
41+
line: 4,
42+
message: '@template V not in use',
43+
},
44+
],
45+
languageOptions: {
46+
parser: typescriptEslintParser
47+
},
48+
},
49+
{
50+
code: `
51+
/**
52+
* @template D
53+
* @template V
54+
* @typedef {[X, Y | undefined]} Pairs
55+
*/
56+
`,
57+
errors: [
58+
{
59+
line: 3,
60+
message: '@template D not in use',
61+
},
62+
{
63+
line: 4,
64+
message: '@template V not in use',
65+
},
66+
],
67+
},
68+
{
69+
code: `
70+
/**
71+
* @template D
72+
* @template V
73+
*/
74+
export type Pairs = [number, undefined];
75+
`,
76+
errors: [
77+
{
78+
line: 3,
79+
message: '@template D not in use',
80+
},
81+
{
82+
line: 4,
83+
message: '@template V not in use',
84+
},
85+
],
86+
languageOptions: {
87+
parser: typescriptEslintParser
88+
},
89+
},
90+
{
91+
code: `
92+
/**
93+
* @template D
94+
* @template V
95+
* @typedef {[undefined]} Pairs
96+
*/
97+
`,
98+
errors: [
99+
{
100+
line: 3,
101+
message: '@template D not in use',
102+
},
103+
{
104+
line: 4,
105+
message: '@template V not in use',
106+
},
107+
],
108+
settings: {
109+
jsdoc: {
110+
mode: 'permissive',
111+
},
112+
},
113+
},
114+
{
115+
code: `
116+
/**
117+
* @template D, U, V
118+
*/
119+
export type Extras<D, U> = [D, U | undefined];
120+
`,
121+
errors: [
122+
{
123+
line: 3,
124+
message: '@template V not in use',
125+
},
126+
],
127+
languageOptions: {
128+
parser: typescriptEslintParser
129+
},
130+
},
131+
{
132+
code: `
133+
/**
134+
* @template D, U, V
135+
* @typedef {[D, U | undefined]} Extras
136+
*/
137+
`,
138+
errors: [
139+
{
140+
line: 3,
141+
message: '@template V not in use',
142+
},
143+
],
144+
},
145+
],
146+
valid: [
147+
{
148+
code: `
149+
/**
150+
* @template D
151+
* @template V
152+
*/
153+
export type Pairs<D, V> = [D, V | undefined];
154+
`,
155+
languageOptions: {
156+
parser: typescriptEslintParser
157+
},
158+
},
159+
{
160+
code: `
161+
/**
162+
* @template D
163+
* @template V
164+
* @typedef {[D, V | undefined]} Pairs
165+
*/
166+
`,
167+
},
168+
{
169+
code: `
170+
/**
171+
* @template D, U, V
172+
*/
173+
export type Extras<D, U, V> = [D, U, V | undefined];
174+
`,
175+
languageOptions: {
176+
parser: typescriptEslintParser
177+
},
178+
},
179+
{
180+
code: `
181+
/**
182+
* @template D, U, V
183+
* @typedef {[D, U, V | undefined]} Extras
184+
*/
185+
`,
186+
},
187+
{
188+
code: `
189+
/**
190+
* @template X
191+
* @typedef {[D, U, V | undefined]} Extras
192+
* @typedef {[D, U, V | undefined]} Extras
193+
*/
194+
`,
195+
},
196+
],
197+
};

‎test/rules/ruleNames.json

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"check-property-names",
99
"check-syntax",
1010
"check-tag-names",
11+
"check-template-names",
1112
"check-types",
1213
"check-values",
1314
"convert-to-jsdoc-comments",

0 commit comments

Comments
 (0)
Please sign in to comment.