@depup/eslint-plugin-jsdoc 64.2.1-depup.1 → 64.3.2-depup.0

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 CHANGED
@@ -13,8 +13,8 @@ npm install @depup/eslint-plugin-jsdoc
13
13
 
14
14
  | Field | Value |
15
15
  |-------|-------|
16
- | Original | [eslint-plugin-jsdoc](https://www.npmjs.com/package/eslint-plugin-jsdoc) @ 64.2.1 |
17
- | Processed | 2026-08-24 |
16
+ | Original | [eslint-plugin-jsdoc](https://www.npmjs.com/package/eslint-plugin-jsdoc) @ 64.3.2 |
17
+ | Processed | 2026-08-31 |
18
18
  | Smoke test | passed |
19
19
  | Deps updated | 1 |
20
20
 
package/changes.json CHANGED
@@ -5,6 +5,6 @@
5
5
  "to": "^0.96.0"
6
6
  }
7
7
  },
8
- "timestamp": "2026-08-24T08:17:04.824Z",
8
+ "timestamp": "2026-08-31T00:36:03.183Z",
9
9
  "totalUpdated": 1
10
10
  }
@@ -1,5 +1,4 @@
1
1
  export function getJsdocProcessorPlugin(options?: JsdocProcessorOptions): ESLint.Plugin;
2
- export type Integer = number;
3
2
  export type JsdocProcessorOptions = {
4
3
  /**
5
4
  * Require captions for example tags
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
- * @type {import('eslint').ESLint.Plugin & {
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: import("eslint").ESLint.Plugin & {
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
@@ -0,0 +1,3 @@
1
+ declare const _default: import("eslint").Rule.RuleModule;
2
+ export default _default;
3
+ //# sourceMappingURL=noUnnecessaryTypeAssertion.d.ts.map
package/dist/rules.d.ts CHANGED
@@ -144,8 +144,8 @@ export interface Rules {
144
144
  */
145
145
  allowExtraTrailingParamDocs?: boolean;
146
146
  /**
147
- * Whether to offer a suggestion to rename a mismatched `@param` to the
148
- * corresponding function parameter name. Defaults to `false`.
147
+ * Whether to auto-fix a mismatched `@param` name to the corresponding
148
+ * function parameter name. A suggestion is always offered. Defaults to `false`.
149
149
  */
150
150
  badParamNames?: boolean;
151
151
  /**
@@ -229,8 +229,8 @@ export interface Rules {
229
229
  */
230
230
  enableFixer?: boolean;
231
231
  /**
232
- * Whether to offer a suggestion to remove an `@param` that has no
233
- * corresponding function parameter. Defaults to `false`.
232
+ * Whether to auto-remove an `@param` that has no corresponding function
233
+ * parameter. A suggestion is always offered. Defaults to `false`.
234
234
  */
235
235
  extraParams?: boolean;
236
236
  /**
@@ -1311,6 +1311,30 @@ export interface Rules {
1311
1311
  }
1312
1312
  ];
1313
1313
 
1314
+ /** Reports redundant @type tags that match or broaden the naturally inferred TypeScript type. */
1315
+ "jsdoc/no-unnecessary-type-assertion":
1316
+ | []
1317
+ | [
1318
+ {
1319
+ /**
1320
+ * Whether to check `const` type assertions as redundant
1321
+ */
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;
1327
+ /**
1328
+ * Whether to treat `any` type casts as redundant
1329
+ */
1330
+ treatAnyAsRedundant?: boolean;
1331
+ /**
1332
+ * An array list of types to ignore
1333
+ */
1334
+ typesToIgnore?: string[];
1335
+ }
1336
+ ];
1337
+
1314
1338
  /** Normalizes labeled links in `@see` tags to a canonical `{@link}` form. */
1315
1339
  "jsdoc/normalize-see-links":
1316
1340
  | []
package/package.json CHANGED
@@ -7,6 +7,7 @@
7
7
  "dependencies": {
8
8
  "@es-joy/jsdoccomment": "^0.96.0",
9
9
  "@es-joy/resolve.exports": "1.2.0",
10
+ "@typescript-eslint/utils": "^8.68.0",
10
11
  "are-docs-informative": "^0.1.1",
11
12
  "comment-parser": "1.4.8",
12
13
  "debug": "^4.4.3",
@@ -68,7 +69,7 @@
68
69
  "semantic-release": "^25.0.9",
69
70
  "sinon": "^22.1.0",
70
71
  "ts-api-utils": "^2.5.0",
71
- "typescript": "5.9.3",
72
+ "typescript": "6.0.3",
72
73
  "typescript-eslint": "8.67.0"
73
74
  },
74
75
  "engines": {
@@ -144,6 +145,7 @@
144
145
  "scripts": {
145
146
  "ruleTypes": "node ./src/bin/generateRuleTypes.js",
146
147
  "tsc": "tsc",
148
+ "tsc:ts7": "npx -y -p typescript@^7 -c \"tsc\"",
147
149
  "tsc-build": "tsc -p tsconfig-prod.json",
148
150
  "build": "rimraf ./dist && mkdir dist && pnpm tsc-build && cp src/rules.d.ts dist/rules.d.ts",
149
151
  "attw": "attw --profile esm-only --pack .",
@@ -160,7 +162,7 @@
160
162
  "test-cov": "TIMING=1 c8 --reporter text pnpm run test-no-cov",
161
163
  "test-index": "pnpm run test-no-cov test/rules/index.js"
162
164
  },
163
- "version": "64.2.1-depup.1",
165
+ "version": "64.3.2-depup.0",
164
166
  "depup": {
165
167
  "changes": {
166
168
  "@es-joy/jsdoccomment": {
@@ -170,8 +172,8 @@
170
172
  },
171
173
  "depsUpdated": 1,
172
174
  "originalPackage": "eslint-plugin-jsdoc",
173
- "originalVersion": "64.2.1",
174
- "processedAt": "2026-08-24T08:17:27.573Z",
175
+ "originalVersion": "64.3.2",
176
+ "processedAt": "2026-08-31T00:36:26.963Z",
175
177
  "smokeTest": "passed"
176
178
  }
177
179
  }
@@ -80,9 +80,6 @@ const getLinesCols = (text) => {
80
80
  ];
81
81
  };
82
82
 
83
- /**
84
- * @typedef {number} Integer
85
- */
86
83
  /**
87
84
  * @typedef {object} JsdocProcessorOptions
88
85
  * @property {boolean} [captionRequired] Require captions for example tags
package/src/index.js CHANGED
@@ -39,6 +39,7 @@ import noRestrictedSyntax from './rules/noRestrictedSyntax.js';
39
39
  import normalizeSeeLinks from './rules/normalizeSeeLinks.js';
40
40
  import noTypes from './rules/noTypes.js';
41
41
  import noUndefinedTypes from './rules/noUndefinedTypes.js';
42
+ import noUnnecessaryTypeAssertion from './rules/noUnnecessaryTypeAssertion.js';
42
43
  import preferImportTag from './rules/preferImportTag.js';
43
44
  import requireAsteriskPrefix from './rules/requireAsteriskPrefix.js';
44
45
  import requireDescription from './rules/requireDescription.js';
@@ -88,7 +89,7 @@ import semver from 'semver';
88
89
  * @typedef {"" | "-error"} ErrorLevelVariants
89
90
  * @typedef {`flat/${ConfigGroups}${ConfigVariants}${ErrorLevelVariants}` |
90
91
  * "flat/recommended-tsdoc" | "flat/recommended-tsdoc-error"} FlatConfigNames
91
- * @type {import('eslint').ESLint.Plugin & {
92
+ * @typedef {import('eslint').ESLint.Plugin & {
92
93
  * configs: Record<
93
94
  * FlatConfigNames,
94
95
  * import('eslint').Linter.Config
@@ -98,10 +99,10 @@ import semver from 'semver';
98
99
  * import('eslint').Linter.Config[]
99
100
  * > &
100
101
  * Record<"flat/recommended-mixed", import('eslint').Linter.Config[]>
101
- * }}
102
+ * }} JsdocPlugin
102
103
  */
103
- const index = {};
104
- index.configs = {};
104
+ const index = /** @type {JsdocPlugin} */ ({});
105
+ index.configs = /** @type {typeof index.configs} */ ({});
105
106
  index.rules = {
106
107
  'check-access': checkAccess,
107
108
  'check-alignment': checkAlignment,
@@ -134,6 +135,7 @@ index.rules = {
134
135
  'no-restricted-syntax': noRestrictedSyntax,
135
136
  'no-types': noTypes,
136
137
  'no-undefined-types': noUndefinedTypes,
138
+ 'no-unnecessary-type-assertion': noUnnecessaryTypeAssertion,
137
139
  'normalize-see-links': normalizeSeeLinks,
138
140
  'prefer-import-tag': preferImportTag,
139
141
  'reject-any-type': buildRejectOrPreferRuleDefinition({
@@ -325,6 +327,7 @@ const createRecommendedRuleset = (warnOrError, flatName) => {
325
327
  'jsdoc/no-restricted-syntax': 'off',
326
328
  'jsdoc/no-types': 'off',
327
329
  'jsdoc/no-undefined-types': warnOrError,
330
+ 'jsdoc/no-unnecessary-type-assertion': 'off',
328
331
  'jsdoc/normalize-see-links': 'off',
329
332
  'jsdoc/prefer-import-tag': 'off',
330
333
  'jsdoc/reject-any-type': warnOrError,
@@ -192,10 +192,10 @@ import esquery from 'esquery';
192
192
  */
193
193
 
194
194
  /**
195
- * @callback ChangeTag
196
- * @param {import('comment-parser').Spec} tag
197
- * @param {...Partial<import('comment-parser').Tokens>} tokens
198
- * @returns {void}
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
- /** @type {import('estree').AssignmentPattern} */ (
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),
@@ -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').GenericResult}
47
+ * @type {import('jsdoc-type-pratt-parser').NameResult}
48
48
  */
49
- (parentNode)?.left?.value === 'Object'
49
+ (
50
+ /**
51
+ * @type {import('jsdoc-type-pratt-parser').GenericResult}
52
+ */
53
+ (parentNode)?.left
54
+ )?.value === 'Object'
50
55
  )
51
56
  )
52
57
  ) {
@@ -0,0 +1,523 @@
1
+ /* eslint-disable no-bitwise -- Convenient */
2
+ /* eslint-disable jsdoc/reject-any-type -- TypeScript types need not be installed */
3
+ import iterateJsdoc from '../iterateJsdoc.js';
4
+ import {
5
+ createRequire,
6
+ } from 'module';
7
+
8
+ let warned = false;
9
+
10
+ /** @type {any} */
11
+ let ts;
12
+
13
+ // 1. Create a require function bound to the current file's URL
14
+ const require = createRequire(import.meta.url);
15
+
16
+ try {
17
+ // 2. Attempt to import the package synchronously
18
+ ts = require('typescript');
19
+ /* c8 ignore next 10 -- Guard */
20
+ } catch (error) {
21
+ // 3. Fall back gracefully if it is not installed
22
+ if (/** @type {{code?: string}} */ (error).code !== 'MODULE_NOT_FOUND') {
23
+ // Re-throw if it's a different error (e.g., syntax error inside the package)
24
+ throw error;
25
+ }
26
+
27
+ // eslint-disable-next-line no-console -- Warning user
28
+ console.warn('⚠️ typescript is not installed. `jsdoc/no-unnecessary-type-assertion` will not work. To disable this warning, you must disable the rule.');
29
+ }
30
+
31
+ // Helper to check for standard literals and boolean/enum/template literals
32
+ /**
33
+ * @param {any} type The type is `ts.Type`
34
+ * @returns {boolean}
35
+ */
36
+ const isLiteralType = (type) => {
37
+ // Standard primitive literals (string, number, bigint, boolean literal flags)
38
+ if (type.isLiteral()) {
39
+ return true;
40
+ }
41
+
42
+ // Check using TypeFlags for specific edge cases like boolean or template literals
43
+
44
+ if (type.flags & (ts.TypeFlags.BooleanLiteral | ts.TypeFlags.TemplateLiteral)) {
45
+ return true;
46
+ }
47
+
48
+ // If dealing with a union (e.g. 'a' | 'b'), you can check if all constituents are literals
49
+ if (type.isUnion()) {
50
+ return type.types.every(
51
+ /**
52
+ * @param {any} typ
53
+ */
54
+ (typ) => {
55
+ return isLiteralType(typ);
56
+ },
57
+ );
58
+ }
59
+
60
+ return false;
61
+ };
62
+
63
+ export default iterateJsdoc(({
64
+ context,
65
+ jsdoc,
66
+ jsdocNode,
67
+ node: nde,
68
+ report,
69
+ sourceCode,
70
+ utils,
71
+ // eslint-disable-next-line complexity -- Numerous type/option permutations
72
+ }) => {
73
+ /* c8 ignore next 4 -- Guard */
74
+ // Already handled
75
+ if (!ts) {
76
+ return;
77
+ }
78
+
79
+ const types = utils.getTags('type');
80
+
81
+ // 1. Only proceed if there is a @type tag
82
+ if (!types.length) {
83
+ return;
84
+ }
85
+
86
+ const {
87
+ // https://typescript-eslint.io/rules/no-unnecessary-type-assertion/
88
+ checkLiteralConstAssertions = false,
89
+ enableFixer = true,
90
+ treatAnyAsRedundant = false,
91
+ typesToIgnore = [],
92
+ } = context.options[0] ?? {};
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
+
124
+ const node =
125
+ /**
126
+ * @type {import('@typescript-eslint/utils').TSESTree.Node}
127
+ */ (nde);
128
+
129
+ // 2. Safely get TypeScript parser services and TypeChecker
130
+ const services =
131
+ /**
132
+ * @type {import('@typescript-eslint/utils').ParserServices}
133
+ */ (context.sourceCode.parserServices);
134
+
135
+ /* c8 ignore next 10 -- Guard */
136
+ if (!services || !services.program) {
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
+
144
+ return;
145
+ }
146
+
147
+ // 1. Grab the parser services and the active TypeChecker
148
+ // const services = ESLintUtils.getParserServices(context);
149
+ const checker = services.program.getTypeChecker();
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
+
157
+ /**
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}
167
+ */
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
+ }
198
+
199
+ const signature = /** @type {any} */ (
200
+ checker.getResolvedSignature(tsExpression)
201
+ );
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
+ }
319
+
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);
328
+ };
329
+
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
+ ]);
340
+
341
+ // 3. `/** @type {T} */ const x = 5;`
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;`.
346
+ const decl = node.declarations[0];
347
+
348
+ /* c8 ignore next 4 -- How to cover? */
349
+ if (!decl.init) {
350
+ // No initializer, type is likely `any`, so @type isn't redundant
351
+ return;
352
+ }
353
+
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
+ );
361
+
362
+ /* c8 ignore next 3 -- Every `@type` tag reaching here has a bound TypeNode */
363
+ if (!jsdocTypeNode) {
364
+ return;
365
+ }
366
+
367
+ const declInitTsNode = services.esTreeNodeToTSNodeMap.get(decl.init);
368
+ if (isGenericCall(declInitTsNode) || isInterpolatedTemplateLiteral(decl.init)) {
369
+ return;
370
+ }
371
+
372
+ const declInferredType = checker.getTypeAtLocation(declInitTsNode);
373
+ const declAssertedType = checker.getTypeFromTypeNode(jsdocTypeNode);
374
+
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
+ }
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) {
420
+ return;
421
+ }
422
+ }
423
+
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;
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
+ );
487
+ }, {
488
+ iterateAllJsdocs: true,
489
+ meta: {
490
+ docs: {
491
+ description: 'Reports redundant @type tags that match or broaden the naturally inferred TypeScript type.',
492
+ },
493
+ fixable: 'code',
494
+ schema: [
495
+ {
496
+ additionalProperties: false,
497
+ properties: {
498
+ checkLiteralConstAssertions: {
499
+ description: 'Whether to check `const` type assertions as redundant',
500
+ type: 'boolean',
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
+ },
506
+ treatAnyAsRedundant: {
507
+ description: 'Whether to treat `any` type casts as redundant',
508
+ type: 'boolean',
509
+ },
510
+ typesToIgnore: {
511
+ description: 'An array list of types to ignore',
512
+ items: {
513
+ type: 'string',
514
+ },
515
+ type: 'array',
516
+ },
517
+ },
518
+ type: 'object',
519
+ },
520
+ ],
521
+ type: 'suggestion',
522
+ },
523
+ });
@@ -152,6 +152,7 @@ export default iterateJsdoc(({
152
152
  return;
153
153
  }
154
154
 
155
+ /** @type {import('jsdoc-type-pratt-parser').NonRootResult} */
155
156
  let currentNode = nde;
156
157
 
157
158
  /** @type {string[]} */
package/src/rules.d.ts CHANGED
@@ -144,8 +144,8 @@ export interface Rules {
144
144
  */
145
145
  allowExtraTrailingParamDocs?: boolean;
146
146
  /**
147
- * Whether to offer a suggestion to rename a mismatched `@param` to the
148
- * corresponding function parameter name. Defaults to `false`.
147
+ * Whether to auto-fix a mismatched `@param` name to the corresponding
148
+ * function parameter name. A suggestion is always offered. Defaults to `false`.
149
149
  */
150
150
  badParamNames?: boolean;
151
151
  /**
@@ -229,8 +229,8 @@ export interface Rules {
229
229
  */
230
230
  enableFixer?: boolean;
231
231
  /**
232
- * Whether to offer a suggestion to remove an `@param` that has no
233
- * corresponding function parameter. Defaults to `false`.
232
+ * Whether to auto-remove an `@param` that has no corresponding function
233
+ * parameter. A suggestion is always offered. Defaults to `false`.
234
234
  */
235
235
  extraParams?: boolean;
236
236
  /**
@@ -1311,6 +1311,30 @@ export interface Rules {
1311
1311
  }
1312
1312
  ];
1313
1313
 
1314
+ /** Reports redundant @type tags that match or broaden the naturally inferred TypeScript type. */
1315
+ "jsdoc/no-unnecessary-type-assertion":
1316
+ | []
1317
+ | [
1318
+ {
1319
+ /**
1320
+ * Whether to check `const` type assertions as redundant
1321
+ */
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;
1327
+ /**
1328
+ * Whether to treat `any` type casts as redundant
1329
+ */
1330
+ treatAnyAsRedundant?: boolean;
1331
+ /**
1332
+ * An array list of types to ignore
1333
+ */
1334
+ typesToIgnore?: string[];
1335
+ }
1336
+ ];
1337
+
1314
1338
  /** Normalizes labeled links in `@see` tags to a canonical `{@link}` form. */
1315
1339
  "jsdoc/normalize-see-links":
1316
1340
  | []