From 52c0c16b218315f0945ce8ea55176cc6f4c8e1c0 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 16 Sep 2025 10:32:24 +0800 Subject: [PATCH 1/3] fix(typescript): descriptions need tag Markdown escaping --- src/index-cjs.js | 6 +++--- src/index.js | 6 +++--- src/rules.d.ts | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/index-cjs.js b/src/index-cjs.js index c6d5ec6eb..55c50ebdc 100644 --- a/src/index-cjs.js +++ b/src/index-cjs.js @@ -122,7 +122,7 @@ index.rules = { message: '@next should have a type', }, ], - description: 'Requires a type for @next tags', + description: 'Requires a type for `@next` tags', url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-next-type.md#repos-sticky-header', }), 'require-param': requireParam, @@ -147,7 +147,7 @@ index.rules = { message: '@throws should have a type', }, ], - description: 'Requires a type for @throws tags', + description: 'Requires a type for `@throws` tags', url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-throws-type.md#repos-sticky-header', }), 'require-yields': requireYields, @@ -160,7 +160,7 @@ index.rules = { message: '@yields should have a type', }, ], - description: 'Requires a type for @yields tags', + description: 'Requires a type for `@yields` tags', url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-yields-type.md#repos-sticky-header', }), 'sort-tags': sortTags, diff --git a/src/index.js b/src/index.js index dec8d6e95..dcd75876d 100644 --- a/src/index.js +++ b/src/index.js @@ -128,7 +128,7 @@ index.rules = { message: '@next should have a type', }, ], - description: 'Requires a type for @next tags', + description: 'Requires a type for `@next` tags', url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-next-type.md#repos-sticky-header', }), 'require-param': requireParam, @@ -153,7 +153,7 @@ index.rules = { message: '@throws should have a type', }, ], - description: 'Requires a type for @throws tags', + description: 'Requires a type for `@throws` tags', url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-throws-type.md#repos-sticky-header', }), 'require-yields': requireYields, @@ -166,7 +166,7 @@ index.rules = { message: '@yields should have a type', }, ], - description: 'Requires a type for @yields tags', + description: 'Requires a type for `@yields` tags', url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-yields-type.md#repos-sticky-header', }), 'sort-tags': sortTags, diff --git a/src/rules.d.ts b/src/rules.d.ts index 920bb604b..04b9bf961 100644 --- a/src/rules.d.ts +++ b/src/rules.d.ts @@ -557,7 +557,7 @@ export interface Rules { } ]; - /** Requires a type for @next tags */ + /** Requires a type for `@next` tags */ "jsdoc/require-next-type": []; /** Requires that all function parameters are documented. */ @@ -748,7 +748,7 @@ export interface Rules { } ]; - /** Requires a type for @throws tags */ + /** Requires a type for `@throws` tags */ "jsdoc/require-throws-type": []; /** Requires yields are documented. */ @@ -790,7 +790,7 @@ export interface Rules { } ]; - /** Requires a type for @yields tags */ + /** Requires a type for `@yields` tags */ "jsdoc/require-yields-type": []; /** Sorts tags by a specified sequence according to tag name. */ From bb607b98db9ffa22b9e1c2862146e8efa91eb32e Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 16 Sep 2025 11:39:16 +0800 Subject: [PATCH 2/3] refactor: move check-types to reusable utility --- src/buildRejectOrPreferRuleDefinition.js | 466 ++++++++++++++++++ src/index-cjs.js | 17 + src/index.js | 17 + src/rules/checkTypes.js | 593 ++++------------------- 4 files changed, 582 insertions(+), 511 deletions(-) create mode 100644 src/buildRejectOrPreferRuleDefinition.js diff --git a/src/buildRejectOrPreferRuleDefinition.js b/src/buildRejectOrPreferRuleDefinition.js new file mode 100644 index 000000000..c29142168 --- /dev/null +++ b/src/buildRejectOrPreferRuleDefinition.js @@ -0,0 +1,466 @@ +import iterateJsdoc from './iterateJsdoc.js'; +import { + parse, + stringify, + traverse, + tryParse, +} from '@es-joy/jsdoccomment'; + +/** + * Adjusts the parent type node `meta` for generic matches (or type node + * `type` for `JsdocTypeAny`) and sets the type node `value`. + * @param {string} type The actual type + * @param {string} preferred The preferred type + * @param {boolean} isGenericMatch + * @param {string} typeNodeName + * @param {import('jsdoc-type-pratt-parser').NonRootResult} node + * @param {import('jsdoc-type-pratt-parser').NonRootResult|undefined} parentNode + * @returns {void} + */ +const adjustNames = (type, preferred, isGenericMatch, typeNodeName, node, parentNode) => { + let ret = preferred; + if (isGenericMatch) { + const parentMeta = /** @type {import('jsdoc-type-pratt-parser').GenericResult} */ ( + parentNode + ).meta; + if (preferred === '[]') { + parentMeta.brackets = 'square'; + parentMeta.dot = false; + ret = 'Array'; + } else { + const dotBracketEnd = preferred.match(/\.(?:<>)?$/v); + if (dotBracketEnd) { + parentMeta.brackets = 'angle'; + parentMeta.dot = true; + ret = preferred.slice(0, -dotBracketEnd[0].length); + } else { + const bracketEnd = preferred.endsWith('<>'); + if (bracketEnd) { + parentMeta.brackets = 'angle'; + parentMeta.dot = false; + ret = preferred.slice(0, -2); + } else if ( + parentMeta?.brackets === 'square' && + (typeNodeName === '[]' || typeNodeName === 'Array') + ) { + parentMeta.brackets = 'angle'; + parentMeta.dot = false; + } + } + } + } else if (type === 'JsdocTypeAny') { + node.type = 'JsdocTypeName'; + } + + /** @type {import('jsdoc-type-pratt-parser').NameResult} */ ( + node + ).value = ret.replace(/(?:\.|<>|\.<>|\[\])$/v, ''); + + // For bare pseudo-types like `<>` + if (!ret) { + /** @type {import('jsdoc-type-pratt-parser').NameResult} */ ( + node + ).value = typeNodeName; + } +}; + +/** + * @param {boolean} [upperCase] + * @returns {string} + */ +const getMessage = (upperCase) => { + return 'Use object shorthand or index signatures instead of ' + + '`' + (upperCase ? 'O' : 'o') + 'bject`, e.g., `{[key: string]: string}`'; +}; + +/** + * @type {{ + * message: string, + * replacement: false + * }} + */ +const info = { + message: getMessage(), + replacement: false, +}; + +/** + * @type {{ + * message: string, + * replacement: false + * }} + */ +const infoUC = { + message: getMessage(true), + replacement: false, +}; + +/** + * @param {{ + * checkNativeTypes: import('./rules/checkTypes.js').CheckNativeTypes|null + * overrideSettings?: null, + * description?: string, + * schema?: import('eslint').Rule.RuleMetaData['schema'], + * url?: string, + * }} cfg + * @returns {import('@eslint/core').RuleDefinition< + * import('@eslint/core').RuleDefinitionTypeOptions + * >} + */ +export const buildRejectOrPreferRuleDefinition = ({ + checkNativeTypes = null, + description = 'Reports invalid types.', + overrideSettings = null, + schema = [], + url = 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/check-types.md#repos-sticky-header', +}) => { + return iterateJsdoc( + ({ + context, + jsdocNode, + report, + settings, + sourceCode, + utils, + }) => { + const jsdocTagsWithPossibleType = utils.filterTags((tag) => { + return Boolean(utils.tagMightHaveTypePosition(tag.tag)); + }); + + const + /** + * @type {{ + * preferredTypes: import('./iterateJsdoc.js').PreferredTypes, + * structuredTags: import('./iterateJsdoc.js').StructuredTags, + * mode: import('./jsdocUtils.js').ParserMode + * }} + */ + { + mode = settings.mode, + preferredTypes: preferredTypesOriginal, + structuredTags = {}, + } = overrideSettings ?? settings; + + const injectObjectPreferredTypes = !('Object' in preferredTypesOriginal || + 'object' in preferredTypesOriginal || + 'object.<>' in preferredTypesOriginal || + 'Object.<>' in preferredTypesOriginal || + 'object<>' in preferredTypesOriginal); + + /** @type {import('./iterateJsdoc.js').PreferredTypes} */ + const typeToInject = mode === 'typescript' ? + { + Object: 'object', + 'object.<>': info, + 'Object.<>': infoUC, + 'object<>': info, + 'Object<>': infoUC, + } : + { + Object: 'object', + 'object.<>': 'Object<>', + 'Object.<>': 'Object<>', + 'object<>': 'Object<>', + }; + + /** @type {import('./iterateJsdoc.js').PreferredTypes} */ + const preferredTypes = { + ...injectObjectPreferredTypes ? + typeToInject : + {}, + ...preferredTypesOriginal, + }; + + const + /** + * @type {{ + * noDefaults: boolean, + * unifyParentAndChildTypeChecks: boolean, + * exemptTagContexts: ({ + * tag: string, + * types: true|string[] + * })[] + * }} + */ { + exemptTagContexts = [], + noDefaults, + unifyParentAndChildTypeChecks, + } = context.options[0] || {}; + + /** + * Gets information about the preferred type: whether there is a matching + * preferred type, what the type is, and whether it is a match to a generic. + * @param {string} _type Not currently in use + * @param {string} typeNodeName + * @param {import('jsdoc-type-pratt-parser').NonRootResult|undefined} parentNode + * @param {string|undefined} property + * @returns {[hasMatchingPreferredType: boolean, typeName: string, isGenericMatch: boolean]} + */ + const getPreferredTypeInfo = (_type, typeNodeName, parentNode, property) => { + let hasMatchingPreferredType = false; + let isGenericMatch = false; + let typeName = typeNodeName; + + const isNameOfGeneric = parentNode !== undefined && parentNode.type === 'JsdocTypeGeneric' && property === 'left'; + + const brackets = /** @type {import('jsdoc-type-pratt-parser').GenericResult} */ ( + parentNode + )?.meta?.brackets; + const dot = /** @type {import('jsdoc-type-pratt-parser').GenericResult} */ ( + parentNode + )?.meta?.dot; + + if (brackets === 'angle') { + const checkPostFixes = dot ? [ + '.', '.<>', + ] : [ + '<>', + ]; + isGenericMatch = checkPostFixes.some((checkPostFix) => { + const preferredType = preferredTypes?.[typeNodeName + checkPostFix]; + + // Does `unifyParentAndChildTypeChecks` need to be checked here? + if ( + (unifyParentAndChildTypeChecks || isNameOfGeneric || + /* c8 ignore next 2 -- If checking `unifyParentAndChildTypeChecks` */ + (typeof preferredType === 'object' && + preferredType?.unifyParentAndChildTypeChecks) + ) && + preferredType !== undefined + ) { + typeName += checkPostFix; + + return true; + } + + return false; + }); + } + + if ( + !isGenericMatch && property && + /** @type {import('jsdoc-type-pratt-parser').NonRootResult} */ ( + parentNode + ).type === 'JsdocTypeGeneric' + ) { + const checkPostFixes = dot ? [ + '.', '.<>', + ] : [ + brackets === 'angle' ? '<>' : '[]', + ]; + + isGenericMatch = checkPostFixes.some((checkPostFix) => { + const preferredType = preferredTypes?.[checkPostFix]; + if ( + // Does `unifyParentAndChildTypeChecks` need to be checked here? + (unifyParentAndChildTypeChecks || isNameOfGeneric || + /* c8 ignore next 2 -- If checking `unifyParentAndChildTypeChecks` */ + (typeof preferredType === 'object' && + preferredType?.unifyParentAndChildTypeChecks)) && + preferredType !== undefined + ) { + typeName = checkPostFix; + + return true; + } + + return false; + }); + } + + const prefType = preferredTypes?.[typeNodeName]; + const directNameMatch = prefType !== undefined && + !Object.values(preferredTypes).includes(typeNodeName); + const specificUnify = typeof prefType === 'object' && + prefType?.unifyParentAndChildTypeChecks; + const unifiedSyntaxParentMatch = property && directNameMatch && (unifyParentAndChildTypeChecks || specificUnify); + isGenericMatch = isGenericMatch || Boolean(unifiedSyntaxParentMatch); + + hasMatchingPreferredType = isGenericMatch || + directNameMatch && !property; + + return [ + hasMatchingPreferredType, typeName, isGenericMatch, + ]; + }; + + /** + * Collect invalid type info. + * @param {string} type + * @param {string} value + * @param {string} tagName + * @param {string} nameInTag + * @param {number} idx + * @param {string|undefined} property + * @param {import('jsdoc-type-pratt-parser').NonRootResult} node + * @param {import('jsdoc-type-pratt-parser').NonRootResult|undefined} parentNode + * @param {(string|false|undefined)[][]} invalidTypes + * @returns {void} + */ + const getInvalidTypes = (type, value, tagName, nameInTag, idx, property, node, parentNode, invalidTypes) => { + let typeNodeName = type === 'JsdocTypeAny' ? '*' : value; + + const [ + hasMatchingPreferredType, + typeName, + isGenericMatch, + ] = getPreferredTypeInfo(type, typeNodeName, parentNode, property); + + let preferred; + let types; + if (hasMatchingPreferredType) { + const preferredSetting = preferredTypes[typeName]; + typeNodeName = typeName === '[]' ? typeName : typeNodeName; + + if (!preferredSetting) { + invalidTypes.push([ + typeNodeName, + ]); + } else if (typeof preferredSetting === 'string') { + preferred = preferredSetting; + invalidTypes.push([ + typeNodeName, preferred, + ]); + } else if (preferredSetting && typeof preferredSetting === 'object') { + const nextItem = preferredSetting.skipRootChecking && jsdocTagsWithPossibleType[idx + 1]; + + if (!nextItem || !nextItem.name.startsWith(`${nameInTag}.`)) { + preferred = preferredSetting.replacement; + invalidTypes.push([ + typeNodeName, + preferred, + preferredSetting.message, + ]); + } + } else { + utils.reportSettings( + 'Invalid `settings.jsdoc.preferredTypes`. Values must be falsy, a string, or an object.', + ); + + return; + } + } else if (Object.entries(structuredTags).some(([ + tag, + { + type: typs, + }, + ]) => { + types = typs; + + return tag === tagName && + Array.isArray(types) && + !types.includes(typeNodeName); + })) { + invalidTypes.push([ + typeNodeName, types, + ]); + } else if (checkNativeTypes && !noDefaults && type === 'JsdocTypeName') { + preferred = checkNativeTypes( + preferredTypes, typeNodeName, preferred, parentNode, invalidTypes, + ); + } + + // For fixer + if (preferred) { + adjustNames(type, preferred, isGenericMatch, typeNodeName, node, parentNode); + } + }; + + for (const [ + idx, + jsdocTag, + ] of jsdocTagsWithPossibleType.entries()) { + /** @type {(string|false|undefined)[][]} */ + const invalidTypes = []; + let typeAst; + + try { + typeAst = mode === 'permissive' ? tryParse(jsdocTag.type) : parse(jsdocTag.type, mode); + } catch { + continue; + } + + const { + name: nameInTag, + tag: tagName, + } = jsdocTag; + + traverse(typeAst, (node, parentNode, property) => { + const { + type, + value, + } = + /** + * @type {import('jsdoc-type-pratt-parser').NameResult} + */ (node); + if (![ + 'JsdocTypeAny', 'JsdocTypeName', + ].includes(type)) { + return; + } + + getInvalidTypes(type, value, tagName, nameInTag, idx, property, node, parentNode, invalidTypes); + }); + + if (invalidTypes.length) { + const fixedType = stringify(typeAst); + + /** + * @type {import('eslint').Rule.ReportFixer} + */ + const fix = (fixer) => { + return fixer.replaceText( + jsdocNode, + sourceCode.getText(jsdocNode).replace( + `{${jsdocTag.type}}`, + `{${fixedType}}`, + ), + ); + }; + + for (const [ + badType, + preferredType = '', + msg, + ] of invalidTypes) { + const tagValue = jsdocTag.name ? ` "${jsdocTag.name}"` : ''; + if (exemptTagContexts.some(({ + tag, + types, + }) => { + return tag === tagName && + (types === true || types.includes(jsdocTag.type)); + })) { + continue; + } + + report( + msg || + `Invalid JSDoc @${tagName}${tagValue} type "${badType}"` + + (preferredType ? '; ' : '.') + + (preferredType ? `prefer: ${JSON.stringify(preferredType)}.` : ''), + preferredType ? fix : null, + jsdocTag, + msg ? { + tagName, + tagValue, + } : undefined, + ); + } + } + } + }, + { + iterateAllJsdocs: true, + meta: { + docs: { + description, + url, + }, + fixable: 'code', + schema, + type: 'suggestion', + }, + }, + ); +}; diff --git a/src/index-cjs.js b/src/index-cjs.js index 55c50ebdc..b49a385d1 100644 --- a/src/index-cjs.js +++ b/src/index-cjs.js @@ -1,6 +1,9 @@ import { buildForbidRuleDefinition, } from './buildForbidRuleDefinition.js'; +// import { +// buildRejectOrPreferRuleDefinition, +// } from './buildRejectOrPreferRuleDefinition.js'; import { getJsdocProcessorPlugin, } from './getJsdocProcessorPlugin.js'; @@ -107,6 +110,20 @@ index.rules = { 'no-restricted-syntax': noRestrictedSyntax, 'no-types': noTypes, 'no-undefined-types': noUndefinedTypes, + // 'reject-any-type': buildRejectOrPreferRuleDefinition({ + // contexts: [ + // { + // unifyParentAndChildTypeChecks: true, + // }, + // ], + // }), + // 'reject-function-type': buildRejectOrPreferRuleDefinition({ + // contexts: [ + // { + // unifyParentAndChildTypeChecks: true, + // }, + // ], + // }), 'require-asterisk-prefix': requireAsteriskPrefix, 'require-description': requireDescription, 'require-description-complete-sentence': requireDescriptionCompleteSentence, diff --git a/src/index.js b/src/index.js index dcd75876d..5f3d0d456 100644 --- a/src/index.js +++ b/src/index.js @@ -7,6 +7,9 @@ import { import { buildForbidRuleDefinition, } from './buildForbidRuleDefinition.js'; +// import { +// buildRejectOrPreferRuleDefinition, +// } from './buildRejectOrPreferRuleDefinition.js'; import { getJsdocProcessorPlugin, } from './getJsdocProcessorPlugin.js'; @@ -113,6 +116,20 @@ index.rules = { 'no-restricted-syntax': noRestrictedSyntax, 'no-types': noTypes, 'no-undefined-types': noUndefinedTypes, + // 'reject-any-type': buildRejectOrPreferRuleDefinition({ + // contexts: [ + // { + // unifyParentAndChildTypeChecks: true, + // }, + // ], + // }), + // 'reject-function-type': buildRejectOrPreferRuleDefinition({ + // contexts: [ + // { + // unifyParentAndChildTypeChecks: true, + // }, + // ], + // }), 'require-asterisk-prefix': requireAsteriskPrefix, 'require-description': requireDescription, 'require-description-complete-sentence': requireDescriptionCompleteSentence, diff --git a/src/rules/checkTypes.js b/src/rules/checkTypes.js index 5294a49be..652069b24 100644 --- a/src/rules/checkTypes.js +++ b/src/rules/checkTypes.js @@ -1,10 +1,6 @@ -import iterateJsdoc from '../iterateJsdoc.js'; import { - parse, - stringify, - traverse, - tryParse, -} from '@es-joy/jsdoccomment'; + buildRejectOrPreferRuleDefinition, +} from '../buildRejectOrPreferRuleDefinition.js'; const strictNativeTypes = [ 'undefined', @@ -22,535 +18,110 @@ const strictNativeTypes = [ ]; /** - * Adjusts the parent type node `meta` for generic matches (or type node - * `type` for `JsdocTypeAny`) and sets the type node `value`. - * @param {string} type The actual type - * @param {string} preferred The preferred type - * @param {boolean} isGenericMatch + * @callback CheckNativeTypes + * Iterates strict types to see if any should be added to `invalidTypes` (and + * the the relevant strict type returned as the new preferred type). + * @param {import('../iterateJsdoc.js').PreferredTypes} preferredTypes * @param {string} typeNodeName - * @param {import('jsdoc-type-pratt-parser').NonRootResult} node + * @param {string|undefined} preferred * @param {import('jsdoc-type-pratt-parser').NonRootResult|undefined} parentNode - * @returns {void} + * @param {(string|false|undefined)[][]} invalidTypes + * @returns {string|undefined} The `preferred` type string, optionally changed */ -const adjustNames = (type, preferred, isGenericMatch, typeNodeName, node, parentNode) => { - let ret = preferred; - if (isGenericMatch) { - const parentMeta = /** @type {import('jsdoc-type-pratt-parser').GenericResult} */ ( - parentNode - ).meta; - if (preferred === '[]') { - parentMeta.brackets = 'square'; - parentMeta.dot = false; - ret = 'Array'; - } else { - const dotBracketEnd = preferred.match(/\.(?:<>)?$/v); - if (dotBracketEnd) { - parentMeta.brackets = 'angle'; - parentMeta.dot = true; - ret = preferred.slice(0, -dotBracketEnd[0].length); - } else { - const bracketEnd = preferred.endsWith('<>'); - if (bracketEnd) { - parentMeta.brackets = 'angle'; - parentMeta.dot = false; - ret = preferred.slice(0, -2); - } else if ( - parentMeta?.brackets === 'square' && - (typeNodeName === '[]' || typeNodeName === 'Array') - ) { - parentMeta.brackets = 'angle'; - parentMeta.dot = false; - } - } - } - } else if (type === 'JsdocTypeAny') { - node.type = 'JsdocTypeName'; - } - - /** @type {import('jsdoc-type-pratt-parser').NameResult} */ ( - node - ).value = ret.replace(/(?:\.|<>|\.<>|\[\])$/v, ''); - - // For bare pseudo-types like `<>` - if (!ret) { - /** @type {import('jsdoc-type-pratt-parser').NameResult} */ ( - node - ).value = typeNodeName; - } -}; - -/** - * @param {boolean} [upperCase] - * @returns {string} - */ -const getMessage = (upperCase) => { - return 'Use object shorthand or index signatures instead of ' + - '`' + (upperCase ? 'O' : 'o') + 'bject`, e.g., `{[key: string]: string}`'; -}; - -/** - * @type {{ - * message: string, - * replacement: false - * }} - */ -const info = { - message: getMessage(), - replacement: false, -}; - -/** - * @type {{ - * message: string, - * replacement: false - * }} - */ -const infoUC = { - message: getMessage(true), - replacement: false, -}; - -export default iterateJsdoc(({ - context, - jsdocNode, - report, - settings, - sourceCode, - utils, -}) => { - const jsdocTagsWithPossibleType = utils.filterTags((tag) => { - return Boolean(utils.tagMightHaveTypePosition(tag.tag)); - }); - - const - /** - * @type {{ - * preferredTypes: import('../iterateJsdoc.js').PreferredTypes, - * structuredTags: import('../iterateJsdoc.js').StructuredTags, - * mode: import('../jsdocUtils.js').ParserMode - * }} - */ - { - mode, - preferredTypes: preferredTypesOriginal, - structuredTags, - } = settings; - - const injectObjectPreferredTypes = !('Object' in preferredTypesOriginal || - 'object' in preferredTypesOriginal || - 'object.<>' in preferredTypesOriginal || - 'Object.<>' in preferredTypesOriginal || - 'object<>' in preferredTypesOriginal); - - /** @type {import('../iterateJsdoc.js').PreferredTypes} */ - const typeToInject = mode === 'typescript' ? - { - Object: 'object', - 'object.<>': info, - 'Object.<>': infoUC, - 'object<>': info, - 'Object<>': infoUC, - } : - { - Object: 'object', - 'object.<>': 'Object<>', - 'Object.<>': 'Object<>', - 'object<>': 'Object<>', - }; - - /** @type {import('../iterateJsdoc.js').PreferredTypes} */ - const preferredTypes = { - ...injectObjectPreferredTypes ? - typeToInject : - {}, - ...preferredTypesOriginal, - }; - - const - /** - * @type {{ - * noDefaults: boolean, - * unifyParentAndChildTypeChecks: boolean, - * exemptTagContexts: ({ - * tag: string, - * types: true|string[] - * })[] - * }} - */ { - exemptTagContexts = [], - noDefaults, - unifyParentAndChildTypeChecks, - } = context.options[0] || {}; - - /** - * Gets information about the preferred type: whether there is a matching - * preferred type, what the type is, and whether it is a match to a generic. - * @param {string} _type Not currently in use - * @param {string} typeNodeName - * @param {import('jsdoc-type-pratt-parser').NonRootResult|undefined} parentNode - * @param {string|undefined} property - * @returns {[hasMatchingPreferredType: boolean, typeName: string, isGenericMatch: boolean]} - */ - const getPreferredTypeInfo = (_type, typeNodeName, parentNode, property) => { - let hasMatchingPreferredType = false; - let isGenericMatch = false; - let typeName = typeNodeName; - - const isNameOfGeneric = parentNode !== undefined && parentNode.type === 'JsdocTypeGeneric' && property === 'left'; - - const brackets = /** @type {import('jsdoc-type-pratt-parser').GenericResult} */ ( - parentNode - )?.meta?.brackets; - const dot = /** @type {import('jsdoc-type-pratt-parser').GenericResult} */ ( - parentNode - )?.meta?.dot; - - if (brackets === 'angle') { - const checkPostFixes = dot ? [ - '.', '.<>', - ] : [ - '<>', - ]; - isGenericMatch = checkPostFixes.some((checkPostFix) => { - const preferredType = preferredTypes?.[typeNodeName + checkPostFix]; - - // Does `unifyParentAndChildTypeChecks` need to be checked here? - if ( - (unifyParentAndChildTypeChecks || isNameOfGeneric || - /* c8 ignore next 2 -- If checking `unifyParentAndChildTypeChecks` */ - (typeof preferredType === 'object' && - preferredType?.unifyParentAndChildTypeChecks) - ) && - preferredType !== undefined - ) { - typeName += checkPostFix; - - return true; - } - - return false; - }); - } +/** @type {CheckNativeTypes} */ +const checkNativeTypes = (preferredTypes, typeNodeName, preferred, parentNode, invalidTypes) => { + let changedPreferred = preferred; + for (const strictNativeType of strictNativeTypes) { if ( - !isGenericMatch && property && - /** @type {import('jsdoc-type-pratt-parser').NonRootResult} */ ( - parentNode - ).type === 'JsdocTypeGeneric' - ) { - const checkPostFixes = dot ? [ - '.', '.<>', - ] : [ - brackets === 'angle' ? '<>' : '[]', - ]; - - isGenericMatch = checkPostFixes.some((checkPostFix) => { - const preferredType = preferredTypes?.[checkPostFix]; - if ( - // Does `unifyParentAndChildTypeChecks` need to be checked here? - (unifyParentAndChildTypeChecks || isNameOfGeneric || - /* c8 ignore next 2 -- If checking `unifyParentAndChildTypeChecks` */ - (typeof preferredType === 'object' && - preferredType?.unifyParentAndChildTypeChecks)) && - preferredType !== undefined - ) { - typeName = checkPostFix; - - return true; - } - - return false; - }); - } - - const prefType = preferredTypes?.[typeNodeName]; - const directNameMatch = prefType !== undefined && - !Object.values(preferredTypes).includes(typeNodeName); - const specificUnify = typeof prefType === 'object' && - prefType?.unifyParentAndChildTypeChecks; - const unifiedSyntaxParentMatch = property && directNameMatch && (unifyParentAndChildTypeChecks || specificUnify); - isGenericMatch = isGenericMatch || Boolean(unifiedSyntaxParentMatch); - - hasMatchingPreferredType = isGenericMatch || - directNameMatch && !property; - - return [ - hasMatchingPreferredType, typeName, isGenericMatch, - ]; - }; - - /** - * Iterates strict types to see if any should be added to `invalidTypes` (and - * the the relevant strict type returned as the new preferred type). - * @param {string} typeNodeName - * @param {string|undefined} preferred - * @param {import('jsdoc-type-pratt-parser').NonRootResult|undefined} parentNode - * @param {(string|false|undefined)[][]} invalidTypes - * @returns {string|undefined} The `preferred` type string, optionally changed - */ - const checkNativeTypes = (typeNodeName, preferred, parentNode, invalidTypes) => { - let changedPreferred = preferred; - for (const strictNativeType of strictNativeTypes) { - if ( - strictNativeType === 'object' && + strictNativeType === 'object' && + ( + // This is not set to remap with exact type match (e.g., + // `object: 'Object'`), so can ignore (including if circular) + !preferredTypes?.[typeNodeName] || + // Although present on `preferredTypes` for remapping, this is a + // parent object without a parent match (and not + // `unifyParentAndChildTypeChecks`) and we don't want + // `object<>` given TypeScript issue https://github.com/microsoft/TypeScript/issues/20555 + /** + * @type {import('jsdoc-type-pratt-parser').GenericResult} + */ ( - // This is not set to remap with exact type match (e.g., - // `object: 'Object'`), so can ignore (including if circular) - !preferredTypes?.[typeNodeName] || - // Although present on `preferredTypes` for remapping, this is a - // parent object without a parent match (and not - // `unifyParentAndChildTypeChecks`) and we don't want - // `object<>` given TypeScript issue https://github.com/microsoft/TypeScript/issues/20555 - /** - * @type {import('jsdoc-type-pratt-parser').GenericResult} - */ + parentNode + )?.elements?.length && ( + /** + * @type {import('jsdoc-type-pratt-parser').GenericResult} + */ ( parentNode - )?.elements?.length && ( + )?.left?.type === 'JsdocTypeName' && /** * @type {import('jsdoc-type-pratt-parser').GenericResult} */ - ( - parentNode - )?.left?.type === 'JsdocTypeName' && - /** - * @type {import('jsdoc-type-pratt-parser').GenericResult} - */ - (parentNode)?.left?.value === 'Object' - ) + (parentNode)?.left?.value === 'Object' ) - ) { - continue; - } - - if (strictNativeType !== typeNodeName && - strictNativeType.toLowerCase() === typeNodeName.toLowerCase() && - - // Don't report if user has own map for a strict native type - (!preferredTypes || preferredTypes?.[strictNativeType] === undefined) - ) { - changedPreferred = strictNativeType; - invalidTypes.push([ - typeNodeName, changedPreferred, - ]); - break; - } + ) + ) { + continue; } - return changedPreferred; - }; - - /** - * Collect invalid type info. - * @param {string} type - * @param {string} value - * @param {string} tagName - * @param {string} nameInTag - * @param {number} idx - * @param {string|undefined} property - * @param {import('jsdoc-type-pratt-parser').NonRootResult} node - * @param {import('jsdoc-type-pratt-parser').NonRootResult|undefined} parentNode - * @param {(string|false|undefined)[][]} invalidTypes - * @returns {void} - */ - const getInvalidTypes = (type, value, tagName, nameInTag, idx, property, node, parentNode, invalidTypes) => { - let typeNodeName = type === 'JsdocTypeAny' ? '*' : value; + if (strictNativeType !== typeNodeName && + strictNativeType.toLowerCase() === typeNodeName.toLowerCase() && - const [ - hasMatchingPreferredType, - typeName, - isGenericMatch, - ] = getPreferredTypeInfo(type, typeNodeName, parentNode, property); - - let preferred; - let types; - if (hasMatchingPreferredType) { - const preferredSetting = preferredTypes[typeName]; - typeNodeName = typeName === '[]' ? typeName : typeNodeName; - - if (!preferredSetting) { - invalidTypes.push([ - typeNodeName, - ]); - } else if (typeof preferredSetting === 'string') { - preferred = preferredSetting; - invalidTypes.push([ - typeNodeName, preferred, - ]); - } else if (preferredSetting && typeof preferredSetting === 'object') { - const nextItem = preferredSetting.skipRootChecking && jsdocTagsWithPossibleType[idx + 1]; - - if (!nextItem || !nextItem.name.startsWith(`${nameInTag}.`)) { - preferred = preferredSetting.replacement; - invalidTypes.push([ - typeNodeName, - preferred, - preferredSetting.message, - ]); - } - } else { - utils.reportSettings( - 'Invalid `settings.jsdoc.preferredTypes`. Values must be falsy, a string, or an object.', - ); - - return; - } - } else if (Object.entries(structuredTags).some(([ - tag, - { - type: typs, - }, - ]) => { - types = typs; - - return tag === tagName && - Array.isArray(types) && - !types.includes(typeNodeName); - })) { + // Don't report if user has own map for a strict native type + (!preferredTypes || preferredTypes?.[strictNativeType] === undefined) + ) { + changedPreferred = strictNativeType; invalidTypes.push([ - typeNodeName, types, + typeNodeName, changedPreferred, ]); - } else if (!noDefaults && type === 'JsdocTypeName') { - preferred = checkNativeTypes(typeNodeName, preferred, parentNode, invalidTypes); - } - - // For fixer - if (preferred) { - adjustNames(type, preferred, isGenericMatch, typeNodeName, node, parentNode); + break; } - }; - - for (const [ - idx, - jsdocTag, - ] of jsdocTagsWithPossibleType.entries()) { - /** @type {(string|false|undefined)[][]} */ - const invalidTypes = []; - let typeAst; - - try { - typeAst = mode === 'permissive' ? tryParse(jsdocTag.type) : parse(jsdocTag.type, mode); - } catch { - continue; - } - - const { - name: nameInTag, - tag: tagName, - } = jsdocTag; - - traverse(typeAst, (node, parentNode, property) => { - const { - type, - value, - } = - /** - * @type {import('jsdoc-type-pratt-parser').NameResult} - */ (node); - if (![ - 'JsdocTypeAny', 'JsdocTypeName', - ].includes(type)) { - return; - } - - getInvalidTypes(type, value, tagName, nameInTag, idx, property, node, parentNode, invalidTypes); - }); - - if (invalidTypes.length) { - const fixedType = stringify(typeAst); - - /** - * @type {import('eslint').Rule.ReportFixer} - */ - const fix = (fixer) => { - return fixer.replaceText( - jsdocNode, - sourceCode.getText(jsdocNode).replace( - `{${jsdocTag.type}}`, - `{${fixedType}}`, - ), - ); - }; + } - for (const [ - badType, - preferredType = '', - msg, - ] of invalidTypes) { - const tagValue = jsdocTag.name ? ` "${jsdocTag.name}"` : ''; - if (exemptTagContexts.some(({ - tag, - types, - }) => { - return tag === tagName && - (types === true || types.includes(jsdocTag.type)); - })) { - continue; - } + return changedPreferred; +}; - report( - msg || - `Invalid JSDoc @${tagName}${tagValue} type "${badType}"` + - (preferredType ? '; ' : '.') + - (preferredType ? `prefer: ${JSON.stringify(preferredType)}.` : ''), - preferredType ? fix : null, - jsdocTag, - msg ? { - tagName, - tagValue, - } : undefined, - ); - } - } - } -}, { - iterateAllJsdocs: true, - meta: { - docs: { - description: 'Reports invalid types.', - url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/check-types.md#repos-sticky-header', - }, - fixable: 'code', - schema: [ - { - additionalProperties: false, - properties: { - exemptTagContexts: { - items: { - additionalProperties: false, - properties: { - tag: { - type: 'string', - }, - types: { - oneOf: [ - { - type: 'boolean', - }, - { - items: { - type: 'string', - }, - type: 'array', +export default buildRejectOrPreferRuleDefinition({ + checkNativeTypes, + schema: [ + { + additionalProperties: false, + properties: { + exemptTagContexts: { + items: { + additionalProperties: false, + properties: { + tag: { + type: 'string', + }, + types: { + oneOf: [ + { + type: 'boolean', + }, + { + items: { + type: 'string', }, - ], - }, + type: 'array', + }, + ], }, - type: 'object', }, - type: 'array', - }, - noDefaults: { - type: 'boolean', - }, - unifyParentAndChildTypeChecks: { - description: '@deprecated Use the `preferredTypes[preferredType]` setting of the same name instead', - type: 'boolean', + type: 'object', }, + type: 'array', + }, + noDefaults: { + type: 'boolean', + }, + unifyParentAndChildTypeChecks: { + description: '@deprecated Use the `preferredTypes[preferredType]` setting of the same name instead', + type: 'boolean', }, - type: 'object', }, - ], - type: 'suggestion', - }, + type: 'object', + }, + ], }); From ae4e95d5d8a8029fa8b4ba8c12e0e635a6426f5e Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 16 Sep 2025 12:16:40 +0800 Subject: [PATCH 3/3] feat: add `reject-any-type` and `reject-function-type` rules and `extraRuleDefinitions.preferTypes` option BREAKING CHANGE: The new rules are added to `recommended` configs --- .README/advanced.md | 53 ++++++ .README/rules/no-multi-asterisks.md | 2 +- .README/rules/reject-any-type.md | 21 +++ .README/rules/reject-function-type.md | 21 +++ docs/advanced.md | 56 ++++++ docs/rules/no-multi-asterisks.md | 2 +- docs/rules/reject-any-type.md | 51 ++++++ docs/rules/reject-function-type.md | 51 ++++++ src/buildRejectOrPreferRuleDefinition.js | 32 ++-- src/index-cjs.js | 49 +++-- src/index-esm.js | 37 +++- src/index.js | 83 +++++++-- src/rules.d.ts | 6 + test/index.js | 192 ++++++++++++++++++++ test/rules/assertions/rejectAnyType.js | 42 +++++ test/rules/assertions/rejectFunctionType.js | 42 +++++ test/rules/ruleNames.json | 2 + 17 files changed, 691 insertions(+), 51 deletions(-) create mode 100644 .README/rules/reject-any-type.md create mode 100644 .README/rules/reject-function-type.md create mode 100644 docs/rules/reject-any-type.md create mode 100644 docs/rules/reject-function-type.md create mode 100644 test/rules/assertions/rejectAnyType.js create mode 100644 test/rules/assertions/rejectFunctionType.js diff --git a/.README/advanced.md b/.README/advanced.md index 3eef463c8..211d14be8 100644 --- a/.README/advanced.md +++ b/.README/advanced.md @@ -164,3 +164,56 @@ export const a = (abc, def) => { }; /* eslint-enable jsdoc/forbid-Any */ ``` + +#### Preferring type structures + +When the structures in question are types, a disadvantage of the previous approach +is that one cannot perform replacements nor can one distinguish between parent and +child types for a generic. + +If targeting a type structure, you can use `extraRuleDefinitions.preferTypes`. + +While one can get this same behavior using the `preferredTypes` setting, the +advantage of creating a rule definition is that handling is distributed not to +a single rule (`jsdoc/check-types`), but to an individual rule for each preferred +type (which can then be selectively enabled and disabled). + +```js +import {jsdoc} from 'eslint-plugin-jsdoc'; + +export default [ + jsdoc({ + config: 'flat/recommended', + extraRuleDefinitions: { + preferTypes: { + // This key will be used in the rule name + promise: { + description: 'This rule disallows Promises without a generic type', + overrideSettings: { + // Uses the same keys are are available on the `preferredTypes` settings + + // This key will indicate the type node name to find + Promise: { + // This is the specific error message if reported + message: 'Add a generic type for this Promise.', + + // This can instead be a string replacement if an auto-replacement + // is desired + replacement: false, + + // If `true`, this will check in both parent and child positions + unifyParentAndChildTypeChecks: false, + }, + }, + url: 'https://example.com/Promise-rule.md', + }, + }, + }, + rules: { + // Don't forget to enable the above-defined rules + 'jsdoc/prefer-type-promise': [ + 'error', + ], + } + }) +``` diff --git a/.README/rules/no-multi-asterisks.md b/.README/rules/no-multi-asterisks.md index 1ba882026..08e1dd607 100644 --- a/.README/rules/no-multi-asterisks.md +++ b/.README/rules/no-multi-asterisks.md @@ -51,7 +51,7 @@ Prevent the likes of this: ||| |---|---| |Context|everywhere| -|Tags|(any)| +|Tags|(Any)| |Recommended|true| |Settings|| |Options|`allowWhitespace`, `preventAtEnd`, `preventAtMiddleLines`| diff --git a/.README/rules/reject-any-type.md b/.README/rules/reject-any-type.md new file mode 100644 index 000000000..1aaadf625 --- /dev/null +++ b/.README/rules/reject-any-type.md @@ -0,0 +1,21 @@ +# `reject-any-type` + +Reports use of `any` (or `*`) type within JSDoc tag types. + +||| +|---|---| +|Context|everywhere| +|Tags|`augments`, `class`, `constant`, `enum`, `implements`, `member`, `module`, `namespace`, `param`, `property`, `returns`, `throws`, `type`, `typedef`, `yields`| +|Aliases|`constructor`, `const`, `extends`, `var`, `arg`, `argument`, `prop`, `return`, `exception`, `yield`| +|Closure-only|`package`, `private`, `protected`, `public`, `static`| +|Recommended|true| +|Settings|`mode`| +|Options|| + +## Failing examples + + + +## Passing examples + + diff --git a/.README/rules/reject-function-type.md b/.README/rules/reject-function-type.md new file mode 100644 index 000000000..3e6533020 --- /dev/null +++ b/.README/rules/reject-function-type.md @@ -0,0 +1,21 @@ +# `reject-function-type` + +Reports use of `Function` type within JSDoc tag types. + +||| +|---|---| +|Context|everywhere| +|Tags|`augments`, `class`, `constant`, `enum`, `implements`, `member`, `module`, `namespace`, `param`, `property`, `returns`, `throws`, `type`, `typedef`, `yields`| +|Aliases|`constructor`, `const`, `extends`, `var`, `arg`, `argument`, `prop`, `return`, `exception`, `yield`| +|Closure-only|`package`, `private`, `protected`, `public`, `static`| +|Recommended|true| +|Settings|`mode`| +|Options|| + +## Failing examples + + + +## Passing examples + + diff --git a/docs/advanced.md b/docs/advanced.md index 688157bbf..f316c44ec 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -8,6 +8,7 @@ * [Uses/Tips for AST](#user-content-advanced-ast-and-selectors-uses-tips-for-ast) * [Creating your own rules](#user-content-advanced-creating-your-own-rules) * [Forbidding structures](#user-content-advanced-creating-your-own-rules-forbidding-structures) + * [Preferring type structures](#user-content-advanced-creating-your-own-rules-preferring-type-structures) @@ -184,3 +185,58 @@ export const a = (abc, def) => { }; /* eslint-enable jsdoc/forbid-Any */ ``` + + + +#### Preferring type structures + +When the structures in question are types, a disadvantage of the previous approach +is that one cannot perform replacements nor can one distinguish between parent and +child types for a generic. + +If targeting a type structure, you can use `extraRuleDefinitions.preferTypes`. + +While one can get this same behavior using the `preferredTypes` setting, the +advantage of creating a rule definition is that handling is distributed not to +a single rule (`jsdoc/check-types`), but to an individual rule for each preferred +type (which can then be selectively enabled and disabled). + +```js +import {jsdoc} from 'eslint-plugin-jsdoc'; + +export default [ + jsdoc({ + config: 'flat/recommended', + extraRuleDefinitions: { + preferTypes: { + // This key will be used in the rule name + promise: { + description: 'This rule disallows Promises without a generic type', + overrideSettings: { + // Uses the same keys are are available on the `preferredTypes` settings + + // This key will indicate the type node name to find + Promise: { + // This is the specific error message if reported + message: 'Add a generic type for this Promise.', + + // This can instead be a string replacement if an auto-replacement + // is desired + replacement: false, + + // If `true`, this will check in both parent and child positions + unifyParentAndChildTypeChecks: false, + }, + }, + url: 'https://example.com/Promise-rule.md', + }, + }, + }, + rules: { + // Don't forget to enable the above-defined rules + 'jsdoc/prefer-type-promise': [ + 'error', + ], + } + }) +``` diff --git a/docs/rules/no-multi-asterisks.md b/docs/rules/no-multi-asterisks.md index 88f2e71ba..6301896b8 100644 --- a/docs/rules/no-multi-asterisks.md +++ b/docs/rules/no-multi-asterisks.md @@ -65,7 +65,7 @@ Prevent the likes of this: ||| |---|---| |Context|everywhere| -|Tags|(any)| +|Tags|(Any)| |Recommended|true| |Settings|| |Options|`allowWhitespace`, `preventAtEnd`, `preventAtMiddleLines`| diff --git a/docs/rules/reject-any-type.md b/docs/rules/reject-any-type.md new file mode 100644 index 000000000..35805f8b8 --- /dev/null +++ b/docs/rules/reject-any-type.md @@ -0,0 +1,51 @@ + + +# reject-any-type + +Reports use of `any` (or `*`) type within JSDoc tag types. + +||| +|---|---| +|Context|everywhere| +|Tags|`augments`, `class`, `constant`, `enum`, `implements`, `member`, `module`, `namespace`, `param`, `property`, `returns`, `throws`, `type`, `typedef`, `yields`| +|Aliases|`constructor`, `const`, `extends`, `var`, `arg`, `argument`, `prop`, `return`, `exception`, `yield`| +|Closure-only|`package`, `private`, `protected`, `public`, `static`| +|Recommended|true| +|Settings|`mode`| +|Options|| + + + +## Failing examples + +The following patterns are considered problems: + +````ts +/** + * @param {any} abc + */ +function quux () {} +// Message: Prefer a more specific type to `any` + +/** + * @param {string|Promise} abc + */ +function quux () {} +// Message: Prefer a more specific type to `any` +```` + + + + + +## Passing examples + +The following patterns are not considered problems: + +````ts +/** + * @param {SomeType} abc + */ +function quux () {} +```` + diff --git a/docs/rules/reject-function-type.md b/docs/rules/reject-function-type.md new file mode 100644 index 000000000..f78016379 --- /dev/null +++ b/docs/rules/reject-function-type.md @@ -0,0 +1,51 @@ + + +# reject-function-type + +Reports use of `Function` type within JSDoc tag types. + +||| +|---|---| +|Context|everywhere| +|Tags|`augments`, `class`, `constant`, `enum`, `implements`, `member`, `module`, `namespace`, `param`, `property`, `returns`, `throws`, `type`, `typedef`, `yields`| +|Aliases|`constructor`, `const`, `extends`, `var`, `arg`, `argument`, `prop`, `return`, `exception`, `yield`| +|Closure-only|`package`, `private`, `protected`, `public`, `static`| +|Recommended|true| +|Settings|`mode`| +|Options|| + + + +## Failing examples + +The following patterns are considered problems: + +````ts +/** + * @param {Function} abc + */ +function quux () {} +// Message: Prefer a more specific type to `Function` + +/** + * @param {string|Array} abc + */ +function quux () {} +// Message: Prefer a more specific type to `Function` +```` + + + + + +## Passing examples + +The following patterns are not considered problems: + +````ts +/** + * @param {SomeType} abc + */ +function quux () {} +```` + diff --git a/src/buildRejectOrPreferRuleDefinition.js b/src/buildRejectOrPreferRuleDefinition.js index c29142168..0d37181ff 100644 --- a/src/buildRejectOrPreferRuleDefinition.js +++ b/src/buildRejectOrPreferRuleDefinition.js @@ -97,10 +97,11 @@ const infoUC = { /** * @param {{ - * checkNativeTypes: import('./rules/checkTypes.js').CheckNativeTypes|null - * overrideSettings?: null, + * checkNativeTypes?: import('./rules/checkTypes.js').CheckNativeTypes|null + * overrideSettings?: import('./iterateJsdoc.js').Settings['preferredTypes']|null, * description?: string, * schema?: import('eslint').Rule.RuleMetaData['schema'], + * typeName?: string, * url?: string, * }} cfg * @returns {import('@eslint/core').RuleDefinition< @@ -109,7 +110,8 @@ const infoUC = { */ export const buildRejectOrPreferRuleDefinition = ({ checkNativeTypes = null, - description = 'Reports invalid types.', + typeName, + description = typeName ?? 'Reports invalid types.', overrideSettings = null, schema = [], url = 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/check-types.md#repos-sticky-header', @@ -136,10 +138,14 @@ export const buildRejectOrPreferRuleDefinition = ({ * }} */ { - mode = settings.mode, + mode, preferredTypes: preferredTypesOriginal, - structuredTags = {}, - } = overrideSettings ?? settings; + structuredTags, + } = overrideSettings ? { + mode: settings.mode, + preferredTypes: overrideSettings, + structuredTags: {}, + } : settings; const injectObjectPreferredTypes = !('Object' in preferredTypesOriginal || 'object' in preferredTypesOriginal || @@ -199,7 +205,7 @@ export const buildRejectOrPreferRuleDefinition = ({ const getPreferredTypeInfo = (_type, typeNodeName, parentNode, property) => { let hasMatchingPreferredType = false; let isGenericMatch = false; - let typeName = typeNodeName; + let typName = typeNodeName; const isNameOfGeneric = parentNode !== undefined && parentNode.type === 'JsdocTypeGeneric' && property === 'left'; @@ -228,7 +234,7 @@ export const buildRejectOrPreferRuleDefinition = ({ ) && preferredType !== undefined ) { - typeName += checkPostFix; + typName += checkPostFix; return true; } @@ -259,7 +265,7 @@ export const buildRejectOrPreferRuleDefinition = ({ preferredType?.unifyParentAndChildTypeChecks)) && preferredType !== undefined ) { - typeName = checkPostFix; + typName = checkPostFix; return true; } @@ -280,7 +286,7 @@ export const buildRejectOrPreferRuleDefinition = ({ directNameMatch && !property; return [ - hasMatchingPreferredType, typeName, isGenericMatch, + hasMatchingPreferredType, typName, isGenericMatch, ]; }; @@ -302,15 +308,15 @@ export const buildRejectOrPreferRuleDefinition = ({ const [ hasMatchingPreferredType, - typeName, + typName, isGenericMatch, ] = getPreferredTypeInfo(type, typeNodeName, parentNode, property); let preferred; let types; if (hasMatchingPreferredType) { - const preferredSetting = preferredTypes[typeName]; - typeNodeName = typeName === '[]' ? typeName : typeNodeName; + const preferredSetting = preferredTypes[typName]; + typeNodeName = typName === '[]' ? typName : typeNodeName; if (!preferredSetting) { invalidTypes.push([ diff --git a/src/index-cjs.js b/src/index-cjs.js index b49a385d1..d08172bc8 100644 --- a/src/index-cjs.js +++ b/src/index-cjs.js @@ -1,9 +1,9 @@ import { buildForbidRuleDefinition, } from './buildForbidRuleDefinition.js'; -// import { -// buildRejectOrPreferRuleDefinition, -// } from './buildRejectOrPreferRuleDefinition.js'; +import { + buildRejectOrPreferRuleDefinition, +} from './buildRejectOrPreferRuleDefinition.js'; import { getJsdocProcessorPlugin, } from './getJsdocProcessorPlugin.js'; @@ -110,20 +110,33 @@ index.rules = { 'no-restricted-syntax': noRestrictedSyntax, 'no-types': noTypes, 'no-undefined-types': noUndefinedTypes, - // 'reject-any-type': buildRejectOrPreferRuleDefinition({ - // contexts: [ - // { - // unifyParentAndChildTypeChecks: true, - // }, - // ], - // }), - // 'reject-function-type': buildRejectOrPreferRuleDefinition({ - // contexts: [ - // { - // unifyParentAndChildTypeChecks: true, - // }, - // ], - // }), + 'reject-any-type': buildRejectOrPreferRuleDefinition({ + description: 'Reports use of `any` or `*` type', + overrideSettings: { + '*': { + message: 'Prefer a more specific type to `*`', + replacement: false, + unifyParentAndChildTypeChecks: true, + }, + any: { + message: 'Prefer a more specific type to `any`', + replacement: false, + unifyParentAndChildTypeChecks: true, + }, + }, + url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/reject-any-type.md#repos-sticky-header', + }), + 'reject-function-type': buildRejectOrPreferRuleDefinition({ + description: 'Reports use of `Function` type', + overrideSettings: { + Function: { + message: 'Prefer a more specific type to `Function`', + replacement: false, + unifyParentAndChildTypeChecks: true, + }, + }, + url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/reject-function-type.md#repos-sticky-header', + }), 'require-asterisk-prefix': requireAsteriskPrefix, 'require-description': requireDescription, 'require-description-complete-sentence': requireDescriptionCompleteSentence, @@ -235,6 +248,8 @@ const createRecommendedRuleset = (warnOrError, flatName) => { 'jsdoc/no-restricted-syntax': 'off', 'jsdoc/no-types': 'off', 'jsdoc/no-undefined-types': warnOrError, + 'jsdoc/reject-any-type': warnOrError, + 'jsdoc/reject-function-type': warnOrError, 'jsdoc/require-asterisk-prefix': 'off', 'jsdoc/require-description': 'off', 'jsdoc/require-description-complete-sentence': 'off', diff --git a/src/index-esm.js b/src/index-esm.js index 9ed80fb38..09e47db9a 100644 --- a/src/index-esm.js +++ b/src/index-esm.js @@ -8,6 +8,9 @@ import index from './index-cjs.js'; import { buildForbidRuleDefinition, } from './buildForbidRuleDefinition.js'; +import { + buildRejectOrPreferRuleDefinition, +} from './buildRejectOrPreferRuleDefinition.js'; // eslint-disable-next-line unicorn/prefer-export-from --- Reusing `index` export default index; @@ -22,7 +25,7 @@ export default index; * settings?: Partial, * rules?: {[key in keyof import('./rules.d.ts').Rules]?: import('eslint').Linter.RuleEntry}, * extraRuleDefinitions?: { - * forbid: { + * forbid?: { * [contextName: string]: { * description?: string, * url?: string, @@ -32,6 +35,19 @@ export default index; * comment: string * })[] * } + * }, + * preferTypes?: { + * [typeName: string]: { + * description: string, + * overrideSettings: { + * [typeNodeName: string]: { + * message: string, + * replacement?: false|string, + * unifyParentAndChildTypeChecks?: boolean, + * } + * }, + * url: string, + * } * } * } * } @@ -125,6 +141,25 @@ export const jsdoc = function (cfg) { }); } } + + if (cfg.extraRuleDefinitions.preferTypes) { + for (const [ + typeName, + { + description, + overrideSettings, + url, + }, + ] of Object.entries(cfg.extraRuleDefinitions.preferTypes)) { + outputConfig.plugins.jsdoc.rules[`prefer-type-${typeName}`] = + buildRejectOrPreferRuleDefinition({ + description, + overrideSettings, + typeName, + url, + }); + } + } } } diff --git a/src/index.js b/src/index.js index 5f3d0d456..358afc454 100644 --- a/src/index.js +++ b/src/index.js @@ -7,9 +7,9 @@ import { import { buildForbidRuleDefinition, } from './buildForbidRuleDefinition.js'; -// import { -// buildRejectOrPreferRuleDefinition, -// } from './buildRejectOrPreferRuleDefinition.js'; +import { + buildRejectOrPreferRuleDefinition, +} from './buildRejectOrPreferRuleDefinition.js'; import { getJsdocProcessorPlugin, } from './getJsdocProcessorPlugin.js'; @@ -116,20 +116,33 @@ index.rules = { 'no-restricted-syntax': noRestrictedSyntax, 'no-types': noTypes, 'no-undefined-types': noUndefinedTypes, - // 'reject-any-type': buildRejectOrPreferRuleDefinition({ - // contexts: [ - // { - // unifyParentAndChildTypeChecks: true, - // }, - // ], - // }), - // 'reject-function-type': buildRejectOrPreferRuleDefinition({ - // contexts: [ - // { - // unifyParentAndChildTypeChecks: true, - // }, - // ], - // }), + 'reject-any-type': buildRejectOrPreferRuleDefinition({ + description: 'Reports use of `any` or `*` type', + overrideSettings: { + '*': { + message: 'Prefer a more specific type to `*`', + replacement: false, + unifyParentAndChildTypeChecks: true, + }, + any: { + message: 'Prefer a more specific type to `any`', + replacement: false, + unifyParentAndChildTypeChecks: true, + }, + }, + url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/reject-any-type.md#repos-sticky-header', + }), + 'reject-function-type': buildRejectOrPreferRuleDefinition({ + description: 'Reports use of `Function` type', + overrideSettings: { + Function: { + message: 'Prefer a more specific type to `Function`', + replacement: false, + unifyParentAndChildTypeChecks: true, + }, + }, + url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/reject-function-type.md#repos-sticky-header', + }), 'require-asterisk-prefix': requireAsteriskPrefix, 'require-description': requireDescription, 'require-description-complete-sentence': requireDescriptionCompleteSentence, @@ -241,6 +254,8 @@ const createRecommendedRuleset = (warnOrError, flatName) => { 'jsdoc/no-restricted-syntax': 'off', 'jsdoc/no-types': 'off', 'jsdoc/no-undefined-types': warnOrError, + 'jsdoc/reject-any-type': warnOrError, + 'jsdoc/reject-function-type': warnOrError, 'jsdoc/require-asterisk-prefix': 'off', 'jsdoc/require-description': 'off', 'jsdoc/require-description-complete-sentence': 'off', @@ -626,7 +641,7 @@ export default index; * settings?: Partial, * rules?: {[key in keyof import('./rules.d.ts').Rules]?: import('eslint').Linter.RuleEntry}, * extraRuleDefinitions?: { - * forbid: { + * forbid?: { * [contextName: string]: { * description?: string, * url?: string, @@ -636,6 +651,19 @@ export default index; * comment: string * })[] * } + * }, + * preferTypes?: { + * [typeName: string]: { + * description: string, + * overrideSettings: { + * [typeNodeName: string]: { + * message: string, + * replacement?: false|string, + * unifyParentAndChildTypeChecks?: boolean, + * } + * }, + * url: string, + * } * } * } * } @@ -729,6 +757,25 @@ export const jsdoc = function (cfg) { }); } } + + if (cfg.extraRuleDefinitions.preferTypes) { + for (const [ + typeName, + { + description, + overrideSettings, + url, + }, + ] of Object.entries(cfg.extraRuleDefinitions.preferTypes)) { + outputConfig.plugins.jsdoc.rules[`prefer-type-${typeName}`] = + buildRejectOrPreferRuleDefinition({ + description, + overrideSettings, + typeName, + url, + }); + } + } } } diff --git a/src/rules.d.ts b/src/rules.d.ts index 04b9bf961..2b9b822fe 100644 --- a/src/rules.d.ts +++ b/src/rules.d.ts @@ -410,6 +410,12 @@ export interface Rules { } ]; + /** Reports use of `any` or `*` type */ + "jsdoc/reject-any-type": []; + + /** Reports use of `Function` type */ + "jsdoc/reject-function-type": []; + /** Requires that each JSDoc line starts with an `*`. */ "jsdoc/require-asterisk-prefix": | [] diff --git a/test/index.js b/test/index.js index d701b6ca0..4de54efa8 100644 --- a/test/index.js +++ b/test/index.js @@ -489,3 +489,195 @@ for (const [ ruleName: `forbid-${contextName}`, }); } + +for (const [ + typeName, + overrideSettings, + assertions, + description, + url, +] of + /** + * @type {[ + * string, + * { + * [key: string]: { + * message: string, + * replacement?: false|string, + * unifyParentAndChildTypeChecks?: boolean + * } + * }, + * import('./rules/index.js').TestCases, + * string, + * string + * ][] + * } + */ ([ + [ + 'promise', + { + Promise: { + message: 'Add a generic type for this Promise.', + replacement: false, + unifyParentAndChildTypeChecks: false, + }, + }, + { + invalid: [ + { + code: ` + /** + * @type {Promise} + */ + `, + errors: [ + { + line: 3, + message: 'Add a generic type for this Promise.', + }, + ], + }, + ], + valid: [ + { + code: ` + /** + * @type {Promise} + */ + `, + }, + { + code: ` + /** + * @type {Promise} + */ + `, + }, + ], + }, + 'Disallow Promises without a generic type', + 'https://example.com/Promise-rule.md', + ], + [ + 'object', + { + Object: { + message: 'Use the specific object type or add `object` to ' + + 'a typedef if truly arbitrary', + replacement: 'object', + unifyParentAndChildTypeChecks: false, + }, + }, + { + invalid: [ + { + code: ` + /** + * @type {Object} + */ + `, + errors: [ + { + line: 3, + message: 'Use the specific object type or add `object` to ' + + 'a typedef if truly arbitrary', + }, + ], + output: ` + /** + * @type {object} + */ + `, + }, + ], + valid: [ + { + code: ` + /** + * @type {object} + */ + `, + }, + { + code: ` + /** + * @type {Object} + */ + `, + }, + ], + }, + 'Replace `Object` with `object', + 'https://example.com/Object-rule.md', + ], + [ + 'object-parent', + { + 'object<>': { + message: 'Use the upper-case form for current TypeScript ' + + 'JSDoc compatibility and generic-like appearance for a parent', + replacement: 'Object<>', + }, + }, + { + invalid: [ + { + code: ` + /** + * @type {object} + */ + `, + errors: [ + { + line: 3, + message: 'Use the upper-case form for current TypeScript ' + + 'JSDoc compatibility and generic-like appearance for a parent', + }, + ], + output: ` + /** + * @type {Object} + */ + `, + }, + ], + valid: [ + { + code: ` + /** + * @type {Object} + */ + `, + }, + { + code: ` + /** + * @type {object} + */ + `, + }, + ], + }, + 'Replace `object<>` with `Object<>', + 'https://example.com/Object-parent-rule.md', + ], + ])) { + runRuleTests({ + assertions, + config: jsdoc({ + extraRuleDefinitions: { + preferTypes: { + [typeName]: { + description, + overrideSettings, + url, + }, + }, + }, + }).plugins?.jsdoc, + languageOptions: { + parser: typescriptEslintParser, + }, + ruleName: `prefer-type-${typeName}`, + }); +} diff --git a/test/rules/assertions/rejectAnyType.js b/test/rules/assertions/rejectAnyType.js new file mode 100644 index 000000000..6dc033a4c --- /dev/null +++ b/test/rules/assertions/rejectAnyType.js @@ -0,0 +1,42 @@ +export default { + invalid: [ + { + code: ` + /** + * @param {any} abc + */ + function quux () {} + `, + errors: [ + { + line: 3, + message: 'Prefer a more specific type to `any`', + }, + ], + }, + { + code: ` + /** + * @param {string|Promise} abc + */ + function quux () {} + `, + errors: [ + { + line: 3, + message: 'Prefer a more specific type to `any`', + }, + ], + }, + ], + valid: [ + { + code: ` + /** + * @param {SomeType} abc + */ + function quux () {} + `, + }, + ], +}; diff --git a/test/rules/assertions/rejectFunctionType.js b/test/rules/assertions/rejectFunctionType.js new file mode 100644 index 000000000..f8297ff8c --- /dev/null +++ b/test/rules/assertions/rejectFunctionType.js @@ -0,0 +1,42 @@ +export default { + invalid: [ + { + code: ` + /** + * @param {Function} abc + */ + function quux () {} + `, + errors: [ + { + line: 3, + message: 'Prefer a more specific type to `Function`', + }, + ], + }, + { + code: ` + /** + * @param {string|Array} abc + */ + function quux () {} + `, + errors: [ + { + line: 3, + message: 'Prefer a more specific type to `Function`', + }, + ], + }, + ], + valid: [ + { + code: ` + /** + * @param {SomeType} abc + */ + function quux () {} + `, + }, + ], +}; diff --git a/test/rules/ruleNames.json b/test/rules/ruleNames.json index 31c48d291..29b483221 100644 --- a/test/rules/ruleNames.json +++ b/test/rules/ruleNames.json @@ -29,6 +29,8 @@ "no-restricted-syntax", "no-types", "no-undefined-types", + "reject-any-type", + "reject-function-type", "require-asterisk-prefix", "require-description", "require-description-complete-sentence",