diff --git a/.README/README.md b/.README/README.md index 9fbf83804..ca2890676 100644 --- a/.README/README.md +++ b/.README/README.md @@ -461,6 +461,34 @@ your files' code which are of interest to check. However, in `eslint-plugin-jsdoc`, we also allow you to use these selectors to define additional contexts where you wish our own rules to be applied. +#### `contexts` format + +While at their simplest, these can be an array of string selectors, one can +also supply an object with `context` (in place of the string) and one of two +properties: + +1. For `require-jsdoc`, there is also a `inlineCommentBlock` property. See + that rule for details. +2. For other rules, there is a `comment` property which adds to the `context` + in requiring that the `comment` AST condition is also met, e.g., to + require that certain tags are present and/or or types and type operators + are in use. Note that this AST has not been standardized and should be + considered experimental. + In addition to being generally useful for precision in specifying contexts, + it is hoped that the ability to specify required tags on structures can + be used for requiring `@type` or other types for a minimalist yet adequate + specification of types which can be used to compile JavaScript+JSDoc (JJ) + to WebAssembly (e.g., by converting it to TypeSscript and then using + AssemblyScript to convert to WebAssembly). (It may be possible that one + will need to require types with certain structures beyond function + declarations and the like, as well as optionally requiring specification + of number types.) + +#### Discovering available AST definitions + To know all of the AST definitions one may target, it will depend on the [parser](https://eslint.org/docs/user-guide/configuring#specifying-parser) you are using with ESLint (e.g., `espree` is the default parser for ESLint, @@ -473,11 +501,13 @@ So you can look up a particular parser to see its rules, e.g., browse through the [ESTree docs](https://github.com/estree/estree) as used by Espree or see ESLint's [overview of the structure of AST](https://eslint.org/docs/developer-guide/working-with-custom-parsers#the-ast-specification). -However, it can sometimes be even more helpful to get an idea of ASt by just +However, it can sometimes be even more helpful to get an idea of AST by just providing some of your JavaScript to the wonderful [AST Explorer](https://astexplorer.net/) tool and see what AST is built out of your code. You can set the tool to the specific parser which you are using. +#### Uses/Tips for AST + And if you wish to introspect on the AST of code within your projects, you can use [eslint-plugin-query](https://github.com/brettz9/eslint-plugin-query). Though it also works as a plugin, you can use it with its own CLI, e.g., diff --git a/README.md b/README.md index 3f2597744..bf4acd3dd 100644 --- a/README.md +++ b/README.md @@ -532,6 +532,36 @@ your files' code which are of interest to check. However, in `eslint-plugin-jsdoc`, we also allow you to use these selectors to define additional contexts where you wish our own rules to be applied. + +#### contexts format + +While at their simplest, these can be an array of string selectors, one can +also supply an object with `context` (in place of the string) and one of two +properties: + +1. For `require-jsdoc`, there is also a `inlineCommentBlock` property. See + that rule for details. +2. For other rules, there is a `comment` property which adds to the `context` + in requiring that the `comment` AST condition is also met, e.g., to + require that certain tags are present and/or or types and type operators + are in use. Note that this AST has not been standardized and should be + considered experimental. + In addition to being generally useful for precision in specifying contexts, + it is hoped that the ability to specify required tags on structures can + be used for requiring `@type` or other types for a minimalist yet adequate + specification of types which can be used to compile JavaScript+JSDoc (JJ) + to WebAssembly (e.g., by converting it to TypeSscript and then using + AssemblyScript to convert to WebAssembly). (It may be possible that one + will need to require types with certain structures beyond function + declarations and the like, as well as optionally requiring specification + of number types.) + + +#### Discovering available AST definitions + To know all of the AST definitions one may target, it will depend on the [parser](https://eslint.org/docs/user-guide/configuring#specifying-parser) you are using with ESLint (e.g., `espree` is the default parser for ESLint, @@ -544,11 +574,14 @@ So you can look up a particular parser to see its rules, e.g., browse through the [ESTree docs](https://github.com/estree/estree) as used by Espree or see ESLint's [overview of the structure of AST](https://eslint.org/docs/developer-guide/working-with-custom-parsers#the-ast-specification). -However, it can sometimes be even more helpful to get an idea of ASt by just +However, it can sometimes be even more helpful to get an idea of AST by just providing some of your JavaScript to the wonderful [AST Explorer](https://astexplorer.net/) tool and see what AST is built out of your code. You can set the tool to the specific parser which you are using. + +#### Uses/Tips for AST + And if you wish to introspect on the AST of code within your projects, you can use [eslint-plugin-query](https://github.com/brettz9/eslint-plugin-query). Though it also works as a plugin, you can use it with its own CLI, e.g., @@ -9042,6 +9075,16 @@ class Foo { } // "jsdoc/require-description": ["error"|"warn", {"checkConstructors":false,"contexts":["MethodDefinition"]}] // Message: Missing JSDoc block description. + +/** + * @class + * @implements {Bar} + */ +class quux { + +} +// "jsdoc/require-description": ["error"|"warn", {"contexts":[{"comment":"JSDocBlock[postDelimiter=\"\"]:has(JSDocTag)","context":"ClassDeclaration"}],"descriptionStyle":"tag"}] +// Message: Missing JSDoc @description declaration. ```` The following patterns are not considered problems: diff --git a/package.json b/package.json index 054c45695..4e0eea810 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@es-joy/jsdoccomment": "^0.1.1", "comment-parser": "1.1.5", "debug": "^4.3.1", + "esquery": "^1.4.0", "jsdoctypeparser": "^9.0.0", "lodash": "^4.17.21", "regextras": "^0.7.1", diff --git a/src/iterateJsdoc.js b/src/iterateJsdoc.js index a5e81fcb5..5751c5b73 100644 --- a/src/iterateJsdoc.js +++ b/src/iterateJsdoc.js @@ -17,6 +17,10 @@ import { seedBlock, seedTokens, } from 'comment-parser/lib/util'; +import esquery from 'esquery'; +import { + parse as jsdoctypeParse, +} from 'jsdoctypeparser'; import _ from 'lodash'; import jsdocUtils from './jsdocUtils'; @@ -247,16 +251,15 @@ const getUtils = ( utils.getDescription = () => { const descriptions = []; let lastDescriptionLine; - if (jsdoc.source[0].tokens.description) { - descriptions.push(jsdoc.source[0].tokens.description); - } - jsdoc.source.slice(1).some(({tokens: {description, tag, end}}, idx) => { - if (tag || end) { - lastDescriptionLine = idx; + jsdoc.source.some(({tokens: {description, tag, end}}, idx) => { + if (idx && (tag || end)) { + lastDescriptionLine = idx - 1; return true; } - descriptions.push(description); + if (idx || description) { + descriptions.push(description); + } return false; }); @@ -705,12 +708,10 @@ const makeReport = (context, commentNode) => { */ const iterate = ( + indent, jsdoc, ruleConfig, context, lines, jsdocNode, node, settings, sourceCode, iterator, state, iteratingAll, ) => { - const sourceLine = lines[jsdocNode.loc.start.line - 1]; - const indent = sourceLine.charAt(0).repeat(jsdocNode.loc.start.column); - const jsdoc = parseComment(jsdocNode, indent); const report = makeReport(context, jsdocNode); const utils = getUtils( @@ -758,6 +759,14 @@ const iterate = ( }); }; +const getIndentAndJSDoc = function (lines, jsdocNode) { + const sourceLine = lines[jsdocNode.loc.start.line - 1]; + const indnt = sourceLine.charAt(0).repeat(jsdocNode.loc.start.column); + const jsdc = parseComment(jsdocNode, indnt); + + return [indnt, jsdc]; +}; + /** * Create an eslint rule that iterates over all JSDocs, regardless of whether * they are attached to a function-like node. @@ -778,7 +787,13 @@ const iterateAllJsdocs = (iterator, ruleConfig) => { if (!(/^\/\*\*\s/).test(sourceCode.getText(jsdocNode))) { return; } + + const [indent, jsdoc] = getIndentAndJSDoc( + lines, jsdocNode, + ); + iterate( + indent, jsdoc, ruleConfig, context, lines, jsdocNode, node, settings, sourceCode, iterator, state, true, @@ -886,6 +901,14 @@ export { parseComment, }; +const toCamelCase = (str) => { + return str.toLowerCase().replace(/^[a-z]/, (init) => { + return init.toUpperCase(); + }).replace(/_([a-z])/, (__, wordInit) => { + return wordInit.toUpperCase(); + }); +}; + /** * @param {JsdocVisitor} iterator * @param {{ @@ -934,29 +957,255 @@ export default function iterateJsdoc (iterator, ruleConfig) { if (!settings) { return {}; } + const {mode} = settings; const {lines} = sourceCode; - const checkJsdoc = (node) => { + const checkJsdoc = (info, handler, node) => { const jsdocNode = getJSDocComment(sourceCode, node, settings); if (!jsdocNode) { return; } + const [indent, jsdoc] = getIndentAndJSDoc( + lines, jsdocNode, + ); + + if ( + // Note, `handler` should already be bound in its first argument + // with these only to be called after the value of + // `comment` + handler && handler(jsdoc) === false + ) { + return; + } + iterate( + indent, jsdoc, ruleConfig, context, lines, jsdocNode, node, settings, sourceCode, iterator, ); }; if (ruleConfig.contextDefaults) { - return jsdocUtils.getContextObject(contexts, checkJsdoc); + return jsdocUtils.getContextObject( + contexts, + checkJsdoc, + (commentSelector, jsdoc) => { + const selector = esquery.parse(commentSelector); + + const {tokens: { + delimiter: delimiterRoot, + postDelimiter: postDelimiterRoot, + end: endRoot, + description: descriptionRoot, + }} = jsdoc.source[0]; + const obj = { + delimiter: delimiterRoot, + description: descriptionRoot, + + // This will be overwritten if there are other entries + end: endRoot, + + postDelimiter: postDelimiterRoot, + }; + const ast = { + type: 'JSDocBlock', + ...obj, + }; + + const tags = []; + let lastDescriptionLine; + let lastTagDescriptionHolder = false; + let lastTagTypeHolder = false; + jsdoc.source.slice(1).forEach((info, idx) => { + const {tokens} = info; + if (tokens.tag || tokens.end) { + if (lastDescriptionLine === undefined) { + lastDescriptionLine = idx; + } + if (tokens.end) { + ast.end = tokens.end; + } else { + const { + // eslint-disable-next-line no-unused-vars -- Discarding + end: ed, + ...tkns + } = tokens; + + let parsedType = null; + try { + parsedType = jsdoctypeParse(tokens.type, {mode}); + } catch { + // Ignore + } + + // Todo: See about getting jsdoctypeparser to make these + // changes; the AST might also be rethought to use + // fewer types and more properties + const sel = esquery.parse('*[type]'); + esquery.traverse(parsedType, sel, (node) => { + const {type} = node; + + node.type = `JSDocType${toCamelCase(type)}`; + }); + + const tag = { + ...tkns, + descriptionLines: [], + parsedType, + rawType: tokens.type, + type: 'JSDocTag', + typeLines: [], + }; + if (!lastTagDescriptionHolder) { + const { + delimiter, + description, + postDelimiter, + start, + } = tkns; + tag.descriptionLines = lastTagDescriptionHolder = [ + { + delimiter, + description, + postDelimiter, + start, + type: 'JSDocDescriptionLine', + }, + ]; + } + if (!lastTagTypeHolder) { + const { + delimiter, + type: rawType, + postDelimiter, + start, + } = tkns; + tag.typeLines = lastTagTypeHolder = [ + { + delimiter, + postDelimiter, + rawType, + start, + type: 'JSDocTypeLine', + }, + ]; + } + tags.push(tag); + } + + return; + + // Multi-line tag descriptions + } + + if (lastTagDescriptionHolder && tokens.description) { + const { + delimiter, + description, + postDelimiter, + start, + } = tokens; + lastTagDescriptionHolder.push( + { + delimiter, + description, + postDelimiter, + start, + type: 'JSDocDescriptionLine', + }, + ); + + return; + } + if (lastTagTypeHolder && tokens.type) { + const { + delimiter, + postDelimiter, + start, + type: rawType, + } = tokens; + lastTagDescriptionHolder.push( + { + delimiter, + postDelimiter, + rawType, + start, + type: 'JSDocTypeLine', + }, + ); + + return; + } + ast.description += '\n' + tokens.description; + }); + + ast.lastDescriptionLine = lastDescriptionLine; + ast.tags = tags; + + console.log('jsdoc', jsdoc); + console.log('ast', ast); + + /* eslint-disable sort-keys-fix/sort-keys-fix -- Keep in order */ + const typeVisitorKeys = { + NAME: [], + NAMED_PARAMETER: ['typeName'], + MEMBER: ['owner'], + UNION: ['left', 'right'], + INTERSECTION: ['left', 'right'], + VARIADIC: ['value'], + RECORD: ['entries'], + RECORD_ENTRY: ['value'], + TUPLE: ['entries'], + GENERIC: ['subject', 'objects'], + MODULE: ['value'], + OPTIONAL: ['value'], + NULLABLE: ['value'], + NOT_NULLABLE: ['value'], + FUNCTION: ['params', 'returns', 'this', 'new'], + ARROW: ['params', 'returns'], + ANY: [], + UNKNOWN: [], + INNER_MEMBER: ['owner'], + INSTANCE_MEMBER: ['owner'], + STRING_VALUE: [], + NUMBER_VALUE: [], + EXTERNAL: [], + FILE_PATH: [], + PARENTHESIS: ['value'], + TYPE_QUERY: ['name'], + KEY_QUERY: ['value'], + IMPORT: ['path'], + /* eslint-enable sort-keys-fix/sort-keys-fix -- Keep in order */ + }; + + const convertedTypeVisitorKeys = Object.entries( + typeVisitorKeys, + ).reduce((object, [key, value]) => { + object[`JSDocType${toCamelCase(key)}`] = value; + + return object; + }, {}); + + return esquery.matches(ast, selector, null, { + visitorKeys: { + ...convertedTypeVisitorKeys, + JSDocBlock: ['tags'], + JSDocDescriptionLine: [], + JSDocTag: ['descriptionLines', 'typeLines', 'parsedType'], + }, + }); + }, + ); } + const checkJsdocNoHandler = checkJsdoc.bind(null, null, null); + return { - ArrowFunctionExpression: checkJsdoc, - FunctionDeclaration: checkJsdoc, - FunctionExpression: checkJsdoc, + ArrowFunctionExpression: checkJsdocNoHandler, + FunctionDeclaration: checkJsdocNoHandler, + FunctionExpression: checkJsdocNoHandler, }; }, meta: ruleConfig.meta, diff --git a/src/jsdocUtils.js b/src/jsdocUtils.js index 6a92d9939..1d8acc7b3 100644 --- a/src/jsdocUtils.js +++ b/src/jsdocUtils.js @@ -1084,15 +1084,22 @@ const enforcedContexts = (context, defaultContexts) => { /** * @param {string[]} contexts * @param {Function} checkJsdoc + * @param {Function} handler */ -const getContextObject = (contexts, checkJsdoc) => { +const getContextObject = (contexts, checkJsdoc, handler) => { const properties = {}; contexts.forEach((prop) => { if (typeof prop === 'object') { - properties[prop.context] = checkJsdoc; + if (prop.comment) { + properties[prop.context] = checkJsdoc.bind( + null, null, handler.bind(null, prop.comment), + ); + } else { + properties[prop.context] = checkJsdoc.bind(null, null, null); + } } else { - properties[prop] = checkJsdoc; + properties[prop] = checkJsdoc.bind(null, null, null); } }); diff --git a/src/rules/implementsOnClasses.js b/src/rules/implementsOnClasses.js index d6c2cbdd0..7a794aee0 100644 --- a/src/rules/implementsOnClasses.js +++ b/src/rules/implementsOnClasses.js @@ -35,7 +35,23 @@ export default iterateJsdoc(({ properties: { contexts: { items: { - type: 'string', + anyOf: [ + { + type: 'string', + }, + { + additionalProperties: false, + properties: { + comment: { + type: 'string', + }, + context: { + type: 'string', + }, + }, + type: 'object', + }, + ], }, type: 'array', }, diff --git a/src/rules/matchDescription.js b/src/rules/matchDescription.js index e897db8f9..91f93ae07 100644 --- a/src/rules/matchDescription.js +++ b/src/rules/matchDescription.js @@ -93,7 +93,23 @@ export default iterateJsdoc(({ properties: { contexts: { items: { - type: 'string', + anyOf: [ + { + type: 'string', + }, + { + additionalProperties: false, + properties: { + comment: { + type: 'string', + }, + context: { + type: 'string', + }, + }, + type: 'object', + }, + ], }, type: 'array', }, diff --git a/src/rules/noDefaults.js b/src/rules/noDefaults.js index 3ae125472..7c4c15c22 100644 --- a/src/rules/noDefaults.js +++ b/src/rules/noDefaults.js @@ -46,7 +46,23 @@ export default iterateJsdoc(({ properties: { contexts: { items: { - type: 'string', + anyOf: [ + { + type: 'string', + }, + { + additionalProperties: false, + properties: { + comment: { + type: 'string', + }, + context: { + type: 'string', + }, + }, + type: 'object', + }, + ], }, type: 'array', }, diff --git a/src/rules/noTypes.js b/src/rules/noTypes.js index 3d5d763f4..a280f2120 100644 --- a/src/rules/noTypes.js +++ b/src/rules/noTypes.js @@ -35,7 +35,23 @@ export default iterateJsdoc(({ properties: { contexts: { items: { - type: 'string', + anyOf: [ + { + type: 'string', + }, + { + additionalProperties: false, + properties: { + comment: { + type: 'string', + }, + context: { + type: 'string', + }, + }, + type: 'object', + }, + ], }, type: 'array', }, diff --git a/src/rules/requireDescription.js b/src/rules/requireDescription.js index 3dc20b6d0..93b9947db 100644 --- a/src/rules/requireDescription.js +++ b/src/rules/requireDescription.js @@ -99,7 +99,23 @@ export default iterateJsdoc(({ }, contexts: { items: { - type: 'string', + anyOf: [ + { + type: 'string', + }, + { + additionalProperties: false, + properties: { + comment: { + type: 'string', + }, + context: { + type: 'string', + }, + }, + type: 'object', + }, + ], }, type: 'array', }, diff --git a/src/rules/requireExample.js b/src/rules/requireExample.js index c328d6da8..86d5bf2e4 100644 --- a/src/rules/requireExample.js +++ b/src/rules/requireExample.js @@ -68,7 +68,23 @@ export default iterateJsdoc(({ }, contexts: { items: { - type: 'string', + anyOf: [ + { + type: 'string', + }, + { + additionalProperties: false, + properties: { + comment: { + type: 'string', + }, + context: { + type: 'string', + }, + }, + type: 'object', + }, + ], }, type: 'array', }, diff --git a/src/rules/requireJsdoc.js b/src/rules/requireJsdoc.js index 45d628780..7a4a253d9 100644 --- a/src/rules/requireJsdoc.js +++ b/src/rules/requireJsdoc.js @@ -196,14 +196,15 @@ export default { publicOnly, exemptEmptyFunctions, exemptEmptyConstructors, enableFixer, } = getOptions(context); - const checkJsDoc = (node, isFunctionContext) => { + const checkJsDoc = (isFunctionContext, handler, node) => { const jsDocNode = getJSDocComment(sourceCode, node, settings); if (jsDocNode) { return; } - // For those who have options configured against ANY constructors (or setters or getters) being reported + // For those who have options configured against ANY constructors (or + // setters or getters) being reported if (jsdocUtils.exemptSpeciaMethods( {tags: []}, node, context, [OPTIONS_SCHEMA], )) { @@ -211,10 +212,12 @@ export default { } if ( - // Avoid reporting param-less, return-less functions (when `exemptEmptyFunctions` option is set) + // Avoid reporting param-less, return-less functions (when + // `exemptEmptyFunctions` option is set) exemptEmptyFunctions && isFunctionContext || - // Avoid reporting param-less, return-less constructor methods (when `exemptEmptyConstructors` option is set) + // Avoid reporting param-less, return-less constructor methods (when + // `exemptEmptyConstructors` option is set) exemptEmptyConstructors && jsdocUtils.isConstructor(node) ) { const functionParameterNames = jsdocUtils.getFunctionParameterNames(node); @@ -288,7 +291,10 @@ export default { }; return { - ...jsdocUtils.getContextObject(jsdocUtils.enforcedContexts(context, []), checkJsDoc), + ...jsdocUtils.getContextObject( + jsdocUtils.enforcedContexts(context, []), + checkJsDoc, + ), ArrowFunctionExpression (node) { if (!hasOption('ArrowFunctionExpression')) { return; @@ -298,7 +304,7 @@ export default { ['VariableDeclarator', 'AssignmentExpression', 'ExportDefaultDeclaration'].includes(node.parent.type) || ['Property', 'ObjectProperty', 'ClassProperty'].includes(node.parent.type) && node === node.parent.value ) { - checkJsDoc(node, true); + checkJsDoc(true, null, node); } }, @@ -307,7 +313,7 @@ export default { return; } - checkJsDoc(node); + checkJsDoc(false, null, node); }, ClassExpression (node) { @@ -315,7 +321,7 @@ export default { return; } - checkJsDoc(node); + checkJsDoc(false, null, node); }, FunctionDeclaration (node) { @@ -323,12 +329,12 @@ export default { return; } - checkJsDoc(node, true); + checkJsDoc(true, null, node); }, FunctionExpression (node) { if (hasOption('MethodDefinition') && node.parent.type === 'MethodDefinition') { - checkJsDoc(node, true); + checkJsDoc(true, null, node); return; } @@ -341,7 +347,7 @@ export default { ['VariableDeclarator', 'AssignmentExpression', 'ExportDefaultDeclaration'].includes(node.parent.type) || ['Property', 'ObjectProperty', 'ClassProperty'].includes(node.parent.type) && node === node.parent.value ) { - checkJsDoc(node, true); + checkJsDoc(true, null, node); } }, }; diff --git a/src/rules/requireParam.js b/src/rules/requireParam.js index 5cba97c15..7b76d0656 100644 --- a/src/rules/requireParam.js +++ b/src/rules/requireParam.js @@ -345,7 +345,23 @@ export default iterateJsdoc(({ }, contexts: { items: { - type: 'string', + anyOf: [ + { + type: 'string', + }, + { + additionalProperties: false, + properties: { + comment: { + type: 'string', + }, + context: { + type: 'string', + }, + }, + type: 'object', + }, + ], }, type: 'array', }, diff --git a/src/rules/requireParamDescription.js b/src/rules/requireParamDescription.js index 0cd154c96..37319f8f5 100644 --- a/src/rules/requireParamDescription.js +++ b/src/rules/requireParamDescription.js @@ -26,7 +26,23 @@ export default iterateJsdoc(({ properties: { contexts: { items: { - type: 'string', + anyOf: [ + { + type: 'string', + }, + { + additionalProperties: false, + properties: { + comment: { + type: 'string', + }, + context: { + type: 'string', + }, + }, + type: 'object', + }, + ], }, type: 'array', }, diff --git a/src/rules/requireParamName.js b/src/rules/requireParamName.js index 0bf05b2bf..1d1c32397 100644 --- a/src/rules/requireParamName.js +++ b/src/rules/requireParamName.js @@ -26,7 +26,23 @@ export default iterateJsdoc(({ properties: { contexts: { items: { - type: 'string', + anyOf: [ + { + type: 'string', + }, + { + additionalProperties: false, + properties: { + comment: { + type: 'string', + }, + context: { + type: 'string', + }, + }, + type: 'object', + }, + ], }, type: 'array', }, diff --git a/src/rules/requireParamType.js b/src/rules/requireParamType.js index 4bd58250d..78b583bd8 100644 --- a/src/rules/requireParamType.js +++ b/src/rules/requireParamType.js @@ -26,7 +26,23 @@ export default iterateJsdoc(({ properties: { contexts: { items: { - type: 'string', + anyOf: [ + { + type: 'string', + }, + { + additionalProperties: false, + properties: { + comment: { + type: 'string', + }, + context: { + type: 'string', + }, + }, + type: 'object', + }, + ], }, type: 'array', }, diff --git a/src/rules/requireReturns.js b/src/rules/requireReturns.js index e9558d20c..11d8dcb59 100644 --- a/src/rules/requireReturns.js +++ b/src/rules/requireReturns.js @@ -114,7 +114,23 @@ export default iterateJsdoc(({ }, contexts: { items: { - type: 'string', + anyOf: [ + { + type: 'string', + }, + { + additionalProperties: false, + properties: { + comment: { + type: 'string', + }, + context: { + type: 'string', + }, + }, + type: 'object', + }, + ], }, type: 'array', }, diff --git a/src/rules/requireReturnsDescription.js b/src/rules/requireReturnsDescription.js index 6e4116389..af7787918 100644 --- a/src/rules/requireReturnsDescription.js +++ b/src/rules/requireReturnsDescription.js @@ -28,7 +28,23 @@ export default iterateJsdoc(({ properties: { contexts: { items: { - type: 'string', + anyOf: [ + { + type: 'string', + }, + { + additionalProperties: false, + properties: { + comment: { + type: 'string', + }, + context: { + type: 'string', + }, + }, + type: 'object', + }, + ], }, type: 'array', }, diff --git a/src/rules/requireReturnsType.js b/src/rules/requireReturnsType.js index 8985c2d6c..ef443f968 100644 --- a/src/rules/requireReturnsType.js +++ b/src/rules/requireReturnsType.js @@ -22,7 +22,23 @@ export default iterateJsdoc(({ properties: { contexts: { items: { - type: 'string', + anyOf: [ + { + type: 'string', + }, + { + additionalProperties: false, + properties: { + comment: { + type: 'string', + }, + context: { + type: 'string', + }, + }, + type: 'object', + }, + ], }, type: 'array', }, diff --git a/src/rules/requireThrows.js b/src/rules/requireThrows.js index 9cd2db6bc..ffda0dfdc 100644 --- a/src/rules/requireThrows.js +++ b/src/rules/requireThrows.js @@ -70,7 +70,23 @@ export default iterateJsdoc(({ properties: { contexts: { items: { - type: 'string', + anyOf: [ + { + type: 'string', + }, + { + additionalProperties: false, + properties: { + comment: { + type: 'string', + }, + context: { + type: 'string', + }, + }, + type: 'object', + }, + ], }, type: 'array', }, diff --git a/src/rules/requireYields.js b/src/rules/requireYields.js index eb22096e3..adfe7d224 100644 --- a/src/rules/requireYields.js +++ b/src/rules/requireYields.js @@ -146,7 +146,23 @@ export default iterateJsdoc(({ properties: { contexts: { items: { - type: 'string', + anyOf: [ + { + type: 'string', + }, + { + additionalProperties: false, + properties: { + comment: { + type: 'string', + }, + context: { + type: 'string', + }, + }, + type: 'object', + }, + ], }, type: 'array', }, diff --git a/src/rules/requireYieldsCheck.js b/src/rules/requireYieldsCheck.js index 9ecc355f2..0b56dfdc4 100644 --- a/src/rules/requireYieldsCheck.js +++ b/src/rules/requireYieldsCheck.js @@ -118,7 +118,23 @@ export default iterateJsdoc(({ }, contexts: { items: { - type: 'string', + anyOf: [ + { + type: 'string', + }, + { + additionalProperties: false, + properties: { + comment: { + type: 'string', + }, + context: { + type: 'string', + }, + }, + type: 'object', + }, + ], }, type: 'array', }, diff --git a/test/rules/assertions/requireDescription.js b/test/rules/assertions/requireDescription.js index 4ce6d0b63..eb6fdcf87 100644 --- a/test/rules/assertions/requireDescription.js +++ b/test/rules/assertions/requireDescription.js @@ -516,6 +516,34 @@ export default { }, ], }, + { + code: ` + /** + * @class + * @implements {Bar} + */ + class quux { + + } + `, + errors: [ + { + message: 'Missing JSDoc @description declaration.', + }, + ], + options: [ + { + contexts: [ + { + // comment: 'JSDocBlock[whitespace=/\\s{4}/] > JSDocTag[name="class"]', + comment: 'JSDocBlock[postDelimiter=""]:has(JSDocTag)', + context: 'ClassDeclaration', + }, + ], + descriptionStyle: 'tag', + }, + ], + }, ], valid: [ {