eslint-plugin-jsdoc 64.3.0 → 64.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/getJsdocProcessorPlugin.d.ts +0 -1
- package/dist/index.d.ts +6 -5
- package/dist/rules.d.ts +4 -0
- package/package.json +3 -2
- package/src/getJsdocProcessorPlugin.js +0 -3
- package/src/index.js +4 -4
- package/src/iterateJsdoc.js +5 -6
- package/src/jsdocUtils.js +1 -3
- package/src/rules/checkTypes.js +7 -2
- package/src/rules/noUnnecessaryTypeAssertion.js +362 -65
- package/src/rules/preferImportTag.js +1 -0
- package/src/rules.d.ts +4 -0
package/README.md
CHANGED
|
@@ -512,7 +512,7 @@ non-default-recommended fixer).
|
|
|
512
512
|
||| [no-restricted-syntax](./docs/rules/no-restricted-syntax.md#readme) | Reports when certain comment structures are present. |
|
|
513
513
|
|On in TS; Off in TS flavor|:wrench:| [no-types](./docs/rules/no-types.md#readme) | This rule reports types being used on `@param` or `@returns` (redundant with TypeScript). |
|
|
514
514
|
|:heavy_check_mark: (Off in TS; Off in TS flavor)|| [no-undefined-types](./docs/rules/no-undefined-types.md#readme) | Besides some expected built-in types, prohibits any types not specified as globals or within `@typedef`. |
|
|
515
|
-
|
|
515
|
+
||:wrench:| [no-unnecessary-type-assertion](./docs/rules/no-unnecessary-type-assertion.md#readme) | Reports redundant @type tags that match or broaden the naturally inferred TypeScript type. |
|
|
516
516
|
||:wrench:| [normalize-see-links](./docs/rules/normalize-see-links.md#readme) | Normalizes labeled links in `@see` tags to a canonical `{@link}` form. |
|
|
517
517
|
||:wrench:| [prefer-import-tag](./docs/rules/prefer-import-tag.md#readme) | Prefer `@import` tags to inline `import()` statements. |
|
|
518
518
|
|:heavy_check_mark:|| [reject-any-type](./docs/rules/reject-any-type.md#readme) | Reports use of `any` or `*` type |
|
package/dist/index.d.ts
CHANGED
|
@@ -86,13 +86,16 @@ export type ConfigGroups = "recommended" | "stylistic" | "contents" | "logical"
|
|
|
86
86
|
export type ConfigVariants = "" | "-typescript" | "-typescript-flavor";
|
|
87
87
|
export type ErrorLevelVariants = "" | "-error";
|
|
88
88
|
export type FlatConfigNames = `flat/${ConfigGroups}${ConfigVariants}${ErrorLevelVariants}` | "flat/recommended-tsdoc" | "flat/recommended-tsdoc-error";
|
|
89
|
+
export type JsdocPlugin = import("eslint").ESLint.Plugin & {
|
|
90
|
+
configs: Record<FlatConfigNames, import("eslint").Linter.Config> & Record<"examples" | "default-expressions" | "examples-and-default-expressions", import("eslint").Linter.Config[]> & Record<"flat/recommended-mixed", import("eslint").Linter.Config[]>;
|
|
91
|
+
};
|
|
89
92
|
/**
|
|
90
93
|
* @typedef {"recommended" | "stylistic" | "contents" | "logical" | "requirements"} ConfigGroups
|
|
91
94
|
* @typedef {"" | "-typescript" | "-typescript-flavor"} ConfigVariants
|
|
92
95
|
* @typedef {"" | "-error"} ErrorLevelVariants
|
|
93
96
|
* @typedef {`flat/${ConfigGroups}${ConfigVariants}${ErrorLevelVariants}` |
|
|
94
97
|
* "flat/recommended-tsdoc" | "flat/recommended-tsdoc-error"} FlatConfigNames
|
|
95
|
-
* @
|
|
98
|
+
* @typedef {import('eslint').ESLint.Plugin & {
|
|
96
99
|
* configs: Record<
|
|
97
100
|
* FlatConfigNames,
|
|
98
101
|
* import('eslint').Linter.Config
|
|
@@ -102,9 +105,7 @@ export type FlatConfigNames = `flat/${ConfigGroups}${ConfigVariants}${ErrorLevel
|
|
|
102
105
|
* import('eslint').Linter.Config[]
|
|
103
106
|
* > &
|
|
104
107
|
* Record<"flat/recommended-mixed", import('eslint').Linter.Config[]>
|
|
105
|
-
* }}
|
|
108
|
+
* }} JsdocPlugin
|
|
106
109
|
*/
|
|
107
|
-
declare const index:
|
|
108
|
-
configs: Record<FlatConfigNames, import("eslint").Linter.Config> & Record<"examples" | "default-expressions" | "examples-and-default-expressions", import("eslint").Linter.Config[]> & Record<"flat/recommended-mixed", import("eslint").Linter.Config[]>;
|
|
109
|
-
};
|
|
110
|
+
declare const index: JsdocPlugin;
|
|
110
111
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/rules.d.ts
CHANGED
|
@@ -1320,6 +1320,10 @@ export interface Rules {
|
|
|
1320
1320
|
* Whether to check `const` type assertions as redundant
|
|
1321
1321
|
*/
|
|
1322
1322
|
checkLiteralConstAssertions?: boolean;
|
|
1323
|
+
/**
|
|
1324
|
+
* Whether to enable the fixer that removes the redundant `@type` tag (and the JSDoc block if it becomes empty). Defaults to `true`.
|
|
1325
|
+
*/
|
|
1326
|
+
enableFixer?: boolean;
|
|
1323
1327
|
/**
|
|
1324
1328
|
* Whether to treat `any` type casts as redundant
|
|
1325
1329
|
*/
|
package/package.json
CHANGED
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"semantic-release": "^25.0.9",
|
|
70
70
|
"sinon": "^22.1.0",
|
|
71
71
|
"ts-api-utils": "^2.5.0",
|
|
72
|
-
"typescript": "
|
|
72
|
+
"typescript": "6.0.3",
|
|
73
73
|
"typescript-eslint": "8.67.0"
|
|
74
74
|
},
|
|
75
75
|
"engines": {
|
|
@@ -139,6 +139,7 @@
|
|
|
139
139
|
"scripts": {
|
|
140
140
|
"ruleTypes": "node ./src/bin/generateRuleTypes.js",
|
|
141
141
|
"tsc": "tsc",
|
|
142
|
+
"tsc:ts7": "npx -y -p typescript@^7 -c \"tsc\"",
|
|
142
143
|
"tsc-build": "tsc -p tsconfig-prod.json",
|
|
143
144
|
"build": "rimraf ./dist && mkdir dist && pnpm tsc-build && cp src/rules.d.ts dist/rules.d.ts",
|
|
144
145
|
"attw": "attw --profile esm-only --pack .",
|
|
@@ -156,5 +157,5 @@
|
|
|
156
157
|
"test-cov": "TIMING=1 c8 --reporter text pnpm run test-no-cov",
|
|
157
158
|
"test-index": "pnpm run test-no-cov test/rules/index.js"
|
|
158
159
|
},
|
|
159
|
-
"version": "64.3.
|
|
160
|
+
"version": "64.3.2"
|
|
160
161
|
}
|
package/src/index.js
CHANGED
|
@@ -89,7 +89,7 @@ import semver from 'semver';
|
|
|
89
89
|
* @typedef {"" | "-error"} ErrorLevelVariants
|
|
90
90
|
* @typedef {`flat/${ConfigGroups}${ConfigVariants}${ErrorLevelVariants}` |
|
|
91
91
|
* "flat/recommended-tsdoc" | "flat/recommended-tsdoc-error"} FlatConfigNames
|
|
92
|
-
* @
|
|
92
|
+
* @typedef {import('eslint').ESLint.Plugin & {
|
|
93
93
|
* configs: Record<
|
|
94
94
|
* FlatConfigNames,
|
|
95
95
|
* import('eslint').Linter.Config
|
|
@@ -99,10 +99,10 @@ import semver from 'semver';
|
|
|
99
99
|
* import('eslint').Linter.Config[]
|
|
100
100
|
* > &
|
|
101
101
|
* Record<"flat/recommended-mixed", import('eslint').Linter.Config[]>
|
|
102
|
-
* }}
|
|
102
|
+
* }} JsdocPlugin
|
|
103
103
|
*/
|
|
104
|
-
const index = {};
|
|
105
|
-
index.configs = {};
|
|
104
|
+
const index = /** @type {JsdocPlugin} */ ({});
|
|
105
|
+
index.configs = /** @type {typeof index.configs} */ ({});
|
|
106
106
|
index.rules = {
|
|
107
107
|
'check-access': checkAccess,
|
|
108
108
|
'check-alignment': checkAlignment,
|
package/src/iterateJsdoc.js
CHANGED
|
@@ -192,10 +192,10 @@ import esquery from 'esquery';
|
|
|
192
192
|
*/
|
|
193
193
|
|
|
194
194
|
/**
|
|
195
|
-
* @
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
195
|
+
* @typedef {(
|
|
196
|
+
* tag: import('comment-parser').Spec,
|
|
197
|
+
* ...tokens: Partial<import('comment-parser').Tokens>[]
|
|
198
|
+
* ) => void} ChangeTag
|
|
199
199
|
*/
|
|
200
200
|
|
|
201
201
|
/**
|
|
@@ -624,8 +624,7 @@ const getBasicUtils = (context, {
|
|
|
624
624
|
mode,
|
|
625
625
|
tagNamePreference,
|
|
626
626
|
}) => {
|
|
627
|
-
/** @type {BasicUtils} */
|
|
628
|
-
const utils = {};
|
|
627
|
+
const utils = /** @type {BasicUtils} */ ({});
|
|
629
628
|
|
|
630
629
|
for (const method of [
|
|
631
630
|
'isNameOrNamepathDefiningTag',
|
package/src/jsdocUtils.js
CHANGED
|
@@ -368,9 +368,7 @@ const getFunctionParameterNames = (
|
|
|
368
368
|
/** @type {import('estree').Identifier} */ (
|
|
369
369
|
param.key
|
|
370
370
|
).name,
|
|
371
|
-
|
|
372
|
-
param.value
|
|
373
|
-
).right.properties.map((prop) => {
|
|
371
|
+
param.value.right.properties.map((prop) => {
|
|
374
372
|
return /** @type {string} */ (getParamName(
|
|
375
373
|
/** @type {import('estree').Property} */
|
|
376
374
|
(prop),
|
package/src/rules/checkTypes.js
CHANGED
|
@@ -44,9 +44,14 @@ const checkNativeTypes = (preferredTypes, typeNodeName, preferred, parentNode, i
|
|
|
44
44
|
parentNode
|
|
45
45
|
)?.left?.type === 'JsdocTypeName' &&
|
|
46
46
|
/**
|
|
47
|
-
* @type {import('jsdoc-type-pratt-parser').
|
|
47
|
+
* @type {import('jsdoc-type-pratt-parser').NameResult}
|
|
48
48
|
*/
|
|
49
|
-
(
|
|
49
|
+
(
|
|
50
|
+
/**
|
|
51
|
+
* @type {import('jsdoc-type-pratt-parser').GenericResult}
|
|
52
|
+
*/
|
|
53
|
+
(parentNode)?.left
|
|
54
|
+
)?.value === 'Object'
|
|
50
55
|
)
|
|
51
56
|
)
|
|
52
57
|
) {
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
/* eslint-disable no-bitwise -- Convenient */
|
|
1
2
|
/* eslint-disable jsdoc/reject-any-type -- TypeScript types need not be installed */
|
|
2
3
|
import iterateJsdoc from '../iterateJsdoc.js';
|
|
3
4
|
import {
|
|
4
5
|
createRequire,
|
|
5
6
|
} from 'module';
|
|
6
7
|
|
|
8
|
+
let warned = false;
|
|
9
|
+
|
|
7
10
|
/** @type {any} */
|
|
8
11
|
let ts;
|
|
9
12
|
|
|
@@ -37,7 +40,7 @@ const isLiteralType = (type) => {
|
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
// Check using TypeFlags for specific edge cases like boolean or template literals
|
|
40
|
-
|
|
43
|
+
|
|
41
44
|
if (type.flags & (ts.TypeFlags.BooleanLiteral | ts.TypeFlags.TemplateLiteral)) {
|
|
42
45
|
return true;
|
|
43
46
|
}
|
|
@@ -59,9 +62,13 @@ const isLiteralType = (type) => {
|
|
|
59
62
|
|
|
60
63
|
export default iterateJsdoc(({
|
|
61
64
|
context,
|
|
65
|
+
jsdoc,
|
|
66
|
+
jsdocNode,
|
|
62
67
|
node: nde,
|
|
63
68
|
report,
|
|
69
|
+
sourceCode,
|
|
64
70
|
utils,
|
|
71
|
+
// eslint-disable-next-line complexity -- Numerous type/option permutations
|
|
65
72
|
}) => {
|
|
66
73
|
/* c8 ignore next 4 -- Guard */
|
|
67
74
|
// Already handled
|
|
@@ -79,10 +86,41 @@ export default iterateJsdoc(({
|
|
|
79
86
|
const {
|
|
80
87
|
// https://typescript-eslint.io/rules/no-unnecessary-type-assertion/
|
|
81
88
|
checkLiteralConstAssertions = false,
|
|
89
|
+
enableFixer = true,
|
|
82
90
|
treatAnyAsRedundant = false,
|
|
83
91
|
typesToIgnore = [],
|
|
84
92
|
} = context.options[0] ?? {};
|
|
85
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Removes the redundant `@type` tag, deleting the whole JSDoc block if it
|
|
96
|
+
* is left empty.
|
|
97
|
+
* @returns {void}
|
|
98
|
+
*/
|
|
99
|
+
const removeType = () => {
|
|
100
|
+
utils.removeTag(jsdoc.tags.indexOf(/** @type {any} */ (types[0])), {
|
|
101
|
+
removeEmptyBlock: true,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// `removeTag` only drops the enclosing block for a single-line comment; for
|
|
105
|
+
// a multi-line block whose sole content was the `@type` tag, clear what is
|
|
106
|
+
// left (only the delimiter lines) so the now-empty comment is removed too.
|
|
107
|
+
const blockIsEmpty = jsdoc.source.every(({
|
|
108
|
+
tokens: {
|
|
109
|
+
description,
|
|
110
|
+
name,
|
|
111
|
+
tag,
|
|
112
|
+
type,
|
|
113
|
+
},
|
|
114
|
+
}) => {
|
|
115
|
+
return !tag && !type && !name && !description.trim();
|
|
116
|
+
});
|
|
117
|
+
if (blockIsEmpty) {
|
|
118
|
+
jsdoc.source.splice(0);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const fixer = enableFixer ? removeType : null;
|
|
123
|
+
|
|
86
124
|
const node =
|
|
87
125
|
/**
|
|
88
126
|
* @type {import('@typescript-eslint/utils').TSESTree.Node}
|
|
@@ -94,11 +132,15 @@ export default iterateJsdoc(({
|
|
|
94
132
|
* @type {import('@typescript-eslint/utils').ParserServices}
|
|
95
133
|
*/ (context.sourceCode.parserServices);
|
|
96
134
|
|
|
97
|
-
/* c8 ignore next
|
|
135
|
+
/* c8 ignore next 10 -- Guard */
|
|
98
136
|
if (!services || !services.program) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
137
|
+
if (!warned) {
|
|
138
|
+
// Cannot proceed without type-aware linting enabled
|
|
139
|
+
// eslint-disable-next-line no-console -- Feedback
|
|
140
|
+
console.warn('⚠️ You must point ESLint to the `typescript-eslint` parser using `languageOptions`. See the documentation for `jsdoc/no-unnecessary-type-assertion`.');
|
|
141
|
+
warned = true;
|
|
142
|
+
}
|
|
143
|
+
|
|
102
144
|
return;
|
|
103
145
|
}
|
|
104
146
|
|
|
@@ -106,37 +148,201 @@ export default iterateJsdoc(({
|
|
|
106
148
|
// const services = ESLintUtils.getParserServices(context);
|
|
107
149
|
const checker = services.program.getTypeChecker();
|
|
108
150
|
|
|
151
|
+
const assertedTypeStr = types[0].type;
|
|
152
|
+
|
|
153
|
+
const message = assertedTypeStr === 'const' ?
|
|
154
|
+
'The @type tag declaring "{{ type }}" is redundant as TypeScript infers it automatically for literals.' :
|
|
155
|
+
'The @type tag declaring "{{ type }}" is redundant as TypeScript infers it automatically.';
|
|
156
|
+
|
|
109
157
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
158
|
+
* Whether `inferredType` is a generic reference carrying `any` type arguments
|
|
159
|
+
* that `assertedType` replaces with concrete ones (e.g. an untyped
|
|
160
|
+
* `document.querySelectorAll(sel)` giving `NodeListOf<any>`, asserted as
|
|
161
|
+
* `NodeListOf<HTMLElement>`, or `new Map()` giving `Map<any, any>`). Such an
|
|
162
|
+
* assertion supplies real type information, so it is not redundant even though
|
|
163
|
+
* `any` leaves the two types mutually assignable.
|
|
164
|
+
* @param {any} inferredType `ts.Type`
|
|
165
|
+
* @param {any} assertedType `ts.Type`
|
|
166
|
+
* @returns {boolean}
|
|
112
167
|
*/
|
|
113
|
-
const
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
168
|
+
const tightensAnyTypeArgument = (inferredType, assertedType) => {
|
|
169
|
+
// Only ever called for an object type, which always carries `objectFlags`.
|
|
170
|
+
if ((inferredType.objectFlags & ts.ObjectFlags.Reference) === 0) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const assertedTypeArguments = checker.getTypeArguments(assertedType);
|
|
175
|
+
|
|
176
|
+
return checker.getTypeArguments(inferredType).some((inferredTypeArgument, index) => {
|
|
177
|
+
return (inferredTypeArgument.flags & ts.TypeFlags.Any) !== 0 &&
|
|
178
|
+
assertedTypeArguments[index] !== undefined &&
|
|
179
|
+
(assertedTypeArguments[index].flags & ts.TypeFlags.Any) === 0;
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* A generic call/`new` expression takes its type arguments partly from the
|
|
185
|
+
* surrounding contextual type, which under a `@type` (a cast, or a
|
|
186
|
+
* declaration) is the asserted type itself. `getTypeAtLocation` then just
|
|
187
|
+
* echoes the asserted type back, so a genuine tightening looks redundant
|
|
188
|
+
* (`document.querySelectorAll(sel)` is really `NodeListOf<Element>`, not the
|
|
189
|
+
* asserted `NodeListOf<HTMLElement>`). The uncontaminated type cannot be
|
|
190
|
+
* recovered here, so such expressions are left alone.
|
|
191
|
+
* @param {any} tsExpression `ts.Node`
|
|
192
|
+
* @returns {boolean}
|
|
193
|
+
*/
|
|
194
|
+
const isGenericCall = (tsExpression) => {
|
|
195
|
+
if (!ts.isCallExpression(tsExpression) && !ts.isNewExpression(tsExpression)) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
121
198
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const typeAlias = /** @type {any} */ (
|
|
125
|
-
virtualSourceFile.statements[0]
|
|
199
|
+
const signature = /** @type {any} */ (
|
|
200
|
+
checker.getResolvedSignature(tsExpression)
|
|
126
201
|
);
|
|
127
|
-
|
|
202
|
+
return Boolean(
|
|
203
|
+
signature &&
|
|
204
|
+
(signature.typeParameters ?? signature.target?.typeParameters)?.length,
|
|
205
|
+
);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* A template literal with interpolations widens to `string` on its own, but a
|
|
210
|
+
* surrounding `@type` (a cast, or a declaration) contextually narrows it to a
|
|
211
|
+
* template-literal type, so `getTypeAtLocation` echoes the asserted literal
|
|
212
|
+
* back and a genuine narrowing (`\`${x}Reference\`` really being `string`,
|
|
213
|
+
* asserted as `AvailableType`) looks redundant.
|
|
214
|
+
* @param {any} estreeExpression
|
|
215
|
+
* @returns {boolean}
|
|
216
|
+
*/
|
|
217
|
+
const isInterpolatedTemplateLiteral = (estreeExpression) => {
|
|
218
|
+
return estreeExpression?.type === 'TemplateLiteral' &&
|
|
219
|
+
estreeExpression.expressions.length > 0;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Whether `type` carries no real structure to compare against — `any`,
|
|
224
|
+
* `never`, `null`, `undefined`, an empty array (`[]` -> `never[]`), an empty
|
|
225
|
+
* object (`{}`), or an array whose element type is itself such a placeholder
|
|
226
|
+
* (`any[]`). An assertion onto a placeholder is always supplying real type
|
|
227
|
+
* information, so it is not redundant.
|
|
228
|
+
* @param {any} type `ts.Type`
|
|
229
|
+
* @returns {boolean}
|
|
230
|
+
*/
|
|
231
|
+
const isPlaceholderType = (type) => {
|
|
232
|
+
if ((type.flags & (
|
|
233
|
+
ts.TypeFlags.Any | ts.TypeFlags.Never |
|
|
234
|
+
ts.TypeFlags.Null | ts.TypeFlags.Undefined
|
|
235
|
+
)) !== 0) {
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if ((type.flags & ts.TypeFlags.Object) === 0) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (checker.isArrayType(type)) {
|
|
244
|
+
const [
|
|
245
|
+
elementType,
|
|
246
|
+
] = checker.getTypeArguments(/** @type {import('typescript').TypeReference} */ (
|
|
247
|
+
type
|
|
248
|
+
));
|
|
249
|
+
return !elementType || (elementType.flags & (
|
|
250
|
+
ts.TypeFlags.Never | ts.TypeFlags.Undefined | ts.TypeFlags.Any
|
|
251
|
+
)) !== 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const {
|
|
255
|
+
objectFlags,
|
|
256
|
+
} = /** @type {import('typescript').ObjectType} */ (type);
|
|
257
|
+
return (objectFlags & ts.ObjectFlags.EmptyObjectLiteral) !== 0 ||
|
|
258
|
+
checker.getPropertiesOfType(type).length === 0;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Whether the JSDoc-asserted type adds nothing over the type TypeScript
|
|
263
|
+
* already infers for the expression it is attached to.
|
|
264
|
+
* @param {any} rawInferredType `ts.Type`
|
|
265
|
+
* @param {any} rawAssertedType `ts.Type`
|
|
266
|
+
* @returns {boolean}
|
|
267
|
+
*/
|
|
268
|
+
const isRedundantAssertion = (rawInferredType, rawAssertedType) => {
|
|
269
|
+
if (assertedTypeStr === 'const') {
|
|
270
|
+
return checkLiteralConstAssertions && isLiteralType(rawInferredType);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!treatAnyAsRedundant && assertedTypeStr === 'any') {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (typesToIgnore.includes(assertedTypeStr)) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// An `any` expression (e.g. a property of `JSON.parse(s)`) is assignable to
|
|
282
|
+
// everything, so a concrete assertion on it is genuinely narrowing rather
|
|
283
|
+
// than redundant. (`@type {any}` on `any` still falls through to the
|
|
284
|
+
// `treatAnyAsRedundant` handling above/below.)
|
|
285
|
+
if (
|
|
286
|
+
(rawInferredType.flags & ts.TypeFlags.Any) !== 0 &&
|
|
287
|
+
(rawAssertedType.flags & ts.TypeFlags.Any) === 0
|
|
288
|
+
) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// A union such as `never[] | {}` (from `cond ? [] : {}`) reaches the
|
|
293
|
+
// primitive path below; if every constituent is a structureless placeholder
|
|
294
|
+
// the assertion is supplying the real shape, so it is not redundant.
|
|
295
|
+
if (rawInferredType.isUnion() && rawInferredType.types.every(isPlaceholderType)) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const isObjectOrArray = (rawInferredType.flags & ts.TypeFlags.Object) !== 0;
|
|
300
|
+
|
|
301
|
+
if (!isObjectOrArray) {
|
|
302
|
+
// Primitives and individual literal values ("text" -> string) use unidirectional verification
|
|
303
|
+
return checker.isTypeAssignableTo(rawInferredType, rawAssertedType);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (isPlaceholderType(rawInferredType)) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (checker.isArrayType(rawInferredType)) {
|
|
311
|
+
// Arrays: standard structural bidirectional assignment matches string[] vs string[]
|
|
312
|
+
return checker.isTypeAssignableTo(rawInferredType, rawAssertedType) &&
|
|
313
|
+
checker.isTypeAssignableTo(rawAssertedType, rawInferredType);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (tightensAnyTypeArgument(rawInferredType, rawAssertedType)) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
128
319
|
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
|
|
320
|
+
// Objects: strip the literal-initialization flags, then require structural
|
|
321
|
+
// equivalence in both directions, so `{prop: string}` vs `{prop: string}` is
|
|
322
|
+
// redundant while `{prop?: string}` vs `{prop: string}` fails backward.
|
|
323
|
+
const inferredBaseType = checker.getBaseTypeOfLiteralType(rawInferredType);
|
|
324
|
+
const assertedBaseType = checker.getBaseTypeOfLiteralType(rawAssertedType);
|
|
325
|
+
|
|
326
|
+
return checker.isTypeAssignableTo(inferredBaseType, assertedBaseType) &&
|
|
327
|
+
checker.isTypeAssignableTo(assertedBaseType, inferredBaseType);
|
|
133
328
|
};
|
|
134
329
|
|
|
135
|
-
//
|
|
330
|
+
// Positions where a bare expression of any precedence is valid and equivalent
|
|
331
|
+
// to the parenthesized form, so a redundant `/** @type {T} */ (expr)` cast can
|
|
332
|
+
// be unwrapped to `expr` without changing meaning.
|
|
333
|
+
const unwrappableParentTypes = new Set([
|
|
334
|
+
'ArrayExpression',
|
|
335
|
+
'AssignmentExpression',
|
|
336
|
+
'ReturnStatement',
|
|
337
|
+
'ThrowStatement',
|
|
338
|
+
'VariableDeclarator',
|
|
339
|
+
]);
|
|
136
340
|
|
|
137
|
-
// 3.
|
|
138
|
-
// e.g., `/** @type {number} */ const x = 5;`
|
|
341
|
+
// 3. `/** @type {T} */ const x = 5;`
|
|
139
342
|
if (node?.type === 'VariableDeclaration') {
|
|
343
|
+
// A leading `@type` tag only influences the first declarator (TypeScript
|
|
344
|
+
// leaves the rest to their own inferred types), so checking `[0]` fully
|
|
345
|
+
// determines whether the tag is redundant even for `const a = 1, b = 2;`.
|
|
140
346
|
const decl = node.declarations[0];
|
|
141
347
|
|
|
142
348
|
/* c8 ignore next 4 -- How to cover? */
|
|
@@ -145,59 +351,146 @@ export default iterateJsdoc(({
|
|
|
145
351
|
return;
|
|
146
352
|
}
|
|
147
353
|
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
|
|
354
|
+
// Resolve the `@type` tag through the real TypeNode that TypeScript already
|
|
355
|
+
// parsed and bound as part of the program. Re-parsing the type string into
|
|
356
|
+
// a detached virtual source file (as an earlier approach did) leaves object
|
|
357
|
+
// type literals unbound, so `{prop: string}` collapses to `{}`.
|
|
358
|
+
const jsdocTypeNode = ts.getJSDocType(
|
|
359
|
+
services.esTreeNodeToTSNodeMap.get(decl),
|
|
360
|
+
);
|
|
151
361
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
362
|
+
/* c8 ignore next 3 -- Every `@type` tag reaching here has a bound TypeNode */
|
|
363
|
+
if (!jsdocTypeNode) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
156
366
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
367
|
+
const declInitTsNode = services.esTreeNodeToTSNodeMap.get(decl.init);
|
|
368
|
+
if (isGenericCall(declInitTsNode) || isInterpolatedTemplateLiteral(decl.init)) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
160
371
|
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
declaredType,
|
|
164
|
-
);
|
|
372
|
+
const declInferredType = checker.getTypeAtLocation(declInitTsNode);
|
|
373
|
+
const declAssertedType = checker.getTypeFromTypeNode(jsdocTypeNode);
|
|
165
374
|
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
375
|
+
if (isRedundantAssertion(declInferredType, declAssertedType)) {
|
|
376
|
+
utils.reportJSDoc(message, types[0], fixer, true, {
|
|
377
|
+
type: assertedTypeStr,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// 4. Inline cast: `/** @type {T} */ (expr)` (TypeScript's JSDoc assertion).
|
|
385
|
+
const exprTsNode = services.esTreeNodeToTSNodeMap.get(node);
|
|
386
|
+
const paren = exprTsNode?.parent;
|
|
387
|
+
if (!paren || !ts.isParenthesizedExpression(paren)) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
177
390
|
|
|
391
|
+
const typeTag = ts.getJSDocTypeTag(paren);
|
|
392
|
+
|
|
393
|
+
/* c8 ignore next 4 -- Defensive: `getJSDocTypeTag` can also surface a `@type`
|
|
394
|
+
inherited from an enclosing statement, whose position precedes the paren */
|
|
395
|
+
if (!typeTag || typeTag.pos < paren.pos) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// The `@type` TypeScript resolves for `paren` must be the very comment being
|
|
400
|
+
// iterated. When the comment actually sits on an inner parenthesized
|
|
401
|
+
// sub-expression that is then a member/argument of the paren's operand
|
|
402
|
+
// (`/** @type {DOMException} */ (reader.error).message`), `paren` is a
|
|
403
|
+
// different, outer cast carrying a different comment, so measuring or fixing
|
|
404
|
+
// against it would target the wrong expression (and drop the inner cast).
|
|
405
|
+
if (typeTag.parent.end !== jsdocNode.range[1]) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (isGenericCall(exprTsNode) || isInterpolatedTemplateLiteral(node)) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// The `unknown` half of a "cast through `unknown`"
|
|
414
|
+
// (`/** @type {T} */ (/** @type {unknown} */ (x))`) is the load-bearing
|
|
415
|
+
// bridge that lets the outer assertion reach an otherwise-incompatible type;
|
|
416
|
+
// it is never redundant, even though `unknown` is broader than everything.
|
|
417
|
+
if (assertedTypeStr === 'unknown' && ts.isParenthesizedExpression(paren.parent)) {
|
|
418
|
+
const outerTypeTag = ts.getJSDocTypeTag(paren.parent);
|
|
419
|
+
if (outerTypeTag && outerTypeTag.pos >= paren.parent.pos) {
|
|
178
420
|
return;
|
|
179
421
|
}
|
|
422
|
+
}
|
|
180
423
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
type: declaredTypeStr,
|
|
191
|
-
},
|
|
192
|
-
);
|
|
193
|
-
}
|
|
424
|
+
const parent = /** @type {any} */ (node.parent);
|
|
425
|
+
const declaration = parent.type === 'VariableDeclarator' ? parent.parent : null;
|
|
426
|
+
|
|
427
|
+
// A `/** @type {const} */` cast only fails to do real work on a `const`
|
|
428
|
+
// declarator, which already infers the literal type. Anywhere else (`let`, a
|
|
429
|
+
// return, an object-property value, some generic call arguments, …) it
|
|
430
|
+
// suppresses widening, so it is not redundant.
|
|
431
|
+
if (assertedTypeStr === 'const' && declaration?.kind !== 'const') {
|
|
432
|
+
return;
|
|
194
433
|
}
|
|
434
|
+
|
|
435
|
+
// For a mutable binding (`let`/`var`) TypeScript widens the initializer, so a
|
|
436
|
+
// narrowing cast such as `/** @type {5} */` is doing real work; widen the
|
|
437
|
+
// uncast type to match before judging redundancy.
|
|
438
|
+
const mutableBinding = Boolean(declaration) && declaration.kind !== 'const';
|
|
439
|
+
const castInferredType = mutableBinding ?
|
|
440
|
+
checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(exprTsNode)) :
|
|
441
|
+
checker.getTypeAtLocation(exprTsNode);
|
|
442
|
+
const castAssertedType = checker.getTypeFromTypeNode(typeTag.typeExpression.type);
|
|
443
|
+
|
|
444
|
+
if (!isRedundantAssertion(castInferredType, castAssertedType)) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const canUnwrap = enableFixer && (
|
|
449
|
+
unwrappableParentTypes.has(parent.type) ||
|
|
450
|
+
(parent.type === 'ConditionalExpression' && parent.test !== node) ||
|
|
451
|
+
((parent.type === 'CallExpression' || parent.type === 'NewExpression') &&
|
|
452
|
+
parent.callee !== node)
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
report(
|
|
456
|
+
message,
|
|
457
|
+
canUnwrap ?
|
|
458
|
+
/**
|
|
459
|
+
* @param {import('eslint').Rule.RuleFixer} ruleFixer
|
|
460
|
+
* @returns {import('eslint').Rule.Fix}
|
|
461
|
+
*/
|
|
462
|
+
(ruleFixer) => {
|
|
463
|
+
const closeParen = /** @type {import('eslint').AST.Token} */ (
|
|
464
|
+
sourceCode.getTokenAfter(/** @type {any} */ (node), {
|
|
465
|
+
filter: ({
|
|
466
|
+
type,
|
|
467
|
+
value,
|
|
468
|
+
}) => {
|
|
469
|
+
return type === 'Punctuator' && value === ')';
|
|
470
|
+
},
|
|
471
|
+
})
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
return ruleFixer.replaceTextRange(
|
|
475
|
+
[
|
|
476
|
+
jsdocNode.range[0], closeParen.range[1],
|
|
477
|
+
],
|
|
478
|
+
sourceCode.getText(/** @type {any} */ (node)),
|
|
479
|
+
);
|
|
480
|
+
} :
|
|
481
|
+
null,
|
|
482
|
+
types[0],
|
|
483
|
+
{
|
|
484
|
+
type: assertedTypeStr,
|
|
485
|
+
},
|
|
486
|
+
);
|
|
195
487
|
}, {
|
|
196
488
|
iterateAllJsdocs: true,
|
|
197
489
|
meta: {
|
|
198
490
|
docs: {
|
|
199
491
|
description: 'Reports redundant @type tags that match or broaden the naturally inferred TypeScript type.',
|
|
200
492
|
},
|
|
493
|
+
fixable: 'code',
|
|
201
494
|
schema: [
|
|
202
495
|
{
|
|
203
496
|
additionalProperties: false,
|
|
@@ -206,6 +499,10 @@ export default iterateJsdoc(({
|
|
|
206
499
|
description: 'Whether to check `const` type assertions as redundant',
|
|
207
500
|
type: 'boolean',
|
|
208
501
|
},
|
|
502
|
+
enableFixer: {
|
|
503
|
+
description: 'Whether to enable the fixer that removes the redundant `@type` tag (and the JSDoc block if it becomes empty). Defaults to `true`.',
|
|
504
|
+
type: 'boolean',
|
|
505
|
+
},
|
|
209
506
|
treatAnyAsRedundant: {
|
|
210
507
|
description: 'Whether to treat `any` type casts as redundant',
|
|
211
508
|
type: 'boolean',
|
package/src/rules.d.ts
CHANGED
|
@@ -1320,6 +1320,10 @@ export interface Rules {
|
|
|
1320
1320
|
* Whether to check `const` type assertions as redundant
|
|
1321
1321
|
*/
|
|
1322
1322
|
checkLiteralConstAssertions?: boolean;
|
|
1323
|
+
/**
|
|
1324
|
+
* Whether to enable the fixer that removes the redundant `@type` tag (and the JSDoc block if it becomes empty). Defaults to `true`.
|
|
1325
|
+
*/
|
|
1326
|
+
enableFixer?: boolean;
|
|
1323
1327
|
/**
|
|
1324
1328
|
* Whether to treat `any` type casts as redundant
|
|
1325
1329
|
*/
|