Skip to content

Commit d46be35

Browse files
committedJul 10, 2024·
feat(require-template): add rule; fixes part of #1120
1 parent 1bb8aa5 commit d46be35

File tree

13 files changed

+692
-71
lines changed

13 files changed

+692
-71
lines changed
 

‎.README/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ non-default-recommended fixer).
236236
|:heavy_check_mark:|:wrench:|[check-tag-names](./docs/rules/check-tag-names.md#readme)|Reports invalid jsdoc (block) tag names|
237237
|:heavy_check_mark:|:wrench:|[check-types](./docs/rules/check-types.md#readme)|Reports types deemed invalid (customizable and with defaults, for preventing and/or recommending replacements)|
238238
|:heavy_check_mark:||[check-values](./docs/rules/check-values.md#readme)|Checks for expected content within some miscellaneous tags (`@version`, `@since`, `@license`, `@author`)|
239+
| || [convert-to-jsdoc-comments](./docs/rules/convert-to-jsdoc-comments.md#readme) | Converts line and block comments preceding or following specified nodes into JSDoc comments|
239240
|:heavy_check_mark:|:wrench:|[empty-tags](./docs/rules/empty-tags.md#readme)|Checks tags that are expected to be empty (e.g., `@abstract` or `@async`), reporting if they have content|
240241
|:heavy_check_mark:||[implements-on-classes](./docs/rules/implements-on-classes.md#readme)|Prohibits use of `@implements` on non-constructor functions (to enforce the tag only being used on classes/constructors)|
241242
|||[informative-docs](./docs/rules/informative-docs.md#readme)|Reports on JSDoc texts that serve only to restate their attached name.|
@@ -270,6 +271,7 @@ non-default-recommended fixer).
270271
|:heavy_check_mark:||[require-returns-check](./docs/rules/require-returns-check.md#readme)|Requires a return statement be present in a function body if a `@returns` tag is specified in the jsdoc comment block (and reports if multiple `@returns` tags are present).|
271272
|:heavy_check_mark:||[require-returns-description](./docs/rules/require-returns-description.md#readme)|Requires that the `@returns` tag has a `description` value (not including `void`/`undefined` type returns).|
272273
|:heavy_check_mark: (off in TS)||[require-returns-type](./docs/rules/require-returns-type.md#readme)|Requires that `@returns` tag has a type value (in curly brackets).|
274+
| || [require-template](./docs/rules/require-template.md#readme) | Requires `@template` tags be present when type parameters are used.|
273275
|||[require-throws](./docs/rules/require-throws.md#readme)|Requires that throw statements are documented|
274276
|:heavy_check_mark:||[require-yields](./docs/rules/require-yields.md#readme)|Requires that yields are documented|
275277
|:heavy_check_mark:||[require-yields-check](./docs/rules/require-yields-check.md#readme)|Ensures that if a `@yields` is present that a `yield` (or `yield` with a value) is present in the function body (or that if a `@next` is present that there is a `yield` with a return value present)|

‎.README/rules/require-template.md

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# `require-template`
2+
3+
Checks to see that `@template` tags are present for any detected type
4+
parameters.
5+
6+
Currently checks `TSTypeAliasDeclaration` such as:
7+
8+
```ts
9+
export type Pairs<D, V> = [D, V | undefined];
10+
```
11+
12+
or
13+
14+
```js
15+
/**
16+
* @typedef {[D, V | undefined]} Pairs
17+
*/
18+
```
19+
20+
Note that in the latter TypeScript-flavor JavaScript example, there is no way
21+
for us to firmly distinguish between `D` and `V` as type parameters or as some
22+
other identifiers, so we use an algorithm that any single capital letters
23+
are assumed to be templates.
24+
25+
## Options
26+
27+
### `requireSeparateTemplates`
28+
29+
Requires that each template have its own separate line, i.e., preventing
30+
templates of this format:
31+
32+
```js
33+
/**
34+
* @template T, U, V
35+
*/
36+
```
37+
38+
Defaults to `false`.
39+
40+
|||
41+
|---|---|
42+
|Context|everywhere|
43+
|Tags|`template`|
44+
|Recommended|true|
45+
|Settings||
46+
|Options|`requireSeparateTemplates`|
47+
48+
## Failing examples
49+
50+
<!-- assertions-failing requireTemplate -->
51+
52+
## Passing examples
53+
54+
<!-- assertions-passing requireTemplate -->

‎.github/workflows/feature.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ jobs:
4242
node_js_version:
4343
- '18'
4444
- '20'
45+
- '22'
4546
build:
4647
runs-on: ubuntu-latest
4748
name: Build

‎.ncurc.cjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
module.exports = {
44
reject: [
5-
// Todo: When package converted to ESM only
5+
// Todo: When our package converted to ESM only
66
'escape-string-regexp',
77

88
// todo[engine:node@>=20]: Can reenable

‎README.md

+2
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ non-default-recommended fixer).
263263
|:heavy_check_mark:|:wrench:|[check-tag-names](./docs/rules/check-tag-names.md#readme)|Reports invalid jsdoc (block) tag names|
264264
|:heavy_check_mark:|:wrench:|[check-types](./docs/rules/check-types.md#readme)|Reports types deemed invalid (customizable and with defaults, for preventing and/or recommending replacements)|
265265
|:heavy_check_mark:||[check-values](./docs/rules/check-values.md#readme)|Checks for expected content within some miscellaneous tags (`@version`, `@since`, `@license`, `@author`)|
266+
| || [convert-to-jsdoc-comments](./docs/rules/convert-to-jsdoc-comments.md#readme) | Converts line and block comments preceding or following specified nodes into JSDoc comments|
266267
|:heavy_check_mark:|:wrench:|[empty-tags](./docs/rules/empty-tags.md#readme)|Checks tags that are expected to be empty (e.g., `@abstract` or `@async`), reporting if they have content|
267268
|:heavy_check_mark:||[implements-on-classes](./docs/rules/implements-on-classes.md#readme)|Prohibits use of `@implements` on non-constructor functions (to enforce the tag only being used on classes/constructors)|
268269
|||[informative-docs](./docs/rules/informative-docs.md#readme)|Reports on JSDoc texts that serve only to restate their attached name.|
@@ -297,6 +298,7 @@ non-default-recommended fixer).
297298
|:heavy_check_mark:||[require-returns-check](./docs/rules/require-returns-check.md#readme)|Requires a return statement be present in a function body if a `@returns` tag is specified in the jsdoc comment block (and reports if multiple `@returns` tags are present).|
298299
|:heavy_check_mark:||[require-returns-description](./docs/rules/require-returns-description.md#readme)|Requires that the `@returns` tag has a `description` value (not including `void`/`undefined` type returns).|
299300
|:heavy_check_mark: (off in TS)||[require-returns-type](./docs/rules/require-returns-type.md#readme)|Requires that `@returns` tag has a type value (in curly brackets).|
301+
| || [require-template](./docs/rules/require-template.md#readme) | Requires `@template` tags be present when type parameters are used.|
300302
|||[require-throws](./docs/rules/require-throws.md#readme)|Requires that throw statements are documented|
301303
|:heavy_check_mark:||[require-yields](./docs/rules/require-yields.md#readme)|Requires that yields are documented|
302304
|:heavy_check_mark:||[require-yields-check](./docs/rules/require-yields-check.md#readme)|Ensures that if a `@yields` is present that a `yield` (or `yield` with a value) is present in the function body (or that if a `@next` is present that there is a `yield` with a return value present)|

‎docs/rules/require-template.md

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<a name="user-content-require-template"></a>
2+
<a name="require-template"></a>
3+
# <code>require-template</code>
4+
5+
Checks to see that `@template` tags are present for any detected type
6+
parameters.
7+
8+
Currently checks `TSTypeAliasDeclaration` such as:
9+
10+
```ts
11+
export type Pairs<D, V> = [D, V | undefined];
12+
```
13+
14+
or
15+
16+
```js
17+
/**
18+
* @typedef {[D, V | undefined]} Pairs
19+
*/
20+
```
21+
22+
Note that in the latter TypeScript-flavor JavaScript example, there is no way
23+
for us to firmly distinguish between `D` and `V` as type parameters or as some
24+
other identifiers, so we use an algorithm that any single capital letters
25+
are assumed to be templates.
26+
27+
<a name="user-content-require-template-options"></a>
28+
<a name="require-template-options"></a>
29+
## Options
30+
31+
<a name="user-content-require-template-options-requireseparatetemplates"></a>
32+
<a name="require-template-options-requireseparatetemplates"></a>
33+
### <code>requireSeparateTemplates</code>
34+
35+
Requires that each template have its own separate line, i.e., preventing
36+
templates of this format:
37+
38+
```js
39+
/**
40+
* @template T, U, V
41+
*/
42+
```
43+
44+
Defaults to `false`.
45+
46+
|||
47+
|---|---|
48+
|Context|everywhere|
49+
|Tags|`template`|
50+
|Recommended|true|
51+
|Settings||
52+
|Options|`requireSeparateTemplates`|
53+
54+
<a name="user-content-require-template-failing-examples"></a>
55+
<a name="require-template-failing-examples"></a>
56+
## Failing examples
57+
58+
The following patterns are considered problems:
59+
60+
````js
61+
/**
62+
*
63+
*/
64+
type Pairs<D, V> = [D, V | undefined];
65+
// Message: Missing @template D
66+
67+
/**
68+
*
69+
*/
70+
export type Pairs<D, V> = [D, V | undefined];
71+
// Message: Missing @template D
72+
73+
/**
74+
* @typedef {[D, V | undefined]} Pairs
75+
*/
76+
// Message: Missing @template D
77+
78+
/**
79+
* @typedef {[D, V | undefined]} Pairs
80+
*/
81+
// Settings: {"jsdoc":{"mode":"permissive"}}
82+
// Message: Missing @template D
83+
84+
/**
85+
* @template D, U
86+
*/
87+
export type Extras<D, U, V> = [D, U, V | undefined];
88+
// Message: Missing @template V
89+
90+
/**
91+
* @template D, U
92+
* @typedef {[D, U, V | undefined]} Extras
93+
*/
94+
// Message: Missing @template V
95+
96+
/**
97+
* @template D, V
98+
*/
99+
export type Pairs<D, V> = [D, V | undefined];
100+
// "jsdoc/require-template": ["error"|"warn", {"requireSeparateTemplates":true}]
101+
// Message: Missing separate @template for V
102+
103+
/**
104+
* @template D, V
105+
* @typedef {[D, V | undefined]} Pairs
106+
*/
107+
// "jsdoc/require-template": ["error"|"warn", {"requireSeparateTemplates":true}]
108+
// Message: Missing separate @template for V
109+
````
110+
111+
112+
113+
<a name="user-content-require-template-passing-examples"></a>
114+
<a name="require-template-passing-examples"></a>
115+
## Passing examples
116+
117+
The following patterns are not considered problems:
118+
119+
````js
120+
/**
121+
* @template D
122+
* @template V
123+
*/
124+
export type Pairs<D, V> = [D, V | undefined];
125+
126+
/**
127+
* @template D
128+
* @template V
129+
* @typedef {[D, V | undefined]} Pairs
130+
*/
131+
132+
/**
133+
* @template D, U, V
134+
*/
135+
export type Extras<D, U, V> = [D, U, V | undefined];
136+
137+
/**
138+
* @template D, U, V
139+
* @typedef {[D, U, V | undefined]} Extras
140+
*/
141+
142+
/**
143+
* @typedef {[D, U, V | undefined]} Extras
144+
* @typedef {[D, U, V | undefined]} Extras
145+
*/
146+
````
147+

‎package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"url": "http://gajus.com"
66
},
77
"dependencies": {
8-
"@es-joy/jsdoccomment": "~0.45.0",
8+
"@es-joy/jsdoccomment": "~0.46.0",
99
"are-docs-informative": "^0.0.2",
1010
"comment-parser": "1.4.1",
1111
"debug": "^4.3.5",
@@ -54,7 +54,7 @@
5454
"eslint": "9.6.0",
5555
"eslint-config-canonical": "~43.0.13",
5656
"espree": "^10.1.0",
57-
"gitdown": "^3.1.5",
57+
"gitdown": "^4.0.0",
5858
"glob": "^10.4.2",
5959
"globals": "^15.8.0",
6060
"husky": "^9.0.11",

‎pnpm-lock.yaml

+149-66
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/bin/generateDocs.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,8 @@ const generateDocs = async () => {
149149
);
150150
}),
151151
...otherPaths,
152-
].map((docPath) => {
153-
const gitdown = Gitdown.readFile(docPath);
152+
].map(async (docPath) => {
153+
const gitdown = await Gitdown.readFile(docPath);
154154

155155
gitdown.setConfig({
156156
gitinfo: {

‎src/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import requireReturns from './rules/requireReturns.js';
4545
import requireReturnsCheck from './rules/requireReturnsCheck.js';
4646
import requireReturnsDescription from './rules/requireReturnsDescription.js';
4747
import requireReturnsType from './rules/requireReturnsType.js';
48+
import requireTemplate from './rules/requireTemplate.js';
4849
import requireThrows from './rules/requireThrows.js';
4950
import requireYields from './rules/requireYields.js';
5051
import requireYieldsCheck from './rules/requireYieldsCheck.js';
@@ -118,6 +119,7 @@ const index = {
118119
'require-returns-check': requireReturnsCheck,
119120
'require-returns-description': requireReturnsDescription,
120121
'require-returns-type': requireReturnsType,
122+
'require-template': requireTemplate,
121123
'require-throws': requireThrows,
122124
'require-yields': requireYields,
123125
'require-yields-check': requireYieldsCheck,
@@ -191,6 +193,7 @@ const createRecommendedRuleset = (warnOrError, flatName) => {
191193
'jsdoc/require-returns-check': warnOrError,
192194
'jsdoc/require-returns-description': warnOrError,
193195
'jsdoc/require-returns-type': warnOrError,
196+
'jsdoc/require-template': 'off',
194197
'jsdoc/require-throws': 'off',
195198
'jsdoc/require-yields': warnOrError,
196199
'jsdoc/require-yields-check': warnOrError,

‎src/rules/requireTemplate.js

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

‎test/rules/ruleNames.json

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"require-returns-check",
4747
"require-returns-description",
4848
"require-returns-type",
49+
"require-template",
4950
"require-throws",
5051
"require-yields",
5152
"require-yields-check",

0 commit comments

Comments
 (0)
Please sign in to comment.