@depup/eslint-plugin-jsdoc 64.3.1-depup.0 → 64.3.3-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,16 +13,17 @@ 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.3.1 |
17
- | Processed | 2026-08-30 |
16
+ | Original | [eslint-plugin-jsdoc](https://www.npmjs.com/package/eslint-plugin-jsdoc) @ 64.3.3 |
17
+ | Processed | 2026-09-01 |
18
18
  | Smoke test | passed |
19
- | Deps updated | 1 |
19
+ | Deps updated | 2 |
20
20
 
21
21
  ## Dependency Changes
22
22
 
23
23
  | Dependency | From | To |
24
24
  |------------|------|-----|
25
25
  | @es-joy/jsdoccomment | ~0.95.1 | ^0.96.0 |
26
+ | @typescript-eslint/utils | ^8.68.0 | ^8.69.0 |
26
27
 
27
28
  ---
28
29
 
package/changes.json CHANGED
@@ -3,8 +3,12 @@
3
3
  "@es-joy/jsdoccomment": {
4
4
  "from": "~0.95.1",
5
5
  "to": "^0.96.0"
6
+ },
7
+ "@typescript-eslint/utils": {
8
+ "from": "^8.68.0",
9
+ "to": "^8.69.0"
6
10
  }
7
11
  },
8
- "timestamp": "2026-08-30T00:56:30.602Z",
9
- "totalUpdated": 1
12
+ "timestamp": "2026-09-01T00:40:40.623Z",
13
+ "totalUpdated": 2
10
14
  }
@@ -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
@@ -170,9 +170,10 @@ export function getAllTags(jsdoc: import("./iterateJsdoc.js").JsdocBlockWithInli
170
170
  * @param {import('./iterateJsdoc.js').Context[]} contexts
171
171
  * @param {import('./iterateJsdoc.js').CheckJsdoc} checkJsdoc
172
172
  * @param {import('@es-joy/jsdoccomment').CommentHandler} [handler]
173
+ * @param {boolean} [convertAny]
173
174
  * @returns {import('eslint').Rule.RuleListener}
174
175
  */
175
- export function getContextObject(contexts: import("./iterateJsdoc.js").Context[], checkJsdoc: import("./iterateJsdoc.js").CheckJsdoc, handler?: import("@es-joy/jsdoccomment").CommentHandler): import("eslint").Rule.RuleListener;
176
+ export function getContextObject(contexts: import("./iterateJsdoc.js").Context[], checkJsdoc: import("./iterateJsdoc.js").CheckJsdoc, handler?: import("@es-joy/jsdoccomment").CommentHandler, convertAny?: boolean): import("eslint").Rule.RuleListener;
176
177
  /**
177
178
  * @param {import('eslint').SourceCode} sourceCode
178
179
  * @returns {import('comment-parser').Spec[]}
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
@@ -7,7 +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
+ "@typescript-eslint/utils": "^8.69.0",
11
11
  "are-docs-informative": "^0.1.1",
12
12
  "comment-parser": "1.4.8",
13
13
  "debug": "^4.4.3",
@@ -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": "5.9.3",
72
+ "typescript": "6.0.3",
73
73
  "typescript-eslint": "8.67.0"
74
74
  },
75
75
  "engines": {
@@ -145,6 +145,7 @@
145
145
  "scripts": {
146
146
  "ruleTypes": "node ./src/bin/generateRuleTypes.js",
147
147
  "tsc": "tsc",
148
+ "tsc:ts7": "npx -y -p typescript@^7 -c \"tsc\"",
148
149
  "tsc-build": "tsc -p tsconfig-prod.json",
149
150
  "build": "rimraf ./dist && mkdir dist && pnpm tsc-build && cp src/rules.d.ts dist/rules.d.ts",
150
151
  "attw": "attw --profile esm-only --pack .",
@@ -161,18 +162,22 @@
161
162
  "test-cov": "TIMING=1 c8 --reporter text pnpm run test-no-cov",
162
163
  "test-index": "pnpm run test-no-cov test/rules/index.js"
163
164
  },
164
- "version": "64.3.1-depup.0",
165
+ "version": "64.3.3-depup.0",
165
166
  "depup": {
166
167
  "changes": {
167
168
  "@es-joy/jsdoccomment": {
168
169
  "from": "~0.95.1",
169
170
  "to": "^0.96.0"
171
+ },
172
+ "@typescript-eslint/utils": {
173
+ "from": "^8.68.0",
174
+ "to": "^8.69.0"
170
175
  }
171
176
  },
172
- "depsUpdated": 1,
177
+ "depsUpdated": 2,
173
178
  "originalPackage": "eslint-plugin-jsdoc",
174
- "originalVersion": "64.3.1",
175
- "processedAt": "2026-08-30T00:56:47.119Z",
179
+ "originalVersion": "64.3.3",
180
+ "processedAt": "2026-09-01T00:41:05.665Z",
176
181
  "smokeTest": "passed"
177
182
  }
178
183
  }
@@ -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
@@ -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
- * @type {import('eslint').ESLint.Plugin & {
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,
@@ -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),
@@ -1657,9 +1655,10 @@ const enforcedContexts = (context, defaultContexts, settings) => {
1657
1655
  * @param {import('./iterateJsdoc.js').Context[]} contexts
1658
1656
  * @param {import('./iterateJsdoc.js').CheckJsdoc} checkJsdoc
1659
1657
  * @param {import('@es-joy/jsdoccomment').CommentHandler} [handler]
1658
+ * @param {boolean} [convertAny]
1660
1659
  * @returns {import('eslint').Rule.RuleListener}
1661
1660
  */
1662
- const getContextObject = (contexts, checkJsdoc, handler) => {
1661
+ const getContextObject = (contexts, checkJsdoc, handler, convertAny) => {
1663
1662
  /** @type {import('eslint').Rule.RuleListener} */
1664
1663
  const properties = {};
1665
1664
 
@@ -1698,11 +1697,16 @@ const getContextObject = (contexts, checkJsdoc, handler) => {
1698
1697
  value = checkJsdoc.bind(null, selInfo, null);
1699
1698
  }
1700
1699
  } else {
1700
+ property = prop;
1701
+
1702
+ if (convertAny && property === 'any') {
1703
+ property = ':not(Program)';
1704
+ }
1705
+
1701
1706
  const selInfo = {
1702
1707
  lastIndex: idx,
1703
- selector: prop,
1708
+ selector: property,
1704
1709
  };
1705
- property = prop;
1706
1710
  value = checkJsdoc.bind(null, selInfo, null);
1707
1711
  }
1708
1712
 
@@ -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
  ) {
@@ -185,6 +185,31 @@ export default {
185
185
  }
186
186
  };
187
187
 
188
+ /**
189
+ * Builds the opening portion of the JSDoc comment, i.e. everything before
190
+ * the closing delimiter.
191
+ * @param {string} indent
192
+ * @param {Token} comment
193
+ * @param {boolean|undefined} inlineCommentBlock
194
+ * @returns {string}
195
+ */
196
+ const getCommentOpening = (indent, comment, inlineCommentBlock) => {
197
+ if (inlineCommentBlock || enforceJsdocLineStyle === 'single') {
198
+ return `/** ${comment.value.trim()} `;
199
+ }
200
+
201
+ const body = comment.value.trimEnd();
202
+
203
+ // When the comment's text already begins on its own line (e.g. a
204
+ // multi-line block comment), there is no need for the fixer to add a
205
+ // leading blank `*` line.
206
+ if ((/^[ \t]*\n/v).test(body)) {
207
+ return `/**${body.replace(/^[ \t]+/v, '')}\n${indent}`;
208
+ }
209
+
210
+ return `/**\n${indent}*${body}\n${indent}`;
211
+ };
212
+
188
213
  /**
189
214
  * @type {import('../iterateJsdoc.js').CheckJsdoc}
190
215
  */
@@ -205,11 +230,7 @@ export default {
205
230
 
206
231
  /** @type {AddComment} */
207
232
  const addComment = (inlineCommentBlock, commentToAdd, indent, lines, fixer) => {
208
- const insertion = (
209
- inlineCommentBlock || enforceJsdocLineStyle === 'single' ?
210
- `/** ${commentToAdd.value.trim()} ` :
211
- `/**\n${indent}*${commentToAdd.value.trimEnd()}\n${indent}`
212
- ) +
233
+ const insertion = getCommentOpening(indent, commentToAdd, inlineCommentBlock) +
213
234
  `*/${'\n'.repeat((lines || 1) - 1)}`;
214
235
 
215
236
  return fixer.replaceText(
@@ -242,11 +263,7 @@ export default {
242
263
 
243
264
  /** @type {AddComment} */
244
265
  const addComment = (inlineCommentBlock, commentToAdd, indent, lines, fixer) => {
245
- const insertion = (
246
- inlineCommentBlock || enforceJsdocLineStyle === 'single' ?
247
- `/** ${commentToAdd.value.trim()} ` :
248
- `/**\n${indent}*${commentToAdd.value.trimEnd()}\n${indent}`
249
- ) +
266
+ const insertion = getCommentOpening(indent, commentToAdd, inlineCommentBlock) +
250
267
  `*/${'\n'.repeat((lines || 1) - 1)}${lines ? `\n${indent.slice(1)}` : ' '}`;
251
268
 
252
269
  return [
@@ -268,12 +285,16 @@ export default {
268
285
  ...getContextObject(
269
286
  enforcedContexts(context, true, settings),
270
287
  checkNonJsdoc,
288
+ undefined,
289
+ true,
271
290
  ),
272
291
  ...getContextObject(
273
292
  contextsAfter,
274
293
  (_info, _handler, node) => {
275
294
  checkNonJsdocAfter(node, contextsAfter);
276
295
  },
296
+ undefined,
297
+ true,
277
298
  ),
278
299
  ...getContextObject(
279
300
  contextsBeforeAndAfter,
@@ -283,6 +304,8 @@ export default {
283
304
  checkNonJsdocAfter(node, contextsBeforeAndAfter);
284
305
  }
285
306
  },
307
+ undefined,
308
+ true,
286
309
  ),
287
310
  };
288
311
  },
@@ -1,60 +1,115 @@
1
1
  import iterateJsdoc from '../iterateJsdoc.js';
2
2
 
3
- const anyWhitespaceLines = /^\s*$/v;
4
- const atLeastTwoLinesWhitespace = /^[ \t]*\n[ \t]*\n\s*$/v;
3
+ const anyWhitespaceLine = /^\s*$/v;
5
4
 
6
5
  export default iterateJsdoc(({
7
6
  jsdoc,
8
7
  utils,
9
8
  }) => {
10
- const {
11
- description,
12
- descriptions,
13
- lastDescriptionLine,
14
- } = utils.getDescription();
15
-
16
- const regex = jsdoc.tags.length ?
17
- anyWhitespaceLines :
18
- atLeastTwoLinesWhitespace;
19
-
20
- if (descriptions.length && regex.test(description)) {
21
- if (jsdoc.tags.length) {
22
- utils.reportJSDoc(
23
- 'There should be no blank lines in block descriptions followed by tags.',
24
- {
25
- line: lastDescriptionLine,
26
- },
27
- () => {
28
- utils.setBlockDescription(() => {
29
- // Remove all lines
30
- return [];
31
- });
32
- },
33
- );
34
- } else {
35
- utils.reportJSDoc(
36
- 'There should be no extra blank lines in block descriptions not followed by tags.',
37
- {
38
- line: lastDescriptionLine,
39
- },
40
- () => {
41
- utils.setBlockDescription((info, seedTokens) => {
42
- return [
43
- // Keep the starting line
44
- {
45
- number: 0,
46
- source: '',
47
- tokens: seedTokens({
48
- ...info,
49
- description: '',
50
- }),
51
- },
52
- ];
53
- });
54
- },
55
- );
9
+ const hasTags = Boolean(jsdoc.tags.length);
10
+
11
+ // Gather the block-description lines (those before the first tag or the
12
+ // closing delimiter).
13
+ let startIdx = -1;
14
+
15
+ /**
16
+ * @type {string[]}
17
+ */
18
+ const descLines = [];
19
+ jsdoc.source.some(({
20
+ tokens: {
21
+ delimiter,
22
+ description,
23
+ end,
24
+ tag,
25
+ },
26
+ }, idx) => {
27
+ if (delimiter === '/**') {
28
+ return false;
29
+ }
30
+
31
+ if (tag || end) {
32
+ return true;
33
+ }
34
+
35
+ if (startIdx === -1) {
36
+ startIdx = idx;
37
+ }
38
+
39
+ descLines.push(description);
40
+
41
+ return false;
42
+ });
43
+
44
+ if (!descLines.length) {
45
+ return;
46
+ }
47
+
48
+ let leadingBlankCount = 0;
49
+ while (
50
+ leadingBlankCount < descLines.length &&
51
+ anyWhitespaceLine.test(descLines[leadingBlankCount])
52
+ ) {
53
+ leadingBlankCount++;
54
+ }
55
+
56
+ const allBlank = leadingBlankCount === descLines.length;
57
+
58
+ /**
59
+ * Rebuilds the kept description lines after dropping `dropCount` leading
60
+ * blank lines.
61
+ * @param {import('../iterateJsdoc.js').Integer} dropCount
62
+ * @returns {() => void}
63
+ */
64
+ const dropLeadingBlankLines = (dropCount) => {
65
+ return () => {
66
+ utils.setBlockDescription((info, seedTokens, descriptions, postDelimiters) => {
67
+ return descriptions.slice(dropCount).map((description, idx) => {
68
+ return {
69
+ number: 0,
70
+ source: '',
71
+ tokens: seedTokens({
72
+ ...info,
73
+ description,
74
+ postDelimiter: description ? postDelimiters[idx + dropCount] : '',
75
+ }),
76
+ };
77
+ });
78
+ });
79
+ };
80
+ };
81
+
82
+ if (hasTags) {
83
+ if (!leadingBlankCount) {
84
+ return;
56
85
  }
86
+
87
+ utils.reportJSDoc(
88
+ 'There should be no blank lines in block descriptions followed by tags.',
89
+ {
90
+ line: startIdx + leadingBlankCount - 1,
91
+ },
92
+ dropLeadingBlankLines(leadingBlankCount),
93
+ );
94
+
95
+ return;
96
+ }
97
+
98
+ // Without tags, only the extra (removable) leading blank lines are a problem;
99
+ // a single leading blank line with no following content is allowed.
100
+ const removeCount = allBlank ? descLines.length - 1 : leadingBlankCount;
101
+
102
+ if (removeCount < 1) {
103
+ return;
57
104
  }
105
+
106
+ utils.reportJSDoc(
107
+ 'There should be no extra blank lines in block descriptions not followed by tags.',
108
+ {
109
+ line: startIdx + removeCount,
110
+ },
111
+ dropLeadingBlankLines(removeCount),
112
+ );
58
113
  }, {
59
114
  iterateAllJsdocs: true,
60
115
  meta: {
@@ -1,3 +1,4 @@
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 {
@@ -39,7 +40,7 @@ const isLiteralType = (type) => {
39
40
  }
40
41
 
41
42
  // Check using TypeFlags for specific edge cases like boolean or template literals
42
- // eslint-disable-next-line no-bitwise -- Convenient
43
+
43
44
  if (type.flags & (ts.TypeFlags.BooleanLiteral | ts.TypeFlags.TemplateLiteral)) {
44
45
  return true;
45
46
  }
@@ -61,9 +62,13 @@ const isLiteralType = (type) => {
61
62
 
62
63
  export default iterateJsdoc(({
63
64
  context,
65
+ jsdoc,
66
+ jsdocNode,
64
67
  node: nde,
65
68
  report,
69
+ sourceCode,
66
70
  utils,
71
+ // eslint-disable-next-line complexity -- Numerous type/option permutations
67
72
  }) => {
68
73
  /* c8 ignore next 4 -- Guard */
69
74
  // Already handled
@@ -81,10 +86,41 @@ export default iterateJsdoc(({
81
86
  const {
82
87
  // https://typescript-eslint.io/rules/no-unnecessary-type-assertion/
83
88
  checkLiteralConstAssertions = false,
89
+ enableFixer = true,
84
90
  treatAnyAsRedundant = false,
85
91
  typesToIgnore = [],
86
92
  } = context.options[0] ?? {};
87
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
+
88
124
  const node =
89
125
  /**
90
126
  * @type {import('@typescript-eslint/utils').TSESTree.Node}
@@ -112,37 +148,201 @@ export default iterateJsdoc(({
112
148
  // const services = ESLintUtils.getParserServices(context);
113
149
  const checker = services.program.getTypeChecker();
114
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
+
115
157
  /**
116
- * @param {string} typeStr
117
- * @returns {any} `ts.Type`
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}
118
167
  */
119
- const getTypeFromString = (typeStr) => {
120
- // 2. Parse the type string into a virtual TypeScript SourceFile
121
- const virtualSourceFile = ts.createSourceFile(
122
- 'inline-type-eval.ts',
123
- `type __TargetType = ${typeStr};`,
124
- ts.ScriptTarget.Latest,
125
- true,
126
- );
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);
127
175
 
128
- // 3. Find the TypeNode in the AST (type __TargetType = <TypeNode>;)
129
- // Type `ts.TypeAliasDeclaration`
130
- const typeAlias = /** @type {any} */ (
131
- virtualSourceFile.statements[0]
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,
132
205
  );
133
- const typeNode = typeAlias.type;
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
+ }
134
253
 
135
- // 4. Resolve the type using the TypeChecker
136
- // Note: If the type string references external symbols or types from your
137
- // actual codebase, the checker may require a complete program context to resolve them.
138
- return checker.getTypeFromTypeNode(typeNode);
254
+ const {
255
+ objectFlags,
256
+ } = /** @type {import('typescript').ObjectType} */ (type);
257
+ return (objectFlags & ts.ObjectFlags.EmptyObjectLiteral) !== 0 ||
258
+ checker.getPropertiesOfType(type).length === 0;
139
259
  };
140
260
 
141
- // Todo: Support more than just VariableDeclaration!
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
+ }
142
315
 
143
- // 3. For this example, let's assume we are checking VariableDeclarators
144
- // e.g., `/** @type {number} */ const x = 5;`
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;`
145
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;`.
146
346
  const decl = node.declarations[0];
147
347
 
148
348
  /* c8 ignore next 4 -- How to cover? */
@@ -151,59 +351,146 @@ export default iterateJsdoc(({
151
351
  return;
152
352
  }
153
353
 
154
- // 4. Map the ESLint AST nodes to TypeScript AST nodes
155
- // const tsNode = services.esTreeNodeToTSNodeMap.get(decl.id);
156
- const tsInit = services.esTreeNodeToTSNodeMap.get(decl.init);
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
+ );
157
361
 
158
- // 5. Get the types
159
- // tsNode type includes the JSDoc explicit type
160
- // tsInit type is the purely inferred type from the right-hand side
161
- const inferredType = checker.getTypeAtLocation(tsInit);
362
+ /* c8 ignore next 3 -- Every `@type` tag reaching here has a bound TypeNode */
363
+ if (!jsdocTypeNode) {
364
+ return;
365
+ }
162
366
 
163
- // 6. Compare the types.
164
- const declaredTypeStr = types[0].type;
165
- const declaredType = getTypeFromString(declaredTypeStr);
367
+ const declInitTsNode = services.esTreeNodeToTSNodeMap.get(decl.init);
368
+ if (isGenericCall(declInitTsNode) || isInterpolatedTemplateLiteral(decl.init)) {
369
+ return;
370
+ }
166
371
 
167
- const isRedundantType = checker.isTypeAssignableTo(
168
- inferredType,
169
- declaredType,
170
- );
372
+ const declInferredType = checker.getTypeAtLocation(declInitTsNode);
373
+ const declAssertedType = checker.getTypeFromTypeNode(jsdocTypeNode);
171
374
 
172
- if (declaredTypeStr === 'const') {
173
- if (checkLiteralConstAssertions && isLiteralType(inferredType)) {
174
- report(
175
- 'The @type tag declaring "{{ type }}" is redundant as TypeScript infers it automatically for literals.',
176
- null,
177
- types[0],
178
- {
179
- type: declaredTypeStr,
180
- },
181
- );
182
- }
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
+ }
183
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) {
184
420
  return;
185
421
  }
422
+ }
186
423
 
187
- if (
188
- isRedundantType && (treatAnyAsRedundant || declaredTypeStr !== 'any') &&
189
- !typesToIgnore.includes(declaredTypeStr)
190
- ) {
191
- report(
192
- 'The @type tag declaring "{{ type }}" is redundant as TypeScript infers it automatically.',
193
- null,
194
- types[0],
195
- {
196
- type: declaredTypeStr,
197
- },
198
- );
199
- }
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;
200
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
+ );
201
487
  }, {
202
488
  iterateAllJsdocs: true,
203
489
  meta: {
204
490
  docs: {
205
491
  description: 'Reports redundant @type tags that match or broaden the naturally inferred TypeScript type.',
206
492
  },
493
+ fixable: 'code',
207
494
  schema: [
208
495
  {
209
496
  additionalProperties: false,
@@ -212,6 +499,10 @@ export default iterateJsdoc(({
212
499
  description: 'Whether to check `const` type assertions as redundant',
213
500
  type: 'boolean',
214
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
+ },
215
506
  treatAnyAsRedundant: {
216
507
  description: 'Whether to treat `any` type casts as redundant',
217
508
  type: 'boolean',
@@ -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
@@ -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
  */