eslint-plugin-jsdoc 64.2.1 → 64.3.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
@@ -512,6 +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
+ ||| [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. |
515
516
  ||:wrench:| [normalize-see-links](./docs/rules/normalize-see-links.md#readme) | Normalizes labeled links in `@see` tags to a canonical `{@link}` form. |
516
517
  ||:wrench:| [prefer-import-tag](./docs/rules/prefer-import-tag.md#readme) | Prefer `@import` tags to inline `import()` statements. |
517
518
  |:heavy_check_mark:|| [reject-any-type](./docs/rules/reject-any-type.md#readme) | Reports use of `any` or `*` type |
@@ -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,26 @@ 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 treat `any` type casts as redundant
1325
+ */
1326
+ treatAnyAsRedundant?: boolean;
1327
+ /**
1328
+ * An array list of types to ignore
1329
+ */
1330
+ typesToIgnore?: string[];
1331
+ }
1332
+ ];
1333
+
1314
1334
  /** Normalizes labeled links in `@see` tags to a canonical `{@link}` form. */
1315
1335
  "jsdoc/normalize-see-links":
1316
1336
  | []
package/package.json CHANGED
@@ -7,6 +7,7 @@
7
7
  "dependencies": {
8
8
  "@es-joy/jsdoccomment": "~0.95.1",
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",
@@ -155,5 +156,5 @@
155
156
  "test-cov": "TIMING=1 c8 --reporter text pnpm run test-no-cov",
156
157
  "test-index": "pnpm run test-no-cov test/rules/index.js"
157
158
  },
158
- "version": "64.2.1"
159
+ "version": "64.3.0"
159
160
  }
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';
@@ -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,
@@ -0,0 +1,226 @@
1
+ /* eslint-disable jsdoc/reject-any-type -- TypeScript types need not be installed */
2
+ import iterateJsdoc from '../iterateJsdoc.js';
3
+ import {
4
+ createRequire,
5
+ } from 'module';
6
+
7
+ /** @type {any} */
8
+ let ts;
9
+
10
+ // 1. Create a require function bound to the current file's URL
11
+ const require = createRequire(import.meta.url);
12
+
13
+ try {
14
+ // 2. Attempt to import the package synchronously
15
+ ts = require('typescript');
16
+ /* c8 ignore next 10 -- Guard */
17
+ } catch (error) {
18
+ // 3. Fall back gracefully if it is not installed
19
+ if (/** @type {{code?: string}} */ (error).code !== 'MODULE_NOT_FOUND') {
20
+ // Re-throw if it's a different error (e.g., syntax error inside the package)
21
+ throw error;
22
+ }
23
+
24
+ // eslint-disable-next-line no-console -- Warning user
25
+ console.warn('⚠️ typescript is not installed. `jsdoc/no-unnecessary-type-assertion` will not work. To disable this warning, you must disable the rule.');
26
+ }
27
+
28
+ // Helper to check for standard literals and boolean/enum/template literals
29
+ /**
30
+ * @param {any} type The type is `ts.Type`
31
+ * @returns {boolean}
32
+ */
33
+ const isLiteralType = (type) => {
34
+ // Standard primitive literals (string, number, bigint, boolean literal flags)
35
+ if (type.isLiteral()) {
36
+ return true;
37
+ }
38
+
39
+ // Check using TypeFlags for specific edge cases like boolean or template literals
40
+ // eslint-disable-next-line no-bitwise -- Convenient
41
+ if (type.flags & (ts.TypeFlags.BooleanLiteral | ts.TypeFlags.TemplateLiteral)) {
42
+ return true;
43
+ }
44
+
45
+ // If dealing with a union (e.g. 'a' | 'b'), you can check if all constituents are literals
46
+ if (type.isUnion()) {
47
+ return type.types.every(
48
+ /**
49
+ * @param {any} typ
50
+ */
51
+ (typ) => {
52
+ return isLiteralType(typ);
53
+ },
54
+ );
55
+ }
56
+
57
+ return false;
58
+ };
59
+
60
+ export default iterateJsdoc(({
61
+ context,
62
+ node: nde,
63
+ report,
64
+ utils,
65
+ }) => {
66
+ /* c8 ignore next 4 -- Guard */
67
+ // Already handled
68
+ if (!ts) {
69
+ return;
70
+ }
71
+
72
+ const types = utils.getTags('type');
73
+
74
+ // 1. Only proceed if there is a @type tag
75
+ if (!types.length) {
76
+ return;
77
+ }
78
+
79
+ const {
80
+ // https://typescript-eslint.io/rules/no-unnecessary-type-assertion/
81
+ checkLiteralConstAssertions = false,
82
+ treatAnyAsRedundant = false,
83
+ typesToIgnore = [],
84
+ } = context.options[0] ?? {};
85
+
86
+ const node =
87
+ /**
88
+ * @type {import('@typescript-eslint/utils').TSESTree.Node}
89
+ */ (nde);
90
+
91
+ // 2. Safely get TypeScript parser services and TypeChecker
92
+ const services =
93
+ /**
94
+ * @type {import('@typescript-eslint/utils').ParserServices}
95
+ */ (context.sourceCode.parserServices);
96
+
97
+ /* c8 ignore next 6 -- Guard */
98
+ if (!services || !services.program) {
99
+ // Cannot proceed without type-aware linting enabled
100
+ // eslint-disable-next-line no-console -- Feedback
101
+ console.warn('⚠️ You must point ESLint to the `typescript-eslint` parser using `languageOptions`. See the documentation for `jsdoc/no-unnecessary-type-assertion`.');
102
+ return;
103
+ }
104
+
105
+ // 1. Grab the parser services and the active TypeChecker
106
+ // const services = ESLintUtils.getParserServices(context);
107
+ const checker = services.program.getTypeChecker();
108
+
109
+ /**
110
+ * @param {string} typeStr
111
+ * @returns {any} `ts.Type`
112
+ */
113
+ const getTypeFromString = (typeStr) => {
114
+ // 2. Parse the type string into a virtual TypeScript SourceFile
115
+ const virtualSourceFile = ts.createSourceFile(
116
+ 'inline-type-eval.ts',
117
+ `type __TargetType = ${typeStr};`,
118
+ ts.ScriptTarget.Latest,
119
+ true,
120
+ );
121
+
122
+ // 3. Find the TypeNode in the AST (type __TargetType = <TypeNode>;)
123
+ // Type `ts.TypeAliasDeclaration`
124
+ const typeAlias = /** @type {any} */ (
125
+ virtualSourceFile.statements[0]
126
+ );
127
+ const typeNode = typeAlias.type;
128
+
129
+ // 4. Resolve the type using the TypeChecker
130
+ // Note: If the type string references external symbols or types from your
131
+ // actual codebase, the checker may require a complete program context to resolve them.
132
+ return checker.getTypeFromTypeNode(typeNode);
133
+ };
134
+
135
+ // Todo: Support more than just VariableDeclaration!
136
+
137
+ // 3. For this example, let's assume we are checking VariableDeclarators
138
+ // e.g., `/** @type {number} */ const x = 5;`
139
+ if (node?.type === 'VariableDeclaration') {
140
+ const decl = node.declarations[0];
141
+
142
+ /* c8 ignore next 4 -- How to cover? */
143
+ if (!decl.init) {
144
+ // No initializer, type is likely `any`, so @type isn't redundant
145
+ return;
146
+ }
147
+
148
+ // 4. Map the ESLint AST nodes to TypeScript AST nodes
149
+ // const tsNode = services.esTreeNodeToTSNodeMap.get(decl.id);
150
+ const tsInit = services.esTreeNodeToTSNodeMap.get(decl.init);
151
+
152
+ // 5. Get the types
153
+ // tsNode type includes the JSDoc explicit type
154
+ // tsInit type is the purely inferred type from the right-hand side
155
+ const inferredType = checker.getTypeAtLocation(tsInit);
156
+
157
+ // 6. Compare the types.
158
+ const declaredTypeStr = types[0].type;
159
+ const declaredType = getTypeFromString(declaredTypeStr);
160
+
161
+ const isRedundantType = checker.isTypeAssignableTo(
162
+ inferredType,
163
+ declaredType,
164
+ );
165
+
166
+ if (declaredTypeStr === 'const') {
167
+ if (checkLiteralConstAssertions && isLiteralType(inferredType)) {
168
+ report(
169
+ 'The @type tag declaring "{{ type }}" is redundant as TypeScript infers it automatically for literals.',
170
+ null,
171
+ types[0],
172
+ {
173
+ type: declaredTypeStr,
174
+ },
175
+ );
176
+ }
177
+
178
+ return;
179
+ }
180
+
181
+ if (
182
+ isRedundantType && (treatAnyAsRedundant || declaredTypeStr !== 'any') &&
183
+ !typesToIgnore.includes(declaredTypeStr)
184
+ ) {
185
+ report(
186
+ 'The @type tag declaring "{{ type }}" is redundant as TypeScript infers it automatically.',
187
+ null,
188
+ types[0],
189
+ {
190
+ type: declaredTypeStr,
191
+ },
192
+ );
193
+ }
194
+ }
195
+ }, {
196
+ iterateAllJsdocs: true,
197
+ meta: {
198
+ docs: {
199
+ description: 'Reports redundant @type tags that match or broaden the naturally inferred TypeScript type.',
200
+ },
201
+ schema: [
202
+ {
203
+ additionalProperties: false,
204
+ properties: {
205
+ checkLiteralConstAssertions: {
206
+ description: 'Whether to check `const` type assertions as redundant',
207
+ type: 'boolean',
208
+ },
209
+ treatAnyAsRedundant: {
210
+ description: 'Whether to treat `any` type casts as redundant',
211
+ type: 'boolean',
212
+ },
213
+ typesToIgnore: {
214
+ description: 'An array list of types to ignore',
215
+ items: {
216
+ type: 'string',
217
+ },
218
+ type: 'array',
219
+ },
220
+ },
221
+ type: 'object',
222
+ },
223
+ ],
224
+ type: 'suggestion',
225
+ },
226
+ });
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,26 @@ 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 treat `any` type casts as redundant
1325
+ */
1326
+ treatAnyAsRedundant?: boolean;
1327
+ /**
1328
+ * An array list of types to ignore
1329
+ */
1330
+ typesToIgnore?: string[];
1331
+ }
1332
+ ];
1333
+
1314
1334
  /** Normalizes labeled links in `@see` tags to a canonical `{@link}` form. */
1315
1335
  "jsdoc/normalize-see-links":
1316
1336
  | []