eslint-plugin-jsdoc 57.2.1 → 58.0.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.
@@ -0,0 +1,472 @@
1
+ import iterateJsdoc from './iterateJsdoc.js';
2
+ import {
3
+ parse,
4
+ stringify,
5
+ traverse,
6
+ tryParse,
7
+ } from '@es-joy/jsdoccomment';
8
+
9
+ /**
10
+ * Adjusts the parent type node `meta` for generic matches (or type node
11
+ * `type` for `JsdocTypeAny`) and sets the type node `value`.
12
+ * @param {string} type The actual type
13
+ * @param {string} preferred The preferred type
14
+ * @param {boolean} isGenericMatch
15
+ * @param {string} typeNodeName
16
+ * @param {import('jsdoc-type-pratt-parser').NonRootResult} node
17
+ * @param {import('jsdoc-type-pratt-parser').NonRootResult|undefined} parentNode
18
+ * @returns {void}
19
+ */
20
+ const adjustNames = (type, preferred, isGenericMatch, typeNodeName, node, parentNode) => {
21
+ let ret = preferred;
22
+ if (isGenericMatch) {
23
+ const parentMeta = /** @type {import('jsdoc-type-pratt-parser').GenericResult} */ (
24
+ parentNode
25
+ ).meta;
26
+ if (preferred === '[]') {
27
+ parentMeta.brackets = 'square';
28
+ parentMeta.dot = false;
29
+ ret = 'Array';
30
+ } else {
31
+ const dotBracketEnd = preferred.match(/\.(?:<>)?$/v);
32
+ if (dotBracketEnd) {
33
+ parentMeta.brackets = 'angle';
34
+ parentMeta.dot = true;
35
+ ret = preferred.slice(0, -dotBracketEnd[0].length);
36
+ } else {
37
+ const bracketEnd = preferred.endsWith('<>');
38
+ if (bracketEnd) {
39
+ parentMeta.brackets = 'angle';
40
+ parentMeta.dot = false;
41
+ ret = preferred.slice(0, -2);
42
+ } else if (
43
+ parentMeta?.brackets === 'square' &&
44
+ (typeNodeName === '[]' || typeNodeName === 'Array')
45
+ ) {
46
+ parentMeta.brackets = 'angle';
47
+ parentMeta.dot = false;
48
+ }
49
+ }
50
+ }
51
+ } else if (type === 'JsdocTypeAny') {
52
+ node.type = 'JsdocTypeName';
53
+ }
54
+
55
+ /** @type {import('jsdoc-type-pratt-parser').NameResult} */ (
56
+ node
57
+ ).value = ret.replace(/(?:\.|<>|\.<>|\[\])$/v, '');
58
+
59
+ // For bare pseudo-types like `<>`
60
+ if (!ret) {
61
+ /** @type {import('jsdoc-type-pratt-parser').NameResult} */ (
62
+ node
63
+ ).value = typeNodeName;
64
+ }
65
+ };
66
+
67
+ /**
68
+ * @param {boolean} [upperCase]
69
+ * @returns {string}
70
+ */
71
+ const getMessage = (upperCase) => {
72
+ return 'Use object shorthand or index signatures instead of ' +
73
+ '`' + (upperCase ? 'O' : 'o') + 'bject`, e.g., `{[key: string]: string}`';
74
+ };
75
+
76
+ /**
77
+ * @type {{
78
+ * message: string,
79
+ * replacement: false
80
+ * }}
81
+ */
82
+ const info = {
83
+ message: getMessage(),
84
+ replacement: false,
85
+ };
86
+
87
+ /**
88
+ * @type {{
89
+ * message: string,
90
+ * replacement: false
91
+ * }}
92
+ */
93
+ const infoUC = {
94
+ message: getMessage(true),
95
+ replacement: false,
96
+ };
97
+
98
+ /**
99
+ * @param {{
100
+ * checkNativeTypes?: import('./rules/checkTypes.js').CheckNativeTypes|null
101
+ * overrideSettings?: import('./iterateJsdoc.js').Settings['preferredTypes']|null,
102
+ * description?: string,
103
+ * schema?: import('eslint').Rule.RuleMetaData['schema'],
104
+ * typeName?: string,
105
+ * url?: string,
106
+ * }} cfg
107
+ * @returns {import('@eslint/core').RuleDefinition<
108
+ * import('@eslint/core').RuleDefinitionTypeOptions
109
+ * >}
110
+ */
111
+ export const buildRejectOrPreferRuleDefinition = ({
112
+ checkNativeTypes = null,
113
+ typeName,
114
+ description = typeName ?? 'Reports invalid types.',
115
+ overrideSettings = null,
116
+ schema = [],
117
+ url = 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/check-types.md#repos-sticky-header',
118
+ }) => {
119
+ return iterateJsdoc(
120
+ ({
121
+ context,
122
+ jsdocNode,
123
+ report,
124
+ settings,
125
+ sourceCode,
126
+ utils,
127
+ }) => {
128
+ const jsdocTagsWithPossibleType = utils.filterTags((tag) => {
129
+ return Boolean(utils.tagMightHaveTypePosition(tag.tag));
130
+ });
131
+
132
+ const
133
+ /**
134
+ * @type {{
135
+ * preferredTypes: import('./iterateJsdoc.js').PreferredTypes,
136
+ * structuredTags: import('./iterateJsdoc.js').StructuredTags,
137
+ * mode: import('./jsdocUtils.js').ParserMode
138
+ * }}
139
+ */
140
+ {
141
+ mode,
142
+ preferredTypes: preferredTypesOriginal,
143
+ structuredTags,
144
+ } = overrideSettings ? {
145
+ mode: settings.mode,
146
+ preferredTypes: overrideSettings,
147
+ structuredTags: {},
148
+ } : settings;
149
+
150
+ const injectObjectPreferredTypes = !('Object' in preferredTypesOriginal ||
151
+ 'object' in preferredTypesOriginal ||
152
+ 'object.<>' in preferredTypesOriginal ||
153
+ 'Object.<>' in preferredTypesOriginal ||
154
+ 'object<>' in preferredTypesOriginal);
155
+
156
+ /** @type {import('./iterateJsdoc.js').PreferredTypes} */
157
+ const typeToInject = mode === 'typescript' ?
158
+ {
159
+ Object: 'object',
160
+ 'object.<>': info,
161
+ 'Object.<>': infoUC,
162
+ 'object<>': info,
163
+ 'Object<>': infoUC,
164
+ } :
165
+ {
166
+ Object: 'object',
167
+ 'object.<>': 'Object<>',
168
+ 'Object.<>': 'Object<>',
169
+ 'object<>': 'Object<>',
170
+ };
171
+
172
+ /** @type {import('./iterateJsdoc.js').PreferredTypes} */
173
+ const preferredTypes = {
174
+ ...injectObjectPreferredTypes ?
175
+ typeToInject :
176
+ {},
177
+ ...preferredTypesOriginal,
178
+ };
179
+
180
+ const
181
+ /**
182
+ * @type {{
183
+ * noDefaults: boolean,
184
+ * unifyParentAndChildTypeChecks: boolean,
185
+ * exemptTagContexts: ({
186
+ * tag: string,
187
+ * types: true|string[]
188
+ * })[]
189
+ * }}
190
+ */ {
191
+ exemptTagContexts = [],
192
+ noDefaults,
193
+ unifyParentAndChildTypeChecks,
194
+ } = context.options[0] || {};
195
+
196
+ /**
197
+ * Gets information about the preferred type: whether there is a matching
198
+ * preferred type, what the type is, and whether it is a match to a generic.
199
+ * @param {string} _type Not currently in use
200
+ * @param {string} typeNodeName
201
+ * @param {import('jsdoc-type-pratt-parser').NonRootResult|undefined} parentNode
202
+ * @param {string|undefined} property
203
+ * @returns {[hasMatchingPreferredType: boolean, typeName: string, isGenericMatch: boolean]}
204
+ */
205
+ const getPreferredTypeInfo = (_type, typeNodeName, parentNode, property) => {
206
+ let hasMatchingPreferredType = false;
207
+ let isGenericMatch = false;
208
+ let typName = typeNodeName;
209
+
210
+ const isNameOfGeneric = parentNode !== undefined && parentNode.type === 'JsdocTypeGeneric' && property === 'left';
211
+
212
+ const brackets = /** @type {import('jsdoc-type-pratt-parser').GenericResult} */ (
213
+ parentNode
214
+ )?.meta?.brackets;
215
+ const dot = /** @type {import('jsdoc-type-pratt-parser').GenericResult} */ (
216
+ parentNode
217
+ )?.meta?.dot;
218
+
219
+ if (brackets === 'angle') {
220
+ const checkPostFixes = dot ? [
221
+ '.', '.<>',
222
+ ] : [
223
+ '<>',
224
+ ];
225
+ isGenericMatch = checkPostFixes.some((checkPostFix) => {
226
+ const preferredType = preferredTypes?.[typeNodeName + checkPostFix];
227
+
228
+ // Does `unifyParentAndChildTypeChecks` need to be checked here?
229
+ if (
230
+ (unifyParentAndChildTypeChecks || isNameOfGeneric ||
231
+ /* c8 ignore next 2 -- If checking `unifyParentAndChildTypeChecks` */
232
+ (typeof preferredType === 'object' &&
233
+ preferredType?.unifyParentAndChildTypeChecks)
234
+ ) &&
235
+ preferredType !== undefined
236
+ ) {
237
+ typName += checkPostFix;
238
+
239
+ return true;
240
+ }
241
+
242
+ return false;
243
+ });
244
+ }
245
+
246
+ if (
247
+ !isGenericMatch && property &&
248
+ /** @type {import('jsdoc-type-pratt-parser').NonRootResult} */ (
249
+ parentNode
250
+ ).type === 'JsdocTypeGeneric'
251
+ ) {
252
+ const checkPostFixes = dot ? [
253
+ '.', '.<>',
254
+ ] : [
255
+ brackets === 'angle' ? '<>' : '[]',
256
+ ];
257
+
258
+ isGenericMatch = checkPostFixes.some((checkPostFix) => {
259
+ const preferredType = preferredTypes?.[checkPostFix];
260
+ if (
261
+ // Does `unifyParentAndChildTypeChecks` need to be checked here?
262
+ (unifyParentAndChildTypeChecks || isNameOfGeneric ||
263
+ /* c8 ignore next 2 -- If checking `unifyParentAndChildTypeChecks` */
264
+ (typeof preferredType === 'object' &&
265
+ preferredType?.unifyParentAndChildTypeChecks)) &&
266
+ preferredType !== undefined
267
+ ) {
268
+ typName = checkPostFix;
269
+
270
+ return true;
271
+ }
272
+
273
+ return false;
274
+ });
275
+ }
276
+
277
+ const prefType = preferredTypes?.[typeNodeName];
278
+ const directNameMatch = prefType !== undefined &&
279
+ !Object.values(preferredTypes).includes(typeNodeName);
280
+ const specificUnify = typeof prefType === 'object' &&
281
+ prefType?.unifyParentAndChildTypeChecks;
282
+ const unifiedSyntaxParentMatch = property && directNameMatch && (unifyParentAndChildTypeChecks || specificUnify);
283
+ isGenericMatch = isGenericMatch || Boolean(unifiedSyntaxParentMatch);
284
+
285
+ hasMatchingPreferredType = isGenericMatch ||
286
+ directNameMatch && !property;
287
+
288
+ return [
289
+ hasMatchingPreferredType, typName, isGenericMatch,
290
+ ];
291
+ };
292
+
293
+ /**
294
+ * Collect invalid type info.
295
+ * @param {string} type
296
+ * @param {string} value
297
+ * @param {string} tagName
298
+ * @param {string} nameInTag
299
+ * @param {number} idx
300
+ * @param {string|undefined} property
301
+ * @param {import('jsdoc-type-pratt-parser').NonRootResult} node
302
+ * @param {import('jsdoc-type-pratt-parser').NonRootResult|undefined} parentNode
303
+ * @param {(string|false|undefined)[][]} invalidTypes
304
+ * @returns {void}
305
+ */
306
+ const getInvalidTypes = (type, value, tagName, nameInTag, idx, property, node, parentNode, invalidTypes) => {
307
+ let typeNodeName = type === 'JsdocTypeAny' ? '*' : value;
308
+
309
+ const [
310
+ hasMatchingPreferredType,
311
+ typName,
312
+ isGenericMatch,
313
+ ] = getPreferredTypeInfo(type, typeNodeName, parentNode, property);
314
+
315
+ let preferred;
316
+ let types;
317
+ if (hasMatchingPreferredType) {
318
+ const preferredSetting = preferredTypes[typName];
319
+ typeNodeName = typName === '[]' ? typName : typeNodeName;
320
+
321
+ if (!preferredSetting) {
322
+ invalidTypes.push([
323
+ typeNodeName,
324
+ ]);
325
+ } else if (typeof preferredSetting === 'string') {
326
+ preferred = preferredSetting;
327
+ invalidTypes.push([
328
+ typeNodeName, preferred,
329
+ ]);
330
+ } else if (preferredSetting && typeof preferredSetting === 'object') {
331
+ const nextItem = preferredSetting.skipRootChecking && jsdocTagsWithPossibleType[idx + 1];
332
+
333
+ if (!nextItem || !nextItem.name.startsWith(`${nameInTag}.`)) {
334
+ preferred = preferredSetting.replacement;
335
+ invalidTypes.push([
336
+ typeNodeName,
337
+ preferred,
338
+ preferredSetting.message,
339
+ ]);
340
+ }
341
+ } else {
342
+ utils.reportSettings(
343
+ 'Invalid `settings.jsdoc.preferredTypes`. Values must be falsy, a string, or an object.',
344
+ );
345
+
346
+ return;
347
+ }
348
+ } else if (Object.entries(structuredTags).some(([
349
+ tag,
350
+ {
351
+ type: typs,
352
+ },
353
+ ]) => {
354
+ types = typs;
355
+
356
+ return tag === tagName &&
357
+ Array.isArray(types) &&
358
+ !types.includes(typeNodeName);
359
+ })) {
360
+ invalidTypes.push([
361
+ typeNodeName, types,
362
+ ]);
363
+ } else if (checkNativeTypes && !noDefaults && type === 'JsdocTypeName') {
364
+ preferred = checkNativeTypes(
365
+ preferredTypes, typeNodeName, preferred, parentNode, invalidTypes,
366
+ );
367
+ }
368
+
369
+ // For fixer
370
+ if (preferred) {
371
+ adjustNames(type, preferred, isGenericMatch, typeNodeName, node, parentNode);
372
+ }
373
+ };
374
+
375
+ for (const [
376
+ idx,
377
+ jsdocTag,
378
+ ] of jsdocTagsWithPossibleType.entries()) {
379
+ /** @type {(string|false|undefined)[][]} */
380
+ const invalidTypes = [];
381
+ let typeAst;
382
+
383
+ try {
384
+ typeAst = mode === 'permissive' ? tryParse(jsdocTag.type) : parse(jsdocTag.type, mode);
385
+ } catch {
386
+ continue;
387
+ }
388
+
389
+ const {
390
+ name: nameInTag,
391
+ tag: tagName,
392
+ } = jsdocTag;
393
+
394
+ traverse(typeAst, (node, parentNode, property) => {
395
+ const {
396
+ type,
397
+ value,
398
+ } =
399
+ /**
400
+ * @type {import('jsdoc-type-pratt-parser').NameResult}
401
+ */ (node);
402
+ if (![
403
+ 'JsdocTypeAny', 'JsdocTypeName',
404
+ ].includes(type)) {
405
+ return;
406
+ }
407
+
408
+ getInvalidTypes(type, value, tagName, nameInTag, idx, property, node, parentNode, invalidTypes);
409
+ });
410
+
411
+ if (invalidTypes.length) {
412
+ const fixedType = stringify(typeAst);
413
+
414
+ /**
415
+ * @type {import('eslint').Rule.ReportFixer}
416
+ */
417
+ const fix = (fixer) => {
418
+ return fixer.replaceText(
419
+ jsdocNode,
420
+ sourceCode.getText(jsdocNode).replace(
421
+ `{${jsdocTag.type}}`,
422
+ `{${fixedType}}`,
423
+ ),
424
+ );
425
+ };
426
+
427
+ for (const [
428
+ badType,
429
+ preferredType = '',
430
+ msg,
431
+ ] of invalidTypes) {
432
+ const tagValue = jsdocTag.name ? ` "${jsdocTag.name}"` : '';
433
+ if (exemptTagContexts.some(({
434
+ tag,
435
+ types,
436
+ }) => {
437
+ return tag === tagName &&
438
+ (types === true || types.includes(jsdocTag.type));
439
+ })) {
440
+ continue;
441
+ }
442
+
443
+ report(
444
+ msg ||
445
+ `Invalid JSDoc @${tagName}${tagValue} type "${badType}"` +
446
+ (preferredType ? '; ' : '.') +
447
+ (preferredType ? `prefer: ${JSON.stringify(preferredType)}.` : ''),
448
+ preferredType ? fix : null,
449
+ jsdocTag,
450
+ msg ? {
451
+ tagName,
452
+ tagValue,
453
+ } : undefined,
454
+ );
455
+ }
456
+ }
457
+ }
458
+ },
459
+ {
460
+ iterateAllJsdocs: true,
461
+ meta: {
462
+ docs: {
463
+ description,
464
+ url,
465
+ },
466
+ fixable: 'code',
467
+ schema,
468
+ type: 'suggestion',
469
+ },
470
+ },
471
+ );
472
+ };
package/src/index-cjs.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import {
2
2
  buildForbidRuleDefinition,
3
3
  } from './buildForbidRuleDefinition.js';
4
+ import {
5
+ buildRejectOrPreferRuleDefinition,
6
+ } from './buildRejectOrPreferRuleDefinition.js';
4
7
  import {
5
8
  getJsdocProcessorPlugin,
6
9
  } from './getJsdocProcessorPlugin.js';
@@ -107,6 +110,33 @@ index.rules = {
107
110
  'no-restricted-syntax': noRestrictedSyntax,
108
111
  'no-types': noTypes,
109
112
  'no-undefined-types': noUndefinedTypes,
113
+ 'reject-any-type': buildRejectOrPreferRuleDefinition({
114
+ description: 'Reports use of `any` or `*` type',
115
+ overrideSettings: {
116
+ '*': {
117
+ message: 'Prefer a more specific type to `*`',
118
+ replacement: false,
119
+ unifyParentAndChildTypeChecks: true,
120
+ },
121
+ any: {
122
+ message: 'Prefer a more specific type to `any`',
123
+ replacement: false,
124
+ unifyParentAndChildTypeChecks: true,
125
+ },
126
+ },
127
+ url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/reject-any-type.md#repos-sticky-header',
128
+ }),
129
+ 'reject-function-type': buildRejectOrPreferRuleDefinition({
130
+ description: 'Reports use of `Function` type',
131
+ overrideSettings: {
132
+ Function: {
133
+ message: 'Prefer a more specific type to `Function`',
134
+ replacement: false,
135
+ unifyParentAndChildTypeChecks: true,
136
+ },
137
+ },
138
+ url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/reject-function-type.md#repos-sticky-header',
139
+ }),
110
140
  'require-asterisk-prefix': requireAsteriskPrefix,
111
141
  'require-description': requireDescription,
112
142
  'require-description-complete-sentence': requireDescriptionCompleteSentence,
@@ -122,7 +152,7 @@ index.rules = {
122
152
  message: '@next should have a type',
123
153
  },
124
154
  ],
125
- description: 'Requires a type for @next tags',
155
+ description: 'Requires a type for `@next` tags',
126
156
  url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-next-type.md#repos-sticky-header',
127
157
  }),
128
158
  'require-param': requireParam,
@@ -147,7 +177,7 @@ index.rules = {
147
177
  message: '@throws should have a type',
148
178
  },
149
179
  ],
150
- description: 'Requires a type for @throws tags',
180
+ description: 'Requires a type for `@throws` tags',
151
181
  url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-throws-type.md#repos-sticky-header',
152
182
  }),
153
183
  'require-yields': requireYields,
@@ -160,7 +190,7 @@ index.rules = {
160
190
  message: '@yields should have a type',
161
191
  },
162
192
  ],
163
- description: 'Requires a type for @yields tags',
193
+ description: 'Requires a type for `@yields` tags',
164
194
  url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-yields-type.md#repos-sticky-header',
165
195
  }),
166
196
  'sort-tags': sortTags,
@@ -218,6 +248,8 @@ const createRecommendedRuleset = (warnOrError, flatName) => {
218
248
  'jsdoc/no-restricted-syntax': 'off',
219
249
  'jsdoc/no-types': 'off',
220
250
  'jsdoc/no-undefined-types': warnOrError,
251
+ 'jsdoc/reject-any-type': warnOrError,
252
+ 'jsdoc/reject-function-type': warnOrError,
221
253
  'jsdoc/require-asterisk-prefix': 'off',
222
254
  'jsdoc/require-description': 'off',
223
255
  'jsdoc/require-description-complete-sentence': 'off',
package/src/index-esm.js CHANGED
@@ -8,6 +8,9 @@ import index from './index-cjs.js';
8
8
  import {
9
9
  buildForbidRuleDefinition,
10
10
  } from './buildForbidRuleDefinition.js';
11
+ import {
12
+ buildRejectOrPreferRuleDefinition,
13
+ } from './buildRejectOrPreferRuleDefinition.js';
11
14
 
12
15
  // eslint-disable-next-line unicorn/prefer-export-from --- Reusing `index`
13
16
  export default index;
@@ -22,7 +25,7 @@ export default index;
22
25
  * settings?: Partial<import('./iterateJsdoc.js').Settings>,
23
26
  * rules?: {[key in keyof import('./rules.d.ts').Rules]?: import('eslint').Linter.RuleEntry<import('./rules.d.ts').Rules[key]>},
24
27
  * extraRuleDefinitions?: {
25
- * forbid: {
28
+ * forbid?: {
26
29
  * [contextName: string]: {
27
30
  * description?: string,
28
31
  * url?: string,
@@ -32,6 +35,19 @@ export default index;
32
35
  * comment: string
33
36
  * })[]
34
37
  * }
38
+ * },
39
+ * preferTypes?: {
40
+ * [typeName: string]: {
41
+ * description: string,
42
+ * overrideSettings: {
43
+ * [typeNodeName: string]: {
44
+ * message: string,
45
+ * replacement?: false|string,
46
+ * unifyParentAndChildTypeChecks?: boolean,
47
+ * }
48
+ * },
49
+ * url: string,
50
+ * }
35
51
  * }
36
52
  * }
37
53
  * }
@@ -125,6 +141,25 @@ export const jsdoc = function (cfg) {
125
141
  });
126
142
  }
127
143
  }
144
+
145
+ if (cfg.extraRuleDefinitions.preferTypes) {
146
+ for (const [
147
+ typeName,
148
+ {
149
+ description,
150
+ overrideSettings,
151
+ url,
152
+ },
153
+ ] of Object.entries(cfg.extraRuleDefinitions.preferTypes)) {
154
+ outputConfig.plugins.jsdoc.rules[`prefer-type-${typeName}`] =
155
+ buildRejectOrPreferRuleDefinition({
156
+ description,
157
+ overrideSettings,
158
+ typeName,
159
+ url,
160
+ });
161
+ }
162
+ }
128
163
  }
129
164
  }
130
165