diff --git a/.README/rules/match-description.md b/.README/rules/match-description.md index e829014ed..33108fc82 100644 --- a/.README/rules/match-description.md +++ b/.README/rules/match-description.md @@ -10,8 +10,14 @@ by our supported Node versions): ``^\n?([A-Z`\\d_][\\s\\S]*[.?!`]\\s*)?$`` -Applies to the jsdoc block description and `@description` (or `@desc`) -by default but the `tags` option (see below) may be used to match other tags. +Applies by default to the jsdoc block description and to the following tags: + +- `@description`/`@desc` +- `@summary` +- `@file`/`@fileoverview`/`@overview` +- `@classdesc` + +In addition, the `tags` option (see below) may be used to match other tags. The default (and all regex options) defaults to using (only) the `u` flag, so to add your own flags, encapsulate your expression as a string, but like a @@ -54,6 +60,21 @@ You may provide a custom default message by using the following format: This can be overridden per tag or for the main block description by setting `message` within `tags` or `mainDescription`, respectively. +### `nonemptyTags` + +If not set to `false`, will enforce that the following tags have at least +some content: + +- `@copyright` +- `@example` +- `@see` +- `@todo` +- `@throws`/`@exception` +- `@yields`/`@yield` + +If you supply your own tag description for any of the above tags in `tags`, +your description will take precedence. + ### `tags` If you want different regular expressions to apply to tags, you may use @@ -161,7 +182,7 @@ section of our README for more on the expected format. |Aliases|`@desc`| |Recommended|false| |Settings|| -|Options|`contexts`, `mainDescription`, `matchDescription`, `message`, `tags`| +|Options|`contexts`, `mainDescription`, `matchDescription`, `message`, `nonemptyTags`, `tags`| ## Failing examples diff --git a/.README/rules/require-description.md b/.README/rules/require-description.md index 0582936cd..af9337d4c 100644 --- a/.README/rules/require-description.md +++ b/.README/rules/require-description.md @@ -2,7 +2,8 @@ {"gitdown": "contents", "rootId": "require-description"} -Requires that all functions have a description. +Requires that all functions (or optionally other structures) with a JSDoc block +have a description. * All functions must have an implicit description (e.g., text above tags) or have the option `descriptionStyle` set to `tag` (requiring `@description` diff --git a/docs/rules/match-description.md b/docs/rules/match-description.md index 1f198523d..a2f02187b 100644 --- a/docs/rules/match-description.md +++ b/docs/rules/match-description.md @@ -5,6 +5,7 @@ * [Options](#user-content-match-description-options) * [`matchDescription`](#user-content-match-description-options-matchdescription) * [`message`](#user-content-match-description-options-message) + * [`nonemptyTags`](#user-content-match-description-options-nonemptytags) * [`tags`](#user-content-match-description-options-tags) * [`mainDescription`](#user-content-match-description-options-maindescription) * [`contexts`](#user-content-match-description-options-contexts) @@ -21,8 +22,14 @@ by our supported Node versions): ``^\n?([A-Z`\\d_][\\s\\S]*[.?!`]\\s*)?$`` -Applies to the jsdoc block description and `@description` (or `@desc`) -by default but the `tags` option (see below) may be used to match other tags. +Applies by default to the jsdoc block description and to the following tags: + +- `@description`/`@desc` +- `@summary` +- `@file`/`@fileoverview`/`@overview` +- `@classdesc` + +In addition, the `tags` option (see below) may be used to match other tags. The default (and all regex options) defaults to using (only) the `u` flag, so to add your own flags, encapsulate your expression as a string, but like a @@ -71,6 +78,23 @@ You may provide a custom default message by using the following format: This can be overridden per tag or for the main block description by setting `message` within `tags` or `mainDescription`, respectively. + + +### nonemptyTags + +If not set to `false`, will enforce that the following tags have at least +some content: + +- `@copyright` +- `@example` +- `@see` +- `@todo` +- `@throws`/`@exception` +- `@yields`/`@yield` + +If you supply your own tag description for any of the above tags in `tags`, +your description will take precedence. + ### tags @@ -186,7 +210,7 @@ section of our README for more on the expected format. |Aliases|`@desc`| |Recommended|false| |Settings|| -|Options|`contexts`, `mainDescription`, `matchDescription`, `message`, `tags`| +|Options|`contexts`, `mainDescription`, `matchDescription`, `message`, `nonemptyTags`, `tags`| @@ -335,7 +359,6 @@ function quux (foo) { function quux () { } -// "jsdoc/match-description": ["error"|"warn", {"tags":{"summary":true}}] // Message: JSDoc description does not satisfy the regex pattern. /** @@ -368,7 +391,6 @@ function quux () { function quux (foo) { } -// "jsdoc/match-description": ["error"|"warn", {"tags":{"description":true}}] // Message: JSDoc description does not satisfy the regex pattern. /** @@ -602,6 +624,13 @@ function quux () { function foo(): string; // "jsdoc/match-description": ["error"|"warn", {"contexts":[{"comment":"JsdocBlock[endLine=0]"}],"matchDescription":"^\\S[\\s\\S]*\\S$"}] // Message: JSDoc description does not satisfy the regex pattern. + +/** + * @copyright + */ +function quux () { +} +// Message: JSDoc description must not be empty. ```` @@ -722,7 +751,6 @@ function quux () { function quux () { } -// "jsdoc/match-description": ["error"|"warn", {"tags":{"description":true}}] /** * @description Foo @@ -732,13 +760,11 @@ function quux () { function quux () { } -// "jsdoc/match-description": ["error"|"warn", {"tags":{"description":true}}] /** @description Foo bar. */ function quux () { } -// "jsdoc/match-description": ["error"|"warn", {"tags":{"description":true}}] /** * @description Foo @@ -747,7 +773,6 @@ function quux () { function quux () { } -// "jsdoc/match-description": ["error"|"warn", {"tags":{"description":true}}] /** * Foo. {@see Math.sin}. @@ -872,7 +897,7 @@ const q = { // "jsdoc/match-description": ["error"|"warn", {"contexts":[]}] /** - * @description foo. + * @deprecated foo. */ function quux () { @@ -887,7 +912,6 @@ function quux () { function quux () { } -// "jsdoc/match-description": ["error"|"warn", {"tags":{"summary":true}}] /** * Foo. @@ -975,5 +999,12 @@ function foo(): string; */ function foo(): void; // "jsdoc/match-description": ["error"|"warn", {"contexts":[{"comment":"JsdocBlock[endLine!=0]:not(:has(JsdocTag))"}],"matchDescription":"^\\S[\\s\\S]*\\S$"}] + +/** + * @copyright + */ +function quux () { +} +// "jsdoc/match-description": ["error"|"warn", {"nonemptyTags":false}] ```` diff --git a/docs/rules/match-name.md b/docs/rules/match-name.md index c5fe1abce..fd25834f5 100644 --- a/docs/rules/match-name.md +++ b/docs/rules/match-name.md @@ -178,6 +178,12 @@ function quux () {} */ // "jsdoc/match-name": ["error"|"warn", {"match":[{"disallowName":"/^opt_/i","replacement":""}]}] // Message: Only allowing names not matching `/^opt_/i` but found "opt_a". + +/** + * @template + */ +// "jsdoc/match-name": ["error"|"warn", {"match":[{"disallowName":"/^$/","tags":["template"]}]}] +// Message: Only allowing names not matching `/^$/u` but found "". ```` diff --git a/docs/rules/no-restricted-syntax.md b/docs/rules/no-restricted-syntax.md index e4622248f..d8f7ec248 100644 --- a/docs/rules/no-restricted-syntax.md +++ b/docs/rules/no-restricted-syntax.md @@ -373,5 +373,11 @@ class Test { abstract Test(): void; } // "jsdoc/no-restricted-syntax": ["error"|"warn", {"contexts":[{"comment":"JsdocBlock:not(*:has(JsdocTag[tag=/returns/]))","context":"TSEmptyBodyFunctionExpression[returnType.typeAnnotation.type!=/TSVoidKeyword|TSUndefinedKeyword/]","message":"methods with non-void return types must have a @returns tag"}]}] + +/** + * @private + */ +function quux () {} +// "jsdoc/no-restricted-syntax": ["error"|"warn", {"contexts":[{"comment":"JsdocBlock:not(JsdocBlock:has(JsdocTag[tag=/private|protected|public/]))","context":"any","message":"Access modifier tags must be present"}]}] ```` diff --git a/docs/rules/require-description.md b/docs/rules/require-description.md index 7a344a453..7d2143da1 100644 --- a/docs/rules/require-description.md +++ b/docs/rules/require-description.md @@ -8,7 +8,8 @@ * [Passing examples](#user-content-require-description-passing-examples) -Requires that all functions have a description. +Requires that all functions (or optionally other structures) with a JSDoc block +have a description. * All functions must have an implicit description (e.g., text above tags) or have the option `descriptionStyle` set to `tag` (requiring `@description` diff --git a/src/getDefaultTagStructureForMode.js b/src/getDefaultTagStructureForMode.js index 5f473b59b..44cd80ce9 100644 --- a/src/getDefaultTagStructureForMode.js +++ b/src/getDefaultTagStructureForMode.js @@ -838,6 +838,10 @@ const getDefaultTagStructureForMode = (mode) => { 'namepathRole', isJsdoc ? 'text' : 'namepath-referencing', ], + [ + 'nameRequired', !isJsdoc, + ], + // Though defines `namepathRole: 'namepath-defining'` in a sense, it is // not parseable in the same way for template (e.g., allowing commas), // so not adding diff --git a/src/iterateJsdoc.js b/src/iterateJsdoc.js index f32690e3e..e6a57ec44 100644 --- a/src/iterateJsdoc.js +++ b/src/iterateJsdoc.js @@ -1644,7 +1644,7 @@ const getUtils = ( /** @type {GetTagsByType} */ utils.getTagsByType = (tags) => { - return jsdocUtils.getTagsByType(context, mode, tags, tagNamePreference); + return jsdocUtils.getTagsByType(context, mode, tags); }; /** @type {HasOptionTag} */ diff --git a/src/jsdocUtils.js b/src/jsdocUtils.js index 49fbcd7c7..10a9f6a98 100644 --- a/src/jsdocUtils.js +++ b/src/jsdocUtils.js @@ -1422,14 +1422,12 @@ const tagsWithNamesAndDescriptions = new Set([ * @param {import('eslint').Rule.RuleContext} context * @param {ParserMode|undefined} mode * @param {import('comment-parser').Spec[]} tags - * @param {TagNamePreference} tagPreference * @returns {{ * tagsWithNames: import('comment-parser').Spec[], * tagsWithoutNames: import('comment-parser').Spec[] * }} */ -const getTagsByType = (context, mode, tags, tagPreference) => { - const descName = getPreferredTagName(context, mode, 'description', tagPreference); +const getTagsByType = (context, mode, tags) => { /** * @type {import('comment-parser').Spec[]} */ @@ -1439,7 +1437,7 @@ const getTagsByType = (context, mode, tags, tagPreference) => { tag: tagName, } = tag; const tagWithName = tagsWithNamesAndDescriptions.has(tagName); - if (!tagWithName && tagName !== descName) { + if (!tagWithName) { tagsWithoutNames.push(tag); } diff --git a/src/rules/matchDescription.js b/src/rules/matchDescription.js index 0f1a181c0..3205d7655 100644 --- a/src/rules/matchDescription.js +++ b/src/rules/matchDescription.js @@ -25,7 +25,8 @@ export default iterateJsdoc(({ mainDescription, matchDescription, message, - tags, + nonemptyTags = true, + tags = {}, } = context.options[0] || {}; /** @@ -81,7 +82,52 @@ export default iterateJsdoc(({ validateDescription(description); } - if (!tags || !Object.keys(tags).length) { + /** + * @param {string} tagName + * @returns {boolean} + */ + const hasNoTag = (tagName) => { + return !tags[tagName]; + }; + + for (const tag of [ + 'description', + 'summary', + 'file', + 'classdesc', + ]) { + utils.forEachPreferredTag(tag, (matchingJsdocTag, targetTagName) => { + const desc = (matchingJsdocTag.name + ' ' + utils.getTagDescription(matchingJsdocTag)).trim(); + if (hasNoTag(targetTagName)) { + validateDescription(desc, matchingJsdocTag); + } + }, true); + } + + if (nonemptyTags) { + for (const tag of [ + 'copyright', + 'example', + 'see', + 'throws', + 'todo', + 'yields', + ]) { + utils.forEachPreferredTag(tag, (matchingJsdocTag, targetTagName) => { + const desc = (matchingJsdocTag.name + ' ' + utils.getTagDescription(matchingJsdocTag)).trim(); + + if (hasNoTag(targetTagName) && !(/.+/u).test(desc)) { + report( + 'JSDoc description must not be empty.', + null, + matchingJsdocTag, + ); + } + }); + } + } + + if (!Object.keys(tags).length) { return; } @@ -93,13 +139,6 @@ export default iterateJsdoc(({ return Boolean(tags[tagName]); }; - utils.forEachPreferredTag('description', (matchingJsdocTag, targetTagName) => { - const desc = (matchingJsdocTag.name + ' ' + utils.getTagDescription(matchingJsdocTag)).trim(); - if (hasOptionTag(targetTagName)) { - validateDescription(desc, matchingJsdocTag); - } - }, true); - const whitelistedTags = utils.filterTags(({ tag: tagName, }) => { @@ -195,6 +234,9 @@ export default iterateJsdoc(({ message: { type: 'string', }, + nonemptyTags: { + type: 'boolean', + }, tags: { patternProperties: { '.*': { diff --git a/test/rules/assertions/matchDescription.js b/test/rules/assertions/matchDescription.js index d25817cc2..776dedb62 100644 --- a/test/rules/assertions/matchDescription.js +++ b/test/rules/assertions/matchDescription.js @@ -344,13 +344,6 @@ export default { message: 'JSDoc description does not satisfy the regex pattern.', }, ], - options: [ - { - tags: { - summary: true, - }, - }, - ], }, { code: ` @@ -419,13 +412,6 @@ export default { message: 'JSDoc description does not satisfy the regex pattern.', }, ], - options: [ - { - tags: { - description: true, - }, - }, - ], }, { code: ` @@ -1001,6 +987,21 @@ export default { ], parser: require.resolve('@typescript-eslint/parser'), }, + { + code: ` + /** + * @copyright + */ + function quux () { + } + `, + errors: [ + { + line: 3, + message: 'JSDoc description must not be empty.', + }, + ], + }, ], valid: [ { @@ -1192,13 +1193,6 @@ export default { } `, - options: [ - { - tags: { - description: true, - }, - }, - ], }, { code: ` @@ -1211,13 +1205,6 @@ export default { } `, - options: [ - { - tags: { - description: true, - }, - }, - ], }, { code: ` @@ -1226,13 +1213,6 @@ export default { } `, - options: [ - { - tags: { - description: true, - }, - }, - ], }, { code: ` @@ -1244,13 +1224,6 @@ export default { } `, - options: [ - { - tags: { - description: true, - }, - }, - ], }, { code: ` @@ -1456,7 +1429,7 @@ export default { { code: ` /** - * @description foo. + * @deprecated foo. */ function quux () { @@ -1481,13 +1454,6 @@ export default { } `, - options: [ - { - tags: { - summary: true, - }, - }, - ], }, { code: ` @@ -1697,5 +1663,19 @@ export default { ], parser: require.resolve('@typescript-eslint/parser'), }, + { + code: ` + /** + * @copyright + */ + function quux () { + } + `, + options: [ + { + nonemptyTags: false, + }, + ], + }, ], }; diff --git a/test/rules/assertions/matchName.js b/test/rules/assertions/matchName.js index 8c933da70..9cc7eec58 100644 --- a/test/rules/assertions/matchName.js +++ b/test/rules/assertions/matchName.js @@ -357,6 +357,31 @@ export default { */ `, }, + { + code: ` + /** + * @template + */ + `, + errors: [ + { + line: 3, + message: 'Only allowing names not matching `/^$/u` but found "".', + }, + ], + options: [ + { + match: [ + { + disallowName: '/^$/', + tags: [ + 'template', + ], + }, + ], + }, + ], + }, ], valid: [ { diff --git a/test/rules/assertions/noRestrictedSyntax.js b/test/rules/assertions/noRestrictedSyntax.js index d49395e75..459d1905e 100644 --- a/test/rules/assertions/noRestrictedSyntax.js +++ b/test/rules/assertions/noRestrictedSyntax.js @@ -1042,5 +1042,24 @@ export default { ], parser: require.resolve('@typescript-eslint/parser'), }, + { + code: ` + /** + * @private + */ + function quux () {} + `, + options: [ + { + contexts: [ + { + comment: 'JsdocBlock:not(JsdocBlock:has(JsdocTag[tag=/private|protected|public/]))', + context: 'any', + message: 'Access modifier tags must be present', + }, + ], + }, + ], + }, ], };