diff --git a/docs/rules/require-returns.md b/docs/rules/require-returns.md index 34d67f841..1f54d2073 100644 --- a/docs/rules/require-returns.md +++ b/docs/rules/require-returns.md @@ -634,6 +634,20 @@ export default async function demo() { return true; } // Message: Missing JSDoc @returns declaration. + +/** + * + */ +function quux () {} + +class Test { + /** + * + */ + abstract Test(): string; +} +// "jsdoc/require-returns": ["error"|"warn", {"contexts":["FunctionDeclaration",{"context":"TSEmptyBodyFunctionExpression","forceRequireReturn":true}]}] +// Message: Missing JSDoc @returns declaration. ```` @@ -1160,5 +1174,18 @@ export function readFixture(path: string): void; * Reads a test fixture. */ export function readFixture(path: string); + +/** + * + */ +function quux () {} + +class Test { + /** + * @returns {string} The test value + */ + abstract Test(): string; +} +// "jsdoc/require-returns": ["error"|"warn", {"contexts":["FunctionDeclaration",{"context":"TSEmptyBodyFunctionExpression","forceRequireReturn":true}]}] ```` diff --git a/src/iterateJsdoc.js b/src/iterateJsdoc.js index 337fb0e64..f32690e3e 100644 --- a/src/iterateJsdoc.js +++ b/src/iterateJsdoc.js @@ -8,6 +8,7 @@ import { stringify as commentStringify, util, } from 'comment-parser'; +import esquery from 'esquery'; /** * @typedef {number} Integer @@ -26,7 +27,8 @@ import { * tags?: string[], * replacement?: string, * minimum?: Integer, - * message?: string + * message?: string, + * forceRequireReturn?: boolean * }} ContextObject */ /** @@ -467,6 +469,16 @@ import { * @returns {boolean} */ +/** + * @callback FindContext + * @param {Context[]} contexts + * @param {string|undefined} comment + * @returns {{ + * foundContext: Context|undefined, + * contextStr: string + * }} + */ + /** * @typedef {BasicUtils & { * isIteratingFunction: IsIteratingFunction, @@ -526,7 +538,8 @@ import { * hasOptionTag: HasOptionTag, * getClassNode: GetClassNode, * getClassJsdoc: GetClassJsdoc, - * classHasTag: ClassHasTag + * classHasTag: ClassHasTag, + * findContext: FindContext * }} Utils */ @@ -1712,6 +1725,39 @@ const getUtils = ( } }; + /** @type {FindContext} */ + utils.findContext = (contexts, comment) => { + const foundContext = contexts.find((cntxt) => { + return typeof cntxt === 'string' ? + esquery.matches( + /** @type {Node} */ (node), + esquery.parse(cntxt), + undefined, + { + visitorKeys: sourceCode.visitorKeys, + }, + ) : + (!cntxt.context || cntxt.context === 'any' || + esquery.matches( + /** @type {Node} */ (node), + esquery.parse(cntxt.context), + undefined, + { + visitorKeys: sourceCode.visitorKeys, + }, + )) && comment === cntxt.comment; + }); + + const contextStr = typeof foundContext === 'object' ? + foundContext.context ?? 'any' : + String(foundContext); + + return { + contextStr, + foundContext, + }; + }; + return utils; }; @@ -1938,7 +1984,6 @@ const makeReport = (context, commentNode) => { * @param {JsdocBlockWithInline} jsdoc * @param {RuleConfig} ruleConfig * @param {import('eslint').Rule.RuleContext} context - * @param {string[]} lines * @param {import('@es-joy/jsdoccomment').Token} jsdocNode * @param {Node|null} node * @param {Settings} settings @@ -1951,7 +1996,7 @@ const makeReport = (context, commentNode) => { const iterate = ( info, indent, jsdoc, - ruleConfig, context, lines, jsdocNode, node, settings, + ruleConfig, context, jsdocNode, node, settings, sourceCode, iterator, state, iteratingAll, ) => { const jsdocNde = /** @type {unknown} */ (jsdocNode); @@ -2145,7 +2190,6 @@ const iterateAllJsdocs = (iterator, ruleConfig, contexts, additiveCommentContext jsdoc, ruleConfig, context, - lines, jsdocNode, /** @type {Node} */ (node), @@ -2188,7 +2232,6 @@ const iterateAllJsdocs = (iterator, ruleConfig, contexts, additiveCommentContext jsdoc, ruleConfig, context, - lines, jsdocNode, node, /** @type {Settings} */ @@ -2448,7 +2491,6 @@ export default function iterateJsdoc (iterator, ruleConfig) { jsdoc, ruleConfig, context, - lines, jsdocNode, node, settings, diff --git a/src/rules/noMissingSyntax.js b/src/rules/noMissingSyntax.js index 19dce93bb..8c06a52d8 100644 --- a/src/rules/noMissingSyntax.js +++ b/src/rules/noMissingSyntax.js @@ -1,5 +1,4 @@ import iterateJsdoc from '../iterateJsdoc'; -import esquery from 'esquery'; /** * @typedef {{ @@ -44,12 +43,11 @@ const incrementSelector = (state, selector, comment) => { export default iterateJsdoc(({ context, - node, info: { comment, }, - sourceCode, state, + utils, }) => { if (!context.options[0]) { // Handle error later @@ -61,30 +59,9 @@ export default iterateJsdoc(({ */ const contexts = context.options[0].contexts; - const foundContext = contexts.find((cntxt) => { - return typeof cntxt === 'string' ? - esquery.matches( - /** @type {import('../iterateJsdoc.js').Node} */ (node), - esquery.parse(cntxt), - undefined, - { - visitorKeys: sourceCode.visitorKeys, - }, - ) : - (!cntxt.context || cntxt.context === 'any' || - esquery.matches( - /** @type {import('../iterateJsdoc.js').Node} */ (node), - esquery.parse(cntxt.context), - undefined, - { - visitorKeys: sourceCode.visitorKeys, - }, - )) && comment === cntxt.comment; - }); - - const contextStr = typeof foundContext === 'object' ? - foundContext.context ?? 'any' : - String(foundContext); + const { + contextStr, + } = utils.findContext(contexts, comment); setDefaults(state); diff --git a/src/rules/noRestrictedSyntax.js b/src/rules/noRestrictedSyntax.js index 02fa4e9c4..291e85248 100644 --- a/src/rules/noRestrictedSyntax.js +++ b/src/rules/noRestrictedSyntax.js @@ -1,14 +1,12 @@ import iterateJsdoc from '../iterateJsdoc'; -import esquery from 'esquery'; export default iterateJsdoc(({ - node, context, info: { comment, }, - sourceCode, report, + utils, }) => { if (!context.options.length) { report('Rule `no-restricted-syntax` is missing a `contexts` option.'); @@ -20,33 +18,10 @@ export default iterateJsdoc(({ contexts, } = context.options[0]; - const foundContext = contexts.find( - /** - * @param {string|{context: string, comment: string}} cntxt - * @returns {boolean} - */ - (cntxt) => { - return typeof cntxt === 'string' ? - esquery.matches( - /** @type {import('../iterateJsdoc.js').Node} */ (node), - esquery.parse(cntxt), - undefined, - { - visitorKeys: sourceCode.visitorKeys, - }, - ) : - (!cntxt.context || cntxt.context === 'any' || - esquery.matches( - /** @type {import('../iterateJsdoc.js').Node} */ (node), - esquery.parse(cntxt.context), - undefined, - { - visitorKeys: sourceCode.visitorKeys, - }, - )) && - comment === cntxt.comment; - }, - ); + const { + foundContext, + contextStr, + } = utils.findContext(contexts, comment); // We are not on the *particular* matching context/comment, so don't assume // we need reporting @@ -54,10 +29,9 @@ export default iterateJsdoc(({ return; } - const contextStr = typeof foundContext === 'object' ? - foundContext.context ?? 'any' : - foundContext; - const message = foundContext?.message ?? + const message = /** @type {import('../iterateJsdoc.js').ContextObject} */ ( + foundContext + )?.message ?? 'Syntax is restricted: {{context}}' + (comment ? ' with {{comment}}' : ''); diff --git a/src/rules/requireReturns.js b/src/rules/requireReturns.js index 02f10974d..3003d3357 100644 --- a/src/rules/requireReturns.js +++ b/src/rules/requireReturns.js @@ -35,11 +35,15 @@ const canSkip = (utils) => { }; export default iterateJsdoc(({ + info: { + comment, + }, report, utils, context, }) => { const { + contexts, forceRequireReturn = false, forceReturnsWithAsync = false, } = context.options[0] || {}; @@ -50,6 +54,17 @@ export default iterateJsdoc(({ return; } + /** @type {boolean|undefined} */ + let forceRequireReturnContext; + if (contexts) { + const { + foundContext, + } = utils.findContext(contexts, comment); + if (typeof foundContext === 'object') { + forceRequireReturnContext = foundContext.forceRequireReturn; + } + } + const tagName = /** @type {string} */ (utils.getPreferredTagName({ tagName: 'returns', })); @@ -76,7 +91,7 @@ export default iterateJsdoc(({ return false; } - if (forceRequireReturn && ( + if ((forceRequireReturn || forceRequireReturnContext) && ( iteratingFunction || utils.isVirtualFunction() )) { return true; @@ -131,6 +146,9 @@ export default iterateJsdoc(({ context: { type: 'string', }, + forceRequireReturn: { + type: 'boolean', + }, }, type: 'object', }, diff --git a/test/rules/assertions/requireReturns.js b/test/rules/assertions/requireReturns.js index 7c57f5e5c..81ba52db9 100644 --- a/test/rules/assertions/requireReturns.js +++ b/test/rules/assertions/requireReturns.js @@ -1765,6 +1765,39 @@ export default { sourceType: 'module', }, }, + { + code: ` + /** + * + */ + function quux () {} + + class Test { + /** + * + */ + abstract Test(): string; + } + `, + errors: [ + { + line: 8, + message: 'Missing JSDoc @returns declaration.', + }, + ], + options: [ + { + contexts: [ + 'FunctionDeclaration', + { + context: 'TSEmptyBodyFunctionExpression', + forceRequireReturn: true, + }, + ], + }, + ], + parser: require.resolve('@typescript-eslint/parser'), + }, ], valid: [ { @@ -2662,5 +2695,32 @@ export default { `, parser: require.resolve('@typescript-eslint/parser'), }, + { + code: ` + /** + * + */ + function quux () {} + + class Test { + /** + * @returns {string} The test value + */ + abstract Test(): string; + } + `, + options: [ + { + contexts: [ + 'FunctionDeclaration', + { + context: 'TSEmptyBodyFunctionExpression', + forceRequireReturn: true, + }, + ], + }, + ], + parser: require.resolve('@typescript-eslint/parser'), + }, ], };