From 866b71955f43b0796f3d7983b8242ad2d2984b98 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Mon, 8 Jul 2019 22:34:49 +0800 Subject: [PATCH 01/46] refactor: remove now unused `returns` as function option --- src/iterateJsdoc.js | 4 ++-- test/iterateJsdoc.js | 10 ---------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/iterateJsdoc.js b/src/iterateJsdoc.js index 1b59733c5..f6bfa4f64 100644 --- a/src/iterateJsdoc.js +++ b/src/iterateJsdoc.js @@ -348,8 +348,8 @@ export default function iterateJsdoc (iterator, ruleConfig) { if (!metaType || !['problem', 'suggestion', 'layout'].includes(metaType)) { throw new TypeError('Rule must include `meta.type` option (with value "problem", "suggestion", or "layout")'); } - if (typeof iterator !== 'function' && (!ruleConfig || typeof ruleConfig.returns !== 'function')) { - throw new TypeError('The iterator argument must be a function or an object with a `returns` method.'); + if (typeof iterator !== 'function') { + throw new TypeError('The iterator argument must be a function.'); } if (ruleConfig.iterateAllJsdocs) { diff --git a/test/iterateJsdoc.js b/test/iterateJsdoc.js index e0aa0dd5a..40124828b 100644 --- a/test/iterateJsdoc.js +++ b/test/iterateJsdoc.js @@ -53,16 +53,6 @@ describe('iterateJsdoc', () => { iterateJsdoc(() => {}, {meta: {type: 'suggestion'}}); }).to.not.throw(); }); - it('Does not throw with object and options', () => { - expect(() => { - iterateJsdoc(undefined, { - meta: {type: 'suggestion'}, - returns () { - return {}; - } - }); - }).to.not.throw(); - }); }); }); }); From aff7358e0e794f246d2b2148ce192413d66f469e Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Mon, 8 Jul 2019 23:05:10 +0800 Subject: [PATCH 02/46] fix(`check-param-names`): report proper line numbers (fixes #48) --- src/rules/checkParamNames.js | 19 +++++++++++-------- test/rules/assertions/checkParamNames.js | 10 ++++++++++ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/rules/checkParamNames.js b/src/rules/checkParamNames.js index 80355211a..2dfcfacec 100644 --- a/src/rules/checkParamNames.js +++ b/src/rules/checkParamNames.js @@ -10,10 +10,11 @@ const validateParameterNames = (targetTagName : string, functionParameterNames : }); return paramTags.some((tag, index) => { - if (paramTags.some((tg, idx) => { + const dupeTag = paramTags.find((tg, idx) => { return tg.name === tag.name && idx !== index; - })) { - report(`Duplicate @${targetTagName} "${tag.name}"`); + }); + if (dupeTag) { + report(`Duplicate @${targetTagName} "${tag.name}"`, null, dupeTag); return true; } @@ -52,15 +53,15 @@ const validateParameterNames = (targetTagName : string, functionParameterNames : }); }; -const validateParameterNamesDeep = (targetTagName : string, jsdocParameterNames : Array, report : Function) => { +const validateParameterNamesDeep = (targetTagName : string, jsdocParameterNames : Array, jsdoc, report : Function) => { let lastRealParameter; - return jsdocParameterNames.some((jsdocParameterName) => { + return jsdocParameterNames.some((jsdocParameterName, idx) => { const isPropertyPath = jsdocParameterName.includes('.'); if (isPropertyPath) { if (!lastRealParameter) { - report(`@${targetTagName} path declaration ("${jsdocParameterName}") appears before any real parameter.`); + report(`@${targetTagName} path declaration ("${jsdocParameterName}") appears before any real parameter.`, null, jsdoc.tags[idx]); return true; } @@ -74,7 +75,9 @@ const validateParameterNamesDeep = (targetTagName : string, jsdocParameterNames if (pathRootNodeName !== lastRealParameter) { report( `@${targetTagName} path declaration ("${jsdocParameterName}") root node name ("${pathRootNodeName}") ` + - `does not match previous real parameter name ("${lastRealParameter}").` + `does not match previous real parameter name ("${lastRealParameter}").`, + null, + jsdoc.tags[idx] ); return true; @@ -104,7 +107,7 @@ export default iterateJsdoc(({ return; } - validateParameterNamesDeep(targetTagName, jsdocParameterNamesDeep, report); + validateParameterNamesDeep(targetTagName, jsdocParameterNamesDeep, jsdoc, report); }, { meta: { type: 'suggestion' diff --git a/test/rules/assertions/checkParamNames.js b/test/rules/assertions/checkParamNames.js index 0c82e23f6..874d41ab5 100644 --- a/test/rules/assertions/checkParamNames.js +++ b/test/rules/assertions/checkParamNames.js @@ -33,6 +33,7 @@ export default { `, errors: [ { + line: 3, message: 'Expected @arg names to be "foo". Got "Foo".' } ], @@ -55,6 +56,7 @@ export default { `, errors: [ { + line: 3, message: 'Expected @param names to be "foo". Got "Foo".' } ] @@ -70,6 +72,7 @@ export default { `, errors: [ { + line: 3, message: '@param path declaration ("Foo.Bar") appears before any real parameter.' } ] @@ -86,6 +89,7 @@ export default { `, errors: [ { + line: 4, message: '@param path declaration ("Foo.Bar") root node name ("Foo") does not match previous real parameter name ("foo").' } ] @@ -103,6 +107,7 @@ export default { `, errors: [ { + line: 3, message: 'Expected @param names to be "bar, foo". Got "foo, bar".' } ] @@ -136,6 +141,7 @@ export default { `, errors: [ { + line: 4, message: 'Duplicate @param "foo"' } ] @@ -152,6 +158,7 @@ export default { `, errors: [ { + line: 4, message: 'Duplicate @param "foo"' } ] @@ -168,6 +175,7 @@ export default { `, errors: [ { + line: 4, message: 'Duplicate @param "foo"' } ] @@ -183,6 +191,7 @@ export default { `, errors: [ { + line: 4, message: 'Expected @param names to be "property". Got "prop".' } ], @@ -202,6 +211,7 @@ export default { `, errors: [ { + line: 3, message: 'Unexpected tag `@param`' } ], From 7400ee7ca62b5b5c98559ba0d4c5bf2e801d4bb8 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 9 Jul 2019 00:07:54 +0800 Subject: [PATCH 03/46] feat: bump jsdoctypeparser to 5.0.1 The update brings in: 1. feat: TypeScript key query (`keyof X`) types 2. feat: Preserve quote style: a. (single, double) for strings b. (single, double, or none) for `MemberName` (e.g., `owner.'Member'`), `RecordTypeExprEntryKey` (e.g., `{"keyOnly"}`), `FilePathExpr` (e.g., `module:"..."`), `ExternalNameExpr2` (e.g., `external:"..."`) 3. feat: changed `SyntaxError` export to `JSDocTypeSyntaxError` to avoid shadowing (not currently in use as such here) 4. feat: jsdoctypeparser binary is available (not currently in use here) 5. fix: allow optional `+` at beginning of decimal literals and optional `e` scientific notation at end, and decimal with no leading digit 6. fix: tighten unquoted `RecordTypeExprEntryKey` to disallow numbers followed by non-numbers (except as part of a decimal and/or scientific notation) 7. fix: allow quotes in `FilePathExpr` (for `module:"..."`) and `ExternalNameExpr` (for `external:"..."`) 8. fix: Disallow unescaped backslash before final quote and escaped backslash sequence before inside quote in `MemberName` (e.g., `owner."Mem.ber\\"`) and `RecordTypeExprEntryKey` (e.g., `{"key\\\\"Only"}`) 9. fix: Improvements in quoting/backslashes of strings 10. fix: accept `MemberName` in place of `NamepathExpr` ("external:..." should point to a (quotable) name, optionally with members, but not a name path with parenthesized literals, imports, etc.) 11. chore: update @types/node --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 90d927af8..fcb0570f4 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "debug": "^4.1.1", "escape-regex-string": "^1.0.6", "flat-map-polyfill": "^0.3.8", - "jsdoctypeparser": "4.0.0", + "jsdoctypeparser": "5.0.1", "lodash": "^4.17.11" }, "description": "JSDoc linting rules for ESLint.", From 76771cb5245a05c5f21b9f9a25951d5603c22ae7 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Mon, 8 Jul 2019 22:15:29 +0800 Subject: [PATCH 04/46] feat(`require-example`): add `contexts` array option (of AST types) to allow overriding enforced contexts (fixes #273) --- .README/rules/require-example.md | 26 ++++++++---- README.md | 54 +++++++++++++++++++++---- src/rules/requireExample.js | 7 ++++ test/rules/assertions/requireExample.js | 50 +++++++++++++++++++++++ 4 files changed, 121 insertions(+), 16 deletions(-) diff --git a/.README/rules/require-example.md b/.README/rules/require-example.md index 37723a793..e49cf0b0b 100644 --- a/.README/rules/require-example.md +++ b/.README/rules/require-example.md @@ -7,20 +7,30 @@ Requires that all functions have examples. #### Options -This rule has an object option: +This rule has an object option. -- `exemptedBy` - Array of tags (e.g., `['type']`) whose presence on the document - block avoids the need for an `@example`. Defaults to an empty array. +##### `exemptedBy` -- `avoidExampleOnConstructors` (default: false) - Set to `true` to avoid the - need for an example on a constructor (whether indicated as such by a - jsdoc tag or by being within an ES6 `class`). +Array of tags (e.g., `['type']`) whose presence on the document +block avoids the need for an `@example`. Defaults to an empty array. + +##### `avoidExampleOnConstructors` + +Set to `true` to avoid the need for an example on a constructor (whether +indicated as such by a jsdoc tag or by being within an ES6 `class`). +Defaults to `false`. + +##### `contexts` + +Set this to an array of strings representing the AST context +where you wish the rule to be applied (e.g., `ClassDeclaration` for ES6 classes). +Overrides the default contexts (see below). ||| |---|---| -|Context|`ArrowFunctionExpression`, `FunctionDeclaration`, `FunctionExpression`| +|Context|`ArrowFunctionExpression`, `FunctionDeclaration`, `FunctionExpression`; others when `contexts` option enabled| |Tags|`example`| -|Options|`exemptedBy`, `avoidExampleOnConstructors`| +|Options|`exemptedBy`, `avoidExampleOnConstructors`, `contexts`| |Settings|`overrideReplacesDocs`, `augmentsExtendsReplacesDocs`, `implementsReplacesDocs`| diff --git a/README.md b/README.md index a3756b2ca..805753b46 100644 --- a/README.md +++ b/README.md @@ -4056,20 +4056,33 @@ Requires that all functions have examples. #### Options -This rule has an object option: +This rule has an object option. -- `exemptedBy` - Array of tags (e.g., `['type']`) whose presence on the document - block avoids the need for an `@example`. Defaults to an empty array. + +##### exemptedBy + +Array of tags (e.g., `['type']`) whose presence on the document +block avoids the need for an `@example`. Defaults to an empty array. + + +##### avoidExampleOnConstructors -- `avoidExampleOnConstructors` (default: false) - Set to `true` to avoid the - need for an example on a constructor (whether indicated as such by a - jsdoc tag or by being within an ES6 `class`). +Set to `true` to avoid the need for an example on a constructor (whether +indicated as such by a jsdoc tag or by being within an ES6 `class`). +Defaults to `false`. + + +##### contexts + +Set this to an array of strings representing the AST context +where you wish the rule to be applied (e.g., `ClassDeclaration` for ES6 classes). +Overrides the default contexts (see below). ||| |---|---| -|Context|`ArrowFunctionExpression`, `FunctionDeclaration`, `FunctionExpression`| +|Context|`ArrowFunctionExpression`, `FunctionDeclaration`, `FunctionExpression`; others when `contexts` option enabled| |Tags|`example`| -|Options|`exemptedBy`, `avoidExampleOnConstructors`| +|Options|`exemptedBy`, `avoidExampleOnConstructors`, `contexts`| |Settings|`overrideReplacesDocs`, `augmentsExtendsReplacesDocs`, `implementsReplacesDocs`| The following patterns are considered problems: @@ -4116,6 +4129,15 @@ function quux () { } // Message: Missing JSDoc @example description. + +/** + * + */ +class quux { + +} +// Options: [{"contexts":["ClassDeclaration"]}] +// Message: Missing JSDoc @example declaration. ```` The following patterns are not considered problems: @@ -4189,6 +4211,22 @@ function quux () { } // Options: [{"exemptedBy":["type"]}] + +/** + * @example Some example code + */ +class quux { + +} +// Options: [{"contexts":["ClassDeclaration"]}] + +/** + * + */ +function quux () { + +} +// Options: [{"contexts":["ClassDeclaration"]}] ```` diff --git a/src/rules/requireExample.js b/src/rules/requireExample.js index aa157d8bb..819ea695c 100644 --- a/src/rules/requireExample.js +++ b/src/rules/requireExample.js @@ -46,6 +46,7 @@ export default iterateJsdoc(({ } }); }, { + contextDefaults: true, meta: { schema: [ { @@ -55,6 +56,12 @@ export default iterateJsdoc(({ default: false, type: 'boolean' }, + contexts: { + items: { + type: 'string' + }, + type: 'array' + }, exemptedBy: { items: { type: 'string' diff --git a/test/rules/assertions/requireExample.js b/test/rules/assertions/requireExample.js index bef750f5e..45f7454c4 100644 --- a/test/rules/assertions/requireExample.js +++ b/test/rules/assertions/requireExample.js @@ -83,6 +83,26 @@ export default { message: 'Missing JSDoc @example description.' } ] + }, + { + code: ` + /** + * + */ + class quux { + + } + `, + errors: [ + { + message: 'Missing JSDoc @example declaration.' + } + ], + options: [ + { + contexts: ['ClassDeclaration'] + } + ] } ], valid: [ @@ -188,6 +208,36 @@ export default { exemptedBy: ['type'] } ] + }, + { + code: ` + /** + * @example Some example code + */ + class quux { + + } + `, + options: [ + { + contexts: ['ClassDeclaration'] + } + ] + }, + { + code: ` + /** + * + */ + function quux () { + + } + `, + options: [ + { + contexts: ['ClassDeclaration'] + } + ] } ] }; From 9fe498909f7a2b5a005bc3fa993da0dd610582d0 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Mon, 8 Jul 2019 22:27:01 +0800 Subject: [PATCH 05/46] feat(`check-examples`): apply to all contexts --- .README/rules/check-examples.md | 2 +- README.md | 22 +++++++++++- src/rules/checkExamples.js | 9 ++--- test/rules/assertions/checkExamples.js | 46 ++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 9 deletions(-) diff --git a/.README/rules/check-examples.md b/.README/rules/check-examples.md index d038624ec..49d4818bf 100644 --- a/.README/rules/check-examples.md +++ b/.README/rules/check-examples.md @@ -98,7 +98,7 @@ decreasing precedence: ||| |---|---| -|Context|`ArrowFunctionExpression`, `ClassDeclaration`, `FunctionDeclaration`, `FunctionExpression`| +|Context|everywhere| |Tags|`example`| |Options| *See above* | diff --git a/README.md b/README.md index a3756b2ca..13326714b 100644 --- a/README.md +++ b/README.md @@ -566,7 +566,7 @@ decreasing precedence: ||| |---|---| -|Context|`ArrowFunctionExpression`, `ClassDeclaration`, `FunctionDeclaration`, `FunctionExpression`| +|Context|everywhere| |Tags|`example`| |Options| *See above* | @@ -612,6 +612,16 @@ function quux () { // Options: [{"baseConfig":{"rules":{"semi":["error","never"]}},"eslintrcForExamples":false,"exampleCodeRegex":"```js ([\\s\\S]*)```"}] // Message: @example error (semi): Extra semicolon. +/** + * @example + * ```js alert('hello'); ``` + */ +var quux = { + +}; +// Options: [{"baseConfig":{"rules":{"semi":["error","never"]}},"eslintrcForExamples":false,"exampleCodeRegex":"```js ([\\s\\S]*)```"}] +// Message: @example error (semi): Extra semicolon. + /** * @example ``` * js alert('hello'); ``` @@ -797,6 +807,16 @@ function quux () {} */ function quux () {} // Options: [{"allowInlineConfig":true,"baseConfig":{"rules":{"semi":["error","always"]}},"eslintrcForExamples":false,"noDefaultExampleRules":true}] + +/** + * @example ```js + alert('hello') + ``` + */ +var quux = { + +}; +// Options: [{"baseConfig":{"rules":{"semi":["error","never"]}},"eslintrcForExamples":false,"exampleCodeRegex":"```js([\\s\\S]*)```"}] ```` diff --git a/src/rules/checkExamples.js b/src/rules/checkExamples.js index f735160cd..005783b88 100644 --- a/src/rules/checkExamples.js +++ b/src/rules/checkExamples.js @@ -231,6 +231,7 @@ export default iterateJsdoc(({ }); }); }, { + iterateAllJsdocs: true, meta: { schema: [ { @@ -276,11 +277,5 @@ export default iterateJsdoc(({ } ], type: 'suggestion' - }, - returns: [ - 'ArrowFunctionExpression', - 'ClassDeclaration', - 'FunctionDeclaration', - 'FunctionExpression' - ] + } }); diff --git a/test/rules/assertions/checkExamples.js b/test/rules/assertions/checkExamples.js index d6bd7138c..ab5babb25 100644 --- a/test/rules/assertions/checkExamples.js +++ b/test/rules/assertions/checkExamples.js @@ -107,6 +107,31 @@ export default { exampleCodeRegex: '```js ([\\s\\S]*)```' }] }, + { + code: ` + /** + * @example + * \`\`\`js alert('hello'); \`\`\` + */ + var quux = { + + }; + `, + errors: [ + { + message: '@example error (semi): Extra semicolon.' + } + ], + options: [{ + baseConfig: { + rules: { + semi: ['error', 'never'] + } + }, + eslintrcForExamples: false, + exampleCodeRegex: '```js ([\\s\\S]*)```' + }] + }, { code: ` /** @@ -544,6 +569,27 @@ export default { eslintrcForExamples: false, noDefaultExampleRules: true }] + }, + { + code: ` + /** + * @example \`\`\`js + alert('hello') + \`\`\` + */ + var quux = { + + }; + `, + options: [{ + baseConfig: { + rules: { + semi: ['error', 'never'] + } + }, + eslintrcForExamples: false, + exampleCodeRegex: '```js([\\s\\S]*)```' + }] } ] }; From 7722f556ee0aa5e84e8720e5f7069f66d8b5809f Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 9 Jul 2019 12:30:13 +0800 Subject: [PATCH 06/46] chore: update devDeps, @babel/preset-env, typescript --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index fcb0570f4..4cb0f6f8d 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "@babel/core": "^7.5.0", "@babel/node": "^7.5.0", "@babel/plugin-transform-flow-strip-types": "^7.4.4", - "@babel/preset-env": "^7.5.0", + "@babel/preset-env": "^7.5.2", "@babel/register": "^7.4.4", "@typescript-eslint/parser": "^1.11.0", "babel-eslint": "^10.0.2", @@ -33,7 +33,7 @@ "mocha": "^6.1.4", "nyc": "^14.1.1", "semantic-release": "^15.13.18", - "typescript": "^3.5.2" + "typescript": "^3.5.3" }, "engines": { "node": ">=6" From 456206e0c548c0125ca220717d9ed14daf78109e Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 9 Jul 2019 12:53:55 +0800 Subject: [PATCH 07/46] fix(require-description-complete-sentence): let fixer work with question and exclamation marks at sentence end (fixes part of #16) --- README.md | 8 ++++++ package.json | 3 ++- .../requireDescriptionCompleteSentence.js | 26 +++++++++++-------- .../requireDescriptionCompleteSentence.js | 23 ++++++++++++++++ 4 files changed, 48 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 7fbdf9182..4b6178d32 100644 --- a/README.md +++ b/README.md @@ -3542,6 +3542,14 @@ function quux () { } // Message: Sentence should start with an uppercase character. +/** + * foo? + */ +function quux () { + +} +// Message: Sentence should start with an uppercase character. + /** * @description foo. */ diff --git a/package.json b/package.json index 4cb0f6f8d..b00fc544e 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "escape-regex-string": "^1.0.6", "flat-map-polyfill": "^0.3.8", "jsdoctypeparser": "5.0.1", - "lodash": "^4.17.11" + "lodash": "^4.17.11", + "regextras": "^0.6.1" }, "description": "JSDoc linting rules for ESLint.", "devDependencies": { diff --git a/src/rules/requireDescriptionCompleteSentence.js b/src/rules/requireDescriptionCompleteSentence.js index bf7f759ac..d9828b966 100644 --- a/src/rules/requireDescriptionCompleteSentence.js +++ b/src/rules/requireDescriptionCompleteSentence.js @@ -1,4 +1,5 @@ import _ from 'lodash'; +import {RegExtras} from 'regextras/dist/main-umd'; import iterateJsdoc from '../iterateJsdoc'; const extractParagraphs = (text) => { @@ -6,20 +7,23 @@ const extractParagraphs = (text) => { }; const extractSentences = (text) => { - return text + const txt = text // Remove all {} tags. - .replace(/\{[\s\S]*?\}\s*/g, '') - .split(/[.?!](?:\s+|$)/) + .replace(/\{[\s\S]*?\}\s*/g, ''); + + const sentenceEndGrouping = /([.?!])(?:\s+|$)/; + const puncts = RegExtras(sentenceEndGrouping).map(txt, (punct) => { + return punct; + }); - // Ignore sentences with only whitespaces. - .filter((sentence) => { - return !/^\s*$/.test(sentence); - }) + return txt + + .split(/[.?!](?:\s+|$)/) // Re-add the dot. - .map((sentence) => { - return `${sentence}.`; + .map((sentence, idx) => { + return /^\s*$/.test(sentence) ? sentence : `${sentence}${puncts[idx] || ''}`; }); }; @@ -67,7 +71,7 @@ const validateDescription = (description, report, jsdocNode, sourceCode, tag) => } for (const sentence of sentences.filter((sentence_) => { - return !isCapitalized(sentence_); + return !(/^\s*$/).test(sentence_) && !isCapitalized(sentence_); })) { const beginning = sentence.split('\n')[0]; @@ -86,7 +90,7 @@ const validateDescription = (description, report, jsdocNode, sourceCode, tag) => }; if (sentences.some((sentence) => { - return !isCapitalized(sentence); + return !(/^\s*$/).test(sentence) && !isCapitalized(sentence); })) { report('Sentence should start with an uppercase character.', fix); } diff --git a/test/rules/assertions/requireDescriptionCompleteSentence.js b/test/rules/assertions/requireDescriptionCompleteSentence.js index e61b5438a..3b994a741 100644 --- a/test/rules/assertions/requireDescriptionCompleteSentence.js +++ b/test/rules/assertions/requireDescriptionCompleteSentence.js @@ -23,6 +23,29 @@ export default { } ` }, + { + code: ` + /** + * foo? + */ + function quux () { + + } + `, + errors: [ + { + message: 'Sentence should start with an uppercase character.' + } + ], + output: ` + /** + * Foo? + */ + function quux () { + + } + ` + }, { code: ` /** From bee3779056ea309205193fae2bb5c2524d26ee3d Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 9 Jul 2019 13:07:25 +0800 Subject: [PATCH 08/46] testing(require-description-complete-sentence): add test cases for Markdown --- README.md | 46 +++++++++++ .../requireDescriptionCompleteSentence.js | 76 +++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/README.md b/README.md index 4b6178d32..038ca7ea2 100644 --- a/README.md +++ b/README.md @@ -3566,6 +3566,14 @@ function quux () { } // Message: Sentence must end with a period. +/** + * `foo` is a variable + */ +function quux () { + +} +// Message: Sentence must end with a period. + /** * Foo. * @@ -3807,6 +3815,44 @@ function quux () { */ function quux () { +} + +/** + * `foo` is a variable. + */ +function quux () { + +} + +/** + * Foo. + * + * `foo`. + */ +function quux () { + +} + +/** + * @param foo - `bar`. + */ +function quux () { + +} + +/** + * @returns {number} `foo`. + */ +function quux () { + +} + +/** + * Foo + * `bar`. + */ +function quux () { + } ```` diff --git a/test/rules/assertions/requireDescriptionCompleteSentence.js b/test/rules/assertions/requireDescriptionCompleteSentence.js index 3b994a741..da0a992c7 100644 --- a/test/rules/assertions/requireDescriptionCompleteSentence.js +++ b/test/rules/assertions/requireDescriptionCompleteSentence.js @@ -92,6 +92,29 @@ export default { } ` }, + { + code: ` + /** + * \`foo\` is a variable + */ + function quux () { + + } + `, + errors: [ + { + message: 'Sentence must end with a period.' + } + ], + output: ` + /** + * \`foo\` is a variable. + */ + function quux () { + + } + ` + }, { code: ` /** @@ -607,6 +630,59 @@ export default { */ function quux () { + } + ` + }, + { + code: ` + /** + * \`foo\` is a variable. + */ + function quux () { + + } + ` + }, + { + code: ` + /** + * Foo. + * + * \`foo\`. + */ + function quux () { + + } + ` + }, + { + code: ` + /** + * @param foo - \`bar\`. + */ + function quux () { + + } + ` + }, + { + code: ` + /** + * @returns {number} \`foo\`. + */ + function quux () { + + } + ` + }, + { + code: ` + /** + * Foo + * \`bar\`. + */ + function quux () { + } ` } From 37081d2ba83d182bf295828d5428bf17d6605eb3 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 9 Jul 2019 00:03:25 +0800 Subject: [PATCH 09/46] fix(`require-description-complete-sentence`): report proper line numbers (fixes #160) --- .../requireDescriptionCompleteSentence.js | 32 ++++++++++++------- .../requireDescriptionCompleteSentence.js | 22 +++++++++++++ 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/src/rules/requireDescriptionCompleteSentence.js b/src/rules/requireDescriptionCompleteSentence.js index d9828b966..629bec052 100644 --- a/src/rules/requireDescriptionCompleteSentence.js +++ b/src/rules/requireDescriptionCompleteSentence.js @@ -51,14 +51,14 @@ const capitalize = (str) => { return str.charAt(0).toUpperCase() + str.slice(1); }; -const validateDescription = (description, report, jsdocNode, sourceCode, tag) => { +const validateDescription = (description, reportOrig, jsdocNode, sourceCode, tag) => { if (!description) { return false; } const paragraphs = extractParagraphs(description); - return paragraphs.some((paragraph) => { + return paragraphs.some((paragraph, parIdx) => { const sentences = extractSentences(paragraph); const fix = (fixer) => { @@ -75,8 +75,8 @@ const validateDescription = (description, report, jsdocNode, sourceCode, tag) => })) { const beginning = sentence.split('\n')[0]; - if (tag) { - const reg = new RegExp(`(@${_.escapeRegExp(tag)}.*)${_.escapeRegExp(beginning)}`); + if (tag.tag) { + const reg = new RegExp(`(@${_.escapeRegExp(tag.tag)}.*)${_.escapeRegExp(beginning)}`); text = text.replace(reg, ($0, $1) => { return $1 + capitalize(beginning); @@ -89,20 +89,28 @@ const validateDescription = (description, report, jsdocNode, sourceCode, tag) => return fixer.replaceText(jsdocNode, text); }; + const report = (msg, fixer, tagObj) => { + tagObj.line += parIdx * 2; + + // Avoid errors if old column doesn't exist here + tagObj.column = 0; + reportOrig(msg, fixer, tagObj); + }; + if (sentences.some((sentence) => { return !(/^\s*$/).test(sentence) && !isCapitalized(sentence); })) { - report('Sentence should start with an uppercase character.', fix); + report('Sentence should start with an uppercase character.', fix, tag); } if (!/[.!?]$/.test(paragraph)) { - report('Sentence must end with a period.', fix); + report('Sentence must end with a period.', fix, tag); return true; } if (!isNewLinePrecededByAPeriod(paragraph)) { - report('A line of text is started with an uppercase character, but preceding line does not end the sentence.'); + report('A line of text is started with an uppercase character, but preceding line does not end the sentence.', null, tag); return true; } @@ -119,14 +127,16 @@ export default iterateJsdoc(({ utils }) => { if (!jsdoc.tags || - validateDescription(jsdoc.description, report, jsdocNode, sourceCode) + validateDescription(jsdoc.description, report, jsdocNode, sourceCode, { + line: jsdoc.line + 1 + }) ) { return; } - utils.forEachPreferredTag('description', (matchingJsdocTag, targetTagName) => { + utils.forEachPreferredTag('description', (matchingJsdocTag) => { const description = `${matchingJsdocTag.name} ${matchingJsdocTag.description}`.trim(); - validateDescription(description, report, jsdocNode, sourceCode, targetTagName); + validateDescription(description, report, jsdocNode, sourceCode, matchingJsdocTag); }); const tags = jsdoc.tags.filter((tag) => { @@ -136,7 +146,7 @@ export default iterateJsdoc(({ tags.some((tag) => { const description = _.trimStart(tag.description, '- '); - return validateDescription(description, report, jsdocNode, sourceCode, tag.tag); + return validateDescription(description, report, jsdocNode, sourceCode, tag); }); }, { iterateAllJsdocs: true, diff --git a/test/rules/assertions/requireDescriptionCompleteSentence.js b/test/rules/assertions/requireDescriptionCompleteSentence.js index da0a992c7..4d7a34b74 100644 --- a/test/rules/assertions/requireDescriptionCompleteSentence.js +++ b/test/rules/assertions/requireDescriptionCompleteSentence.js @@ -11,6 +11,7 @@ export default { `, errors: [ { + line: 3, message: 'Sentence should start with an uppercase character.' } ], @@ -57,6 +58,7 @@ export default { `, errors: [ { + line: 3, message: 'Sentence should start with an uppercase character.' } ], @@ -80,6 +82,7 @@ export default { `, errors: [ { + line: 3, message: 'Sentence must end with a period.' } ], @@ -128,6 +131,7 @@ export default { `, errors: [ { + line: 5, message: 'Sentence should start with an uppercase character.' } ], @@ -153,6 +157,7 @@ export default { `, errors: [ { + line: 3, message: 'Sentence should start with an uppercase character.' } ], @@ -176,6 +181,7 @@ export default { `, errors: [ { + line: 3, message: 'Sentence must end with a period.' } ], @@ -200,6 +206,7 @@ export default { `, errors: [ { + line: 3, message: 'A line of text is started with an uppercase character, but preceding line does not end the sentence.' } ] @@ -217,6 +224,7 @@ export default { `, errors: [ { + line: 5, message: 'Sentence should start with an uppercase character.' } ], @@ -244,9 +252,11 @@ export default { `, errors: [ { + line: 5, message: 'Sentence should start with an uppercase character.' }, { + line: 5, message: 'Sentence must end with a period.' } ], @@ -272,9 +282,11 @@ export default { `, errors: [ { + line: 3, message: 'Sentence should start with an uppercase character.' }, { + line: 3, message: 'Sentence must end with a period.' } ], @@ -300,9 +312,11 @@ export default { `, errors: [ { + line: 5, message: 'Sentence should start with an uppercase character.' }, { + line: 5, message: 'Sentence must end with a period.' } ], @@ -330,6 +344,7 @@ export default { `, errors: [ { + line: 5, message: 'Sentence should start with an uppercase character.' } ], @@ -360,6 +375,7 @@ export default { `, errors: [ { + line: 3, message: 'Sentence should start with an uppercase character.' } ], @@ -388,6 +404,7 @@ export default { `, errors: [ { + line: 3, message: 'Sentence must end with a period.' } ], @@ -411,6 +428,7 @@ export default { `, errors: [ { + line: 3, message: 'Sentence must end with a period.' } ], @@ -434,9 +452,11 @@ export default { `, errors: [ { + line: 3, message: 'Sentence should start with an uppercase character.' }, { + line: 3, message: 'Sentence must end with a period.' } ], @@ -462,9 +482,11 @@ export default { `, errors: [ { + line: 5, message: 'Sentence should start with an uppercase character.' }, { + line: 5, message: 'Sentence must end with a period.' } ], From 4049db1ea57adceae8ceb8ce38e212c8851711c7 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 10 Jul 2019 08:21:57 +0800 Subject: [PATCH 10/46] fix(check-syntax, implements-on-classes): report error line numbers --- src/rules/checkSyntax.js | 2 +- src/rules/implementsOnClasses.js | 6 +++--- test/rules/assertions/checkSyntax.js | 1 + test/rules/assertions/implementsOnClasses.js | 1 + 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/rules/checkSyntax.js b/src/rules/checkSyntax.js index 6f9527959..b42b9a9a3 100644 --- a/src/rules/checkSyntax.js +++ b/src/rules/checkSyntax.js @@ -10,7 +10,7 @@ export default iterateJsdoc(({ for (const tag of jsdoc.tags) { if (tag.type.slice(-1) === '=') { - report('Syntax should not be Google Closure Compiler style.'); + report('Syntax should not be Google Closure Compiler style.', null, tag); break; } } diff --git a/src/rules/implementsOnClasses.js b/src/rules/implementsOnClasses.js index a89546776..2d41126df 100644 --- a/src/rules/implementsOnClasses.js +++ b/src/rules/implementsOnClasses.js @@ -14,9 +14,9 @@ export default iterateJsdoc(({ return; } - if (utils.hasTag('implements')) { - report('@implements used on a non-constructor function'); - } + utils.forEachPreferredTag('implements', (tag) => { + report('@implements used on a non-constructor function', null, tag); + }); }, { meta: { type: 'suggestion' diff --git a/test/rules/assertions/checkSyntax.js b/test/rules/assertions/checkSyntax.js index 7285eb648..87df4514e 100644 --- a/test/rules/assertions/checkSyntax.js +++ b/test/rules/assertions/checkSyntax.js @@ -11,6 +11,7 @@ export default { `, errors: [ { + line: 3, message: 'Syntax should not be Google Closure Compiler style.' } ] diff --git a/test/rules/assertions/implementsOnClasses.js b/test/rules/assertions/implementsOnClasses.js index c3d594b61..af119af2a 100644 --- a/test/rules/assertions/implementsOnClasses.js +++ b/test/rules/assertions/implementsOnClasses.js @@ -11,6 +11,7 @@ export default { `, errors: [ { + line: 3, message: '@implements used on a non-constructor function' } ] From 8f8431ddbfcf15b7a495c4ed524559998067c45b Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 10 Jul 2019 09:21:12 +0800 Subject: [PATCH 11/46] fix(check-alignment, check-indentation, newline-after-description): report error line numbers --- src/rules/checkAlignment.js | 13 +++++++---- src/rules/checkIndentation.js | 7 ++++-- src/rules/newlineAfterDescription.js | 22 ++++++++++--------- test/rules/assertions/checkAlignment.js | 15 +++++++++++-- test/rules/assertions/checkIndentation.js | 2 ++ .../assertions/newlineAfterDescription.js | 3 +++ 6 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/rules/checkAlignment.js b/src/rules/checkAlignment.js index 343264a2f..2b7b7db87 100644 --- a/src/rules/checkAlignment.js +++ b/src/rules/checkAlignment.js @@ -31,12 +31,17 @@ export default iterateJsdoc(({ return fixer.replaceText(jsdocNode, replacement); }; - for (const line of sourceLines) { + sourceLines.some((line, lineNum) => { if (line.length !== indentLevel) { - report('Expected JSDoc block to be aligned.', fix); - break; + report('Expected JSDoc block to be aligned.', fix, { + line: lineNum + 1 + }); + + return true; } - } + + return false; + }); }, { iterateAllJsdocs: true, meta: { diff --git a/src/rules/checkIndentation.js b/src/rules/checkIndentation.js index 732e61f07..fccdffaf2 100644 --- a/src/rules/checkIndentation.js +++ b/src/rules/checkIndentation.js @@ -5,11 +5,14 @@ export default iterateJsdoc(({ jsdocNode, report }) => { - const reg = new RegExp(/^[ \t]+\*[ \t]{2}/m); + const reg = new RegExp(/^[ \t]+\*[ \t]{2}/gm); const text = sourceCode.getText(jsdocNode); if (reg.test(text)) { - report('There must be no indentation.'); + const lineBreaks = text.slice(0, reg.lastIndex).match(/\n/g) || []; + report('There must be no indentation.', null, { + line: lineBreaks.length + }); } }, { iterateAllJsdocs: true, diff --git a/src/rules/newlineAfterDescription.js b/src/rules/newlineAfterDescription.js index 6d8960ea5..938db4b57 100644 --- a/src/rules/newlineAfterDescription.js +++ b/src/rules/newlineAfterDescription.js @@ -28,29 +28,31 @@ export default iterateJsdoc(({ if (always) { if (!descriptionEndsWithANewline) { + const sourceLines = sourceCode.getText(jsdocNode).split('\n'); + const lastDescriptionLine = _.findLastIndex(sourceLines, (line) => { + return line.includes(_.last(jsdoc.description.split('\n'))); + }); report('There must be a newline after the description of the JSDoc block.', (fixer) => { - const sourceLines = sourceCode.getText(jsdocNode).split('\n'); - const lastDescriptionLine = _.findLastIndex(sourceLines, (line) => { - return line.includes(_.last(jsdoc.description.split('\n'))); - }); - // Add the new line sourceLines.splice(lastDescriptionLine + 1, 0, `${indent} *`); return fixer.replaceText(jsdocNode, sourceLines.join('\n')); + }, { + line: lastDescriptionLine }); } } else if (descriptionEndsWithANewline) { + const sourceLines = sourceCode.getText(jsdocNode).split('\n'); + const lastDescriptionLine = _.findLastIndex(sourceLines, (line) => { + return line.includes(_.last(jsdoc.description.split('\n'))); + }); report('There must be no newline after the description of the JSDoc block.', (fixer) => { - const sourceLines = sourceCode.getText(jsdocNode).split('\n'); - const lastDescriptionLine = _.findLastIndex(sourceLines, (line) => { - return line.includes(_.last(jsdoc.description.split('\n'))); - }); - // Remove the extra line sourceLines.splice(lastDescriptionLine + 1, 1); return fixer.replaceText(jsdocNode, sourceLines.join('\n')); + }, { + line: lastDescriptionLine + 1 }); } }, { diff --git a/test/rules/assertions/checkAlignment.js b/test/rules/assertions/checkAlignment.js index 3f3a58d62..73c22e145 100644 --- a/test/rules/assertions/checkAlignment.js +++ b/test/rules/assertions/checkAlignment.js @@ -11,6 +11,7 @@ export default { `, errors: [ { + line: 3, message: 'Expected JSDoc block to be aligned.' } ], @@ -34,6 +35,7 @@ export default { `, errors: [ { + line: 4, message: 'Expected JSDoc block to be aligned.' } ], @@ -57,6 +59,7 @@ export default { `, errors: [ { + line: 3, message: 'Expected JSDoc block to be aligned.' } ], @@ -80,6 +83,7 @@ export default { `, errors: [ { + line: 4, message: 'Expected JSDoc block to be aligned.' } ], @@ -103,6 +107,7 @@ export default { `, errors: [ { + line: 3, message: 'Expected JSDoc block to be aligned.' } ], @@ -122,7 +127,10 @@ export default { */ `, errors: [ - {message: 'Expected JSDoc block to be aligned.'} + { + line: 3, + message: 'Expected JSDoc block to be aligned.' + } ] }, { @@ -136,7 +144,10 @@ export default { } `, errors: [ - {message: 'Expected JSDoc block to be aligned.'} + { + line: 5, + message: 'Expected JSDoc block to be aligned.' + } ] } ], diff --git a/test/rules/assertions/checkIndentation.js b/test/rules/assertions/checkIndentation.js index 0a18721c1..1bec7eebf 100644 --- a/test/rules/assertions/checkIndentation.js +++ b/test/rules/assertions/checkIndentation.js @@ -14,6 +14,7 @@ export default { `, errors: [ { + line: 6, message: 'There must be no indentation.' } ] @@ -28,6 +29,7 @@ export default { `, errors: [ { + line: 4, message: 'There must be no indentation.' } ] diff --git a/test/rules/assertions/newlineAfterDescription.js b/test/rules/assertions/newlineAfterDescription.js index cf2845205..9c288059f 100644 --- a/test/rules/assertions/newlineAfterDescription.js +++ b/test/rules/assertions/newlineAfterDescription.js @@ -14,6 +14,7 @@ export default { `, errors: [ { + line: 5, message: 'There must be a newline after the description of the JSDoc block.' } ], @@ -47,6 +48,7 @@ export default { `, errors: [ { + line: 5, message: 'There must be a newline after the description of the JSDoc block.' } ], @@ -78,6 +80,7 @@ export default { `, errors: [ { + line: 6, message: 'There must be no newline after the description of the JSDoc block.' } ], From 1a13d8efed62f4eea47ab7a334a63e8deaff4b22 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 10 Jul 2019 09:26:48 +0800 Subject: [PATCH 12/46] fix(check-tag-names, check-types, no-undefined-types): ensure bad settings are reported as line 1 --- src/iterateJsdoc.js | 12 ++++++++++++ src/rules/checkTagNames.js | 2 +- src/rules/checkTypes.js | 6 ++---- src/rules/noUndefinedTypes.js | 2 +- test/rules/assertions/checkTagNames.js | 1 + test/rules/assertions/checkTypes.js | 1 + test/rules/assertions/noUndefinedTypes.js | 1 + 7 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/iterateJsdoc.js b/src/iterateJsdoc.js index f6bfa4f64..57003b4fc 100644 --- a/src/iterateJsdoc.js +++ b/src/iterateJsdoc.js @@ -208,6 +208,18 @@ const getUtils = ( }); }; + utils.reportSettings = (message) => { + context.report({ + loc: { + start: { + column: 1, + line: 1 + } + }, + message + }); + }; + return utils; }; diff --git a/src/rules/checkTagNames.js b/src/rules/checkTagNames.js index 04526bf17..6e57b87d7 100644 --- a/src/rules/checkTagNames.js +++ b/src/rules/checkTagNames.js @@ -28,7 +28,7 @@ export default iterateJsdoc(({ return undefined; } if (typeof preferredTag !== 'object') { - report( + utils.reportSettings( 'Invalid `settings.jsdoc.tagNamePreference`. Values must be falsy, a string, or an object.' ); } diff --git a/src/rules/checkTypes.js b/src/rules/checkTypes.js index e1239d08d..21a70f354 100644 --- a/src/rules/checkTypes.js +++ b/src/rules/checkTypes.js @@ -154,10 +154,8 @@ export default iterateJsdoc(({ _.get(preferredSetting, 'message') ]); } else { - report( - 'Invalid `settings.jsdoc.preferredTypes`. Values must be falsy, a string, or an object.', - null, - jsdocTag + utils.reportSettings( + 'Invalid `settings.jsdoc.preferredTypes`. Values must be falsy, a string, or an object.' ); return; diff --git a/src/rules/noUndefinedTypes.js b/src/rules/noUndefinedTypes.js index 8c2e12eed..cec6ea600 100644 --- a/src/rules/noUndefinedTypes.js +++ b/src/rules/noUndefinedTypes.js @@ -41,7 +41,7 @@ export default iterateJsdoc(({ return undefined; } if (typeof preferredType !== 'object') { - report( + utils.reportSettings( 'Invalid `settings.jsdoc.preferredTypes`. Values must be falsy, a string, or an object.' ); } diff --git a/test/rules/assertions/checkTagNames.js b/test/rules/assertions/checkTagNames.js index 9725364bf..bf6bc4b6b 100644 --- a/test/rules/assertions/checkTagNames.js +++ b/test/rules/assertions/checkTagNames.js @@ -295,6 +295,7 @@ export default { `, errors: [ { + line: 1, message: 'Invalid `settings.jsdoc.tagNamePreference`. Values must be falsy, a string, or an object.' }, { diff --git a/test/rules/assertions/checkTypes.js b/test/rules/assertions/checkTypes.js index 2eb311771..a7836c575 100644 --- a/test/rules/assertions/checkTypes.js +++ b/test/rules/assertions/checkTypes.js @@ -11,6 +11,7 @@ export default { `, errors: [ { + line: 1, message: 'Invalid `settings.jsdoc.preferredTypes`. Values must be falsy, a string, or an object.' } ], diff --git a/test/rules/assertions/noUndefinedTypes.js b/test/rules/assertions/noUndefinedTypes.js index 60d98e47b..14d304540 100644 --- a/test/rules/assertions/noUndefinedTypes.js +++ b/test/rules/assertions/noUndefinedTypes.js @@ -11,6 +11,7 @@ export default { `, errors: [ { + line: 1, message: 'Invalid `settings.jsdoc.preferredTypes`. Values must be falsy, a string, or an object.' }, { From 006c14055987c600a080afecbee3e21c26d8bb59 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 10 Jul 2019 09:31:52 +0800 Subject: [PATCH 13/46] fix(check-indentation): check for indentation when no line breaks in doc block --- README.md | 11 +++++++++++ src/rules/checkIndentation.js | 2 +- test/rules/assertions/checkIndentation.js | 22 ++++++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 038ca7ea2..d1e378f78 100644 --- a/README.md +++ b/README.md @@ -833,6 +833,12 @@ Reports invalid padding inside JSDoc block. The following patterns are considered problems: ````js +/*** foo */ +function quux () { + +} +// Message: There must be no indentation. + /** * foo * @@ -863,6 +869,11 @@ The following patterns are not considered problems: */ function quux () { +} + +/*** foo */ +function quux () { + } ```` diff --git a/src/rules/checkIndentation.js b/src/rules/checkIndentation.js index fccdffaf2..d0e2d2c98 100644 --- a/src/rules/checkIndentation.js +++ b/src/rules/checkIndentation.js @@ -5,7 +5,7 @@ export default iterateJsdoc(({ jsdocNode, report }) => { - const reg = new RegExp(/^[ \t]+\*[ \t]{2}/gm); + const reg = new RegExp(/^(?:\/?\**|[ \t]*)\*[ \t]{2}/gm); const text = sourceCode.getText(jsdocNode); if (reg.test(text)) { diff --git a/test/rules/assertions/checkIndentation.js b/test/rules/assertions/checkIndentation.js index 1bec7eebf..6ab824361 100644 --- a/test/rules/assertions/checkIndentation.js +++ b/test/rules/assertions/checkIndentation.js @@ -1,5 +1,19 @@ export default { invalid: [ + { + code: ` + /*** foo */ + function quux () { + + } + `, + errors: [ + { + line: 2, + message: 'There must be no indentation.' + } + ] + }, { code: ` /** @@ -46,6 +60,14 @@ export default { */ function quux () { + } + ` + }, + { + code: ` + /*** foo */ + function quux () { + } ` } From 1b93fa8e08d2b8b5ec36536b96be61023e6244c9 Mon Sep 17 00:00:00 2001 From: golopot Date: Wed, 10 Jul 2019 15:19:29 +0800 Subject: [PATCH 14/46] refactor(iterateJsdoc): remove unused param `returns` --- src/iterateJsdoc.js | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/iterateJsdoc.js b/src/iterateJsdoc.js index 57003b4fc..6caf211bc 100644 --- a/src/iterateJsdoc.js +++ b/src/iterateJsdoc.js @@ -351,7 +351,6 @@ export { * @param {{ * meta: any, * contextDefaults?: true | string[], - * returns?: string[], * iterateAllJsdocs?: true, * }} ruleConfig */ @@ -383,11 +382,6 @@ export default function iterateJsdoc (iterator, ruleConfig) { const settings = getSettings(context); - let contexts = ruleConfig.returns; - if (ruleConfig.contextDefaults) { - contexts = jsdocUtils.enforcedContexts(context, ruleConfig.contextDefaults); - } - const checkJsdoc = (node) => { const jsdocNode = getJSDocComment(sourceCode, node); @@ -429,15 +423,18 @@ export default function iterateJsdoc (iterator, ruleConfig) { utils }); }; - if (!contexts) { - return { - ArrowFunctionExpression: checkJsdoc, - FunctionDeclaration: checkJsdoc, - FunctionExpression: checkJsdoc - }; + + if (ruleConfig.contextDefaults) { + const contexts = jsdocUtils.enforcedContexts(context, ruleConfig.contextDefaults); + + return jsdocUtils.getContextObject(contexts, checkJsdoc); } - return jsdocUtils.getContextObject(contexts, checkJsdoc); + return { + ArrowFunctionExpression: checkJsdoc, + FunctionDeclaration: checkJsdoc, + FunctionExpression: checkJsdoc + }; }, meta: ruleConfig.meta }; From 647ad874818c57ea88487c0455c0d645847a44df Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 10 Jul 2019 22:13:08 +0800 Subject: [PATCH 15/46] - Clarify eslint code has been modified --- src/eslint/getJSDocComment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/eslint/getJSDocComment.js b/src/eslint/getJSDocComment.js index 1065be6f0..a453c8208 100644 --- a/src/eslint/getJSDocComment.js +++ b/src/eslint/getJSDocComment.js @@ -1,5 +1,5 @@ /** - * Obtained from {@link https://github.com/eslint/eslint/blob/master/lib/util/source-code.js#L313} + * Obtained originally from {@link https://github.com/eslint/eslint/blob/master/lib/util/source-code.js#L313} * @license MIT */ From f0c246cc583ed9d79869d2556023453d47807f67 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 10 Jul 2019 22:18:28 +0800 Subject: [PATCH 16/46] chore(npm): update devDeps (lodash, babel, eslint-config-canonical) --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index b00fc544e..a58bb7760 100644 --- a/package.json +++ b/package.json @@ -10,16 +10,16 @@ "escape-regex-string": "^1.0.6", "flat-map-polyfill": "^0.3.8", "jsdoctypeparser": "5.0.1", - "lodash": "^4.17.11", + "lodash": "^4.17.13", "regextras": "^0.6.1" }, "description": "JSDoc linting rules for ESLint.", "devDependencies": { "@babel/cli": "^7.5.0", - "@babel/core": "^7.5.0", + "@babel/core": "^7.5.4", "@babel/node": "^7.5.0", "@babel/plugin-transform-flow-strip-types": "^7.4.4", - "@babel/preset-env": "^7.5.2", + "@babel/preset-env": "^7.5.4", "@babel/register": "^7.4.4", "@typescript-eslint/parser": "^1.11.0", "babel-eslint": "^10.0.2", @@ -27,7 +27,7 @@ "babel-plugin-istanbul": "^5.1.4", "chai": "^4.2.0", "eslint": "^6.0.1", - "eslint-config-canonical": "^17.1.1", + "eslint-config-canonical": "^17.1.2", "gitdown": "^2.6.1", "glob": "^7.1.4", "husky": "^3.0.0", From 0a47c8f7ba8033d8c3d2872d954bd0ce8922bc65 Mon Sep 17 00:00:00 2001 From: Antoine Nozeret Date: Thu, 11 Jul 2019 12:21:35 +0200 Subject: [PATCH 17/46] fix(checkTagNames): #327 - Tag name autofix confused by adjacent tags --- src/rules/checkTagNames.js | 2 +- test/rules/assertions/checkTagNames.js | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/rules/checkTagNames.js b/src/rules/checkTagNames.js index 6e57b87d7..bd2efe586 100644 --- a/src/rules/checkTagNames.js +++ b/src/rules/checkTagNames.js @@ -57,7 +57,7 @@ export default iterateJsdoc(({ if (preferredTagName !== tagName) { report(message, (fixer) => { - const replacement = sourceCode.getText(jsdocNode).replace(`@${tagName}`, `@${preferredTagName}`); + const replacement = sourceCode.getText(jsdocNode).replace(`@${tagName} `, `@${preferredTagName} `); return fixer.replaceText(jsdocNode, replacement); }, jsdocTag); diff --git a/test/rules/assertions/checkTagNames.js b/test/rules/assertions/checkTagNames.js index bf6bc4b6b..73e3f0c1f 100644 --- a/test/rules/assertions/checkTagNames.js +++ b/test/rules/assertions/checkTagNames.js @@ -309,6 +309,32 @@ export default { } } } + }, + { + code: ` + /** + * @property {object} a + * @prop {boolean} b + */ + function quux () { + + } + `, + errors: [ + { + line: 4, + message: 'Invalid JSDoc tag (preference). Replace "prop" JSDoc tag with "property".' + } + ], + output: ` + /** + * @property {object} a + * @property {boolean} b + */ + function quux () { + + } + ` } ], valid: [ From 2a605bfac023b339a09f63bca55de5a32501e24d Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Jul 2019 08:30:51 -0700 Subject: [PATCH 18/46] fix(check-tag-names): treat keys in `tagNamePreference` setting as defined so appropriate message will appear and fixer applied --- src/rules/checkTagNames.js | 5 +++- test/rules/assertions/checkTagNames.js | 38 ++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/rules/checkTagNames.js b/src/rules/checkTagNames.js index bd2efe586..7daad1b61 100644 --- a/src/rules/checkTagNames.js +++ b/src/rules/checkTagNames.js @@ -16,8 +16,11 @@ export default iterateJsdoc(({ const {definedTags = []} = context.options[0] || {}; let definedPreferredTags = []; + let definedNonPreferredTags = []; const {tagNamePreference} = settings; if (Object.keys(tagNamePreference).length) { + definedNonPreferredTags = _.keys(tagNamePreference); + // Replace `_.values` with `Object.values` when we may start requiring Node 7+ definedPreferredTags = _.values(tagNamePreference).map((preferredTag) => { if (typeof preferredTag === 'string') { @@ -41,7 +44,7 @@ export default iterateJsdoc(({ jsdoc.tags.forEach((jsdocTag) => { const tagName = jsdocTag.tag; - if (utils.isValidTag(tagName, [...definedTags, ...definedPreferredTags])) { + if (utils.isValidTag(tagName, [...definedTags, ...definedPreferredTags, ...definedNonPreferredTags])) { let preferredTagName = utils.getPreferredTagName( tagName, true, diff --git a/test/rules/assertions/checkTagNames.js b/test/rules/assertions/checkTagNames.js index 73e3f0c1f..59e09767f 100644 --- a/test/rules/assertions/checkTagNames.js +++ b/test/rules/assertions/checkTagNames.js @@ -335,6 +335,44 @@ export default { } ` + }, + { + code: ` + /** + * @abc foo + * @abcd bar + */ + function quux () { + + } + `, + errors: [ + { + line: 3, + message: 'Invalid JSDoc tag (preference). Replace "abc" JSDoc tag with "abcd".' + } + ], + options: [ + { + definedTags: ['abcd'] + } + ], + output: ` + /** + * @abcd foo + * @abcd bar + */ + function quux () { + + } + `, + settings: { + jsdoc: { + tagNamePreference: { + abc: 'abcd' + } + } + } } ], valid: [ From 15424f83f2dfba9fb3a8c3c76a83f5ac73e47803 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Jul 2019 08:30:51 -0700 Subject: [PATCH 19/46] docs: generate docs --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index d1e378f78..aa059ca7d 100644 --- a/README.md +++ b/README.md @@ -1379,6 +1379,26 @@ function quux () { } // Settings: {"jsdoc":{"tagNamePreference":{"todo":55}}} // Message: Invalid `settings.jsdoc.tagNamePreference`. Values must be falsy, a string, or an object. + +/** + * @property {object} a + * @prop {boolean} b + */ +function quux () { + +} +// Message: Invalid JSDoc tag (preference). Replace "prop" JSDoc tag with "property". + +/** + * @abc foo + * @abcd bar + */ +function quux () { + +} +// Settings: {"jsdoc":{"tagNamePreference":{"abc":"abcd"}}} +// Options: [{"definedTags":["abcd"]}] +// Message: Invalid JSDoc tag (preference). Replace "abc" JSDoc tag with "abcd". ```` The following patterns are not considered problems: From 1145cd0597e81e97160a803617d830b967e1d49d Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Jul 2019 08:35:03 -0700 Subject: [PATCH 20/46] fix(check-tag-names): loosen tag replacement to allow for no-space if at word boundary (as at end of line) --- src/rules/checkTagNames.js | 5 +++- test/rules/assertions/checkTagNames.js | 33 ++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/rules/checkTagNames.js b/src/rules/checkTagNames.js index 7daad1b61..529901ee2 100644 --- a/src/rules/checkTagNames.js +++ b/src/rules/checkTagNames.js @@ -60,7 +60,10 @@ export default iterateJsdoc(({ if (preferredTagName !== tagName) { report(message, (fixer) => { - const replacement = sourceCode.getText(jsdocNode).replace(`@${tagName} `, `@${preferredTagName} `); + const replacement = sourceCode.getText(jsdocNode).replace( + new RegExp(`@${_.escapeRegExp(tagName)}\\b`), + `@${preferredTagName}` + ); return fixer.replaceText(jsdocNode, replacement); }, jsdocTag); diff --git a/test/rules/assertions/checkTagNames.js b/test/rules/assertions/checkTagNames.js index 59e09767f..fb09222e0 100644 --- a/test/rules/assertions/checkTagNames.js +++ b/test/rules/assertions/checkTagNames.js @@ -373,6 +373,39 @@ export default { } } } + }, + { + code: ` + /** + * @abc + * @abcd + */ + function quux () { + + } + `, + errors: [ + { + line: 3, + message: 'Invalid JSDoc tag (preference). Replace "abc" JSDoc tag with "abcd".' + } + ], + output: ` + /** + * @abcd + * @abcd + */ + function quux () { + + } + `, + settings: { + jsdoc: { + tagNamePreference: { + abc: 'abcd' + } + } + } } ], valid: [ From 9e399148e1a9be2cff79d8874696fb6c98ad83a7 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Jul 2019 08:35:03 -0700 Subject: [PATCH 21/46] docs: generate docs --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index aa059ca7d..a12bc6792 100644 --- a/README.md +++ b/README.md @@ -1399,6 +1399,16 @@ function quux () { // Settings: {"jsdoc":{"tagNamePreference":{"abc":"abcd"}}} // Options: [{"definedTags":["abcd"]}] // Message: Invalid JSDoc tag (preference). Replace "abc" JSDoc tag with "abcd". + +/** + * @abc + * @abcd + */ +function quux () { + +} +// Settings: {"jsdoc":{"tagNamePreference":{"abc":"abcd"}}} +// Message: Invalid JSDoc tag (preference). Replace "abc" JSDoc tag with "abcd". ```` The following patterns are not considered problems: From 3c39014e71217e98b49f67233a60a69d5b3a4112 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Jul 2019 09:36:40 -0700 Subject: [PATCH 22/46] fix(check-examples): disallow empty caption (as with missing caption); fixes #330 --- src/rules/checkExamples.js | 4 ++-- test/rules/assertions/checkExamples.js | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/rules/checkExamples.js b/src/rules/checkExamples.js index 005783b88..483cabb44 100644 --- a/src/rules/checkExamples.js +++ b/src/rules/checkExamples.js @@ -6,7 +6,7 @@ import warnRemovedSettings from '../warnRemovedSettings'; const zeroBasedLineIndexAdjust = -1; const likelyNestedJSDocIndentSpace = 1; const preTagSpaceLength = 1; -const hasCaptionRegex = /^\s*.*?<\/caption>/; +const hasCaptionRegex = /^\s*(.*?)<\/caption>/; const escapeStringRegexp = (str) => { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); @@ -81,7 +81,7 @@ export default iterateJsdoc(({ let source = tag.source.slice(initialTagLength); const match = source.match(hasCaptionRegex); - if (captionRequired && !match) { + if (captionRequired && (!match || !match[1].trim())) { report('Caption is expected for examples.', null, tag); } diff --git a/test/rules/assertions/checkExamples.js b/test/rules/assertions/checkExamples.js index ab5babb25..fe25703ab 100644 --- a/test/rules/assertions/checkExamples.js +++ b/test/rules/assertions/checkExamples.js @@ -443,6 +443,24 @@ export default { reportUnusedDisableDirectives: true } } + }, + { + code: ` + /** + * @typedef {string} Foo + * @example + * 'foo' + */ + `, + errors: [ + { + message: 'Caption is expected for examples.' + } + ], + options: [{ + captionRequired: true, + eslintrcForExamples: false + }] } ], valid: [ From 57f33da01c33929c9e0bef7c489807f3d173cdff Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Jul 2019 09:36:40 -0700 Subject: [PATCH 23/46] docs: generate docs --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index a12bc6792..847861e57 100644 --- a/README.md +++ b/README.md @@ -742,6 +742,14 @@ function f () { } // Settings: {"jsdoc":{"allowInlineConfig":true,"baseConfig":{},"captionRequired":false,"configFile":"configFile.js","eslintrcForExamples":true,"exampleCodeRegex":".*?","matchingFileName":"test.md","noDefaultExampleRules":false,"rejectExampleCodeRegex":"\\W*","reportUnusedDisableDirectives":true}} // Message: `settings.jsdoc.captionRequired` has been removed, use options in the rule `check-examples` instead. + +/** +* @typedef {string} Foo +* @example +* 'foo' +*/ +// Options: [{"captionRequired":true,"eslintrcForExamples":false}] +// Message: Caption is expected for examples. ```` The following patterns are not considered problems: From d75fd6fc19abcbcbafd4e70e6398223e8fcdce3e Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Jul 2019 10:56:30 -0700 Subject: [PATCH 24/46] refactor: extract out retrieval for `tagsWithNames` and `tagsWithoutNames` through `getTagsByType`; also add `jsdocUtils.filterTags` --- README.md | 6 ++++++ src/iterateJsdoc.js | 6 +++++- src/jsdocUtils.js | 27 +++++++++++++++++++++++++++ src/rules/matchDescription.js | 19 +++---------------- 4 files changed, 41 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 847861e57..949cf0b16 100644 --- a/README.md +++ b/README.md @@ -3752,6 +3752,12 @@ function quux (foo) { } // Message: Sentence should start with an uppercase character. + +/** + * @typedef {Object} Hello World + * hello world +*/ +// Message: Sentence must end with a period. ```` The following patterns are not considered problems: diff --git a/src/iterateJsdoc.js b/src/iterateJsdoc.js index 6caf211bc..2efeeebdd 100644 --- a/src/iterateJsdoc.js +++ b/src/iterateJsdoc.js @@ -161,7 +161,11 @@ const getUtils = ( }; utils.filterTags = (filter) => { - return (jsdoc.tags || []).filter(filter); + return jsdocUtils.filterTags(jsdoc.tags, filter); + }; + + utils.getTagsByType = (tags) => { + return jsdocUtils.getTagsByType(tags, tagNamePreference); }; utils.getClassNode = () => { diff --git a/src/jsdocUtils.js b/src/jsdocUtils.js index 375e69a01..0af1829ad 100644 --- a/src/jsdocUtils.js +++ b/src/jsdocUtils.js @@ -520,13 +520,40 @@ const getContextObject = (contexts, checkJsdoc) => { }, {}); }; +const filterTags = (tags = [], filter) => { + return tags.filter(filter); +}; + +const tagsWithNamesAndDescriptions = ['param', 'arg', 'argument', 'returns', 'return']; + +const getTagsByType = (tags, tagPreference) => { + const descName = getPreferredTagName('description', tagPreference); + const tagsWithoutNames = []; + const tagsWithNames = filterTags(tags, (tag) => { + const {tag: tagName} = tag; + const tagWithName = tagsWithNamesAndDescriptions.includes(tagName); + if (!tagWithName && tagName !== descName) { + tagsWithoutNames.push(tag); + } + + return tagWithName; + }); + + return { + tagsWithoutNames, + tagsWithNames + }; +}; + export default { enforcedContexts, + filterTags, getContextObject, getFunctionParameterNames, getJsdocParameterNames, getJsdocParameterNamesDeep, getPreferredTagName, + getTagsByType, hasATag, hasDefinedTypeReturnTag, hasReturnValue, diff --git a/src/rules/matchDescription.js b/src/rules/matchDescription.js index d1ef93260..fc594c83c 100644 --- a/src/rules/matchDescription.js +++ b/src/rules/matchDescription.js @@ -1,8 +1,6 @@ import _ from 'lodash'; import iterateJsdoc from '../iterateJsdoc'; -const tagsWithNamesAndDescriptions = ['param', 'arg', 'argument']; - // If supporting Node >= 10, we could loosen the default to this for the // initial letter: \\p{Upper} const matchDescriptionDefault = '^[A-Z`\\d_][\\s\\S]*[.?!`]$'; @@ -57,28 +55,17 @@ export default iterateJsdoc(({ return Boolean(options.tags[tagName]); }; - let descName; utils.forEachPreferredTag('description', (matchingJsdocTag, targetTagName) => { - descName = targetTagName; const description = (matchingJsdocTag.name + ' ' + matchingJsdocTag.description).trim(); if (hasOptionTag(targetTagName)) { validateDescription(description, matchingJsdocTag); } }); - const tagsWithoutNames = []; - const tagsWithNames = utils.filterTags((tag) => { - const {tag: tagName} = tag; - if (!hasOptionTag(tagName)) { - return false; - } - const tagWithName = tagsWithNamesAndDescriptions.includes(tagName); - if (!tagWithName && tagName !== descName) { - tagsWithoutNames.push(tag); - } - - return tagWithName; + const whitelistedTags = utils.filterTags(({tag: tagName}) => { + return hasOptionTag(tagName); }); + const {tagsWithNames, tagsWithoutNames} = utils.getTagsByType(whitelistedTags); tagsWithNames.some((tag) => { const description = _.trimStart(tag.description, '- '); From 78f9b61b3a8e0a4c9e0496d79589c288f7f7d511 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Jul 2019 11:15:11 -0700 Subject: [PATCH 25/46] fix(require-description-complete-sentence): validate tags without names (e.g., beyond param/returns); fixes #333 --- src/rules/requireDescriptionCompleteSentence.js | 12 ++++++++---- .../requireDescriptionCompleteSentence.js | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/rules/requireDescriptionCompleteSentence.js b/src/rules/requireDescriptionCompleteSentence.js index 629bec052..c6542c71c 100644 --- a/src/rules/requireDescriptionCompleteSentence.js +++ b/src/rules/requireDescriptionCompleteSentence.js @@ -139,15 +139,19 @@ export default iterateJsdoc(({ validateDescription(description, report, jsdocNode, sourceCode, matchingJsdocTag); }); - const tags = jsdoc.tags.filter((tag) => { - return ['param', 'arg', 'argument', 'returns', 'return'].includes(tag.tag); - }); + const {tagsWithNames, tagsWithoutNames} = utils.getTagsByType(jsdoc.tags); - tags.some((tag) => { + tagsWithNames.some((tag) => { const description = _.trimStart(tag.description, '- '); return validateDescription(description, report, jsdocNode, sourceCode, tag); }); + + tagsWithoutNames.some((tag) => { + const description = (tag.name + ' ' + tag.description).trim(); + + return validateDescription(description, report, jsdocNode, sourceCode, tag); + }); }, { iterateAllJsdocs: true, meta: { diff --git a/test/rules/assertions/requireDescriptionCompleteSentence.js b/test/rules/assertions/requireDescriptionCompleteSentence.js index 4d7a34b74..1505f692a 100644 --- a/test/rules/assertions/requireDescriptionCompleteSentence.js +++ b/test/rules/assertions/requireDescriptionCompleteSentence.js @@ -500,6 +500,20 @@ export default { } ` + }, + { + code: ` + /** + * @typedef {Object} Hello World + * hello world + */ + `, + errors: [ + { + line: 3, + message: 'Sentence must end with a period.' + } + ] } ], valid: [ From 87110edd8c76a3be50bfb5e3456061f201eab25a Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Jul 2019 11:27:17 -0700 Subject: [PATCH 26/46] chore: update lodash dep. and devDeps (typescript-eslint/parser, eslint-config-canonical) --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index a58bb7760..71b381632 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "escape-regex-string": "^1.0.6", "flat-map-polyfill": "^0.3.8", "jsdoctypeparser": "5.0.1", - "lodash": "^4.17.13", + "lodash": "^4.17.14", "regextras": "^0.6.1" }, "description": "JSDoc linting rules for ESLint.", @@ -21,13 +21,13 @@ "@babel/plugin-transform-flow-strip-types": "^7.4.4", "@babel/preset-env": "^7.5.4", "@babel/register": "^7.4.4", - "@typescript-eslint/parser": "^1.11.0", + "@typescript-eslint/parser": "^1.12.0", "babel-eslint": "^10.0.2", "babel-plugin-add-module-exports": "^1.0.2", "babel-plugin-istanbul": "^5.1.4", "chai": "^4.2.0", "eslint": "^6.0.1", - "eslint-config-canonical": "^17.1.2", + "eslint-config-canonical": "^17.1.3", "gitdown": "^2.6.1", "glob": "^7.1.4", "husky": "^3.0.0", From c7f79fa1bba8a86d79387d92beb5bbcc0c8a4b34 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Jul 2019 12:22:02 -0700 Subject: [PATCH 27/46] fix(newline-after-description): avoid matching duplicate or partial matches within tag descriptions after the block description; fixes #328 --- src/rules/newlineAfterDescription.js | 4 +- .../assertions/newlineAfterDescription.js | 50 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/rules/newlineAfterDescription.js b/src/rules/newlineAfterDescription.js index 938db4b57..e06471ea1 100644 --- a/src/rules/newlineAfterDescription.js +++ b/src/rules/newlineAfterDescription.js @@ -30,7 +30,7 @@ export default iterateJsdoc(({ if (!descriptionEndsWithANewline) { const sourceLines = sourceCode.getText(jsdocNode).split('\n'); const lastDescriptionLine = _.findLastIndex(sourceLines, (line) => { - return line.includes(_.last(jsdoc.description.split('\n'))); + return line.replace(/^\s*\*\s*/, '') === _.last(jsdoc.description.split('\n')); }); report('There must be a newline after the description of the JSDoc block.', (fixer) => { // Add the new line @@ -44,7 +44,7 @@ export default iterateJsdoc(({ } else if (descriptionEndsWithANewline) { const sourceLines = sourceCode.getText(jsdocNode).split('\n'); const lastDescriptionLine = _.findLastIndex(sourceLines, (line) => { - return line.includes(_.last(jsdoc.description.split('\n'))); + return line.replace(/^\s*\*\s*/, '') === _.last(jsdoc.description.split('\n')); }); report('There must be no newline after the description of the JSDoc block.', (fixer) => { // Remove the extra line diff --git a/test/rules/assertions/newlineAfterDescription.js b/test/rules/assertions/newlineAfterDescription.js index 9c288059f..fb2f99f96 100644 --- a/test/rules/assertions/newlineAfterDescription.js +++ b/test/rules/assertions/newlineAfterDescription.js @@ -98,6 +98,56 @@ export default { } ` + }, + { + code: ` + /** + * A. + * + * @typedef {Object} A + * @prop {boolean} a A. + */ + `, + errors: [ + { + message: 'There must be no newline after the description of the JSDoc block.' + } + ], + options: [ + 'never' + ], + output: ` + /** + * A. + * @typedef {Object} A + * @prop {boolean} a A. + */ + ` + }, + { + code: ` + /** + * A. + * @typedef {Object} A + * @prop {boolean} a A. + */ + `, + errors: [ + { + message: 'There must be a newline after the description of the JSDoc block.' + } + ], + options: [ + 'always' + ], + output: ` + /** + * A. + * + * @typedef {Object} A + * @prop {boolean} a A. + */ + ` } ], valid: [ From e5767d3c5a0292162227f5a6abc3b213de1efbb0 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Jul 2019 12:25:18 -0700 Subject: [PATCH 28/46] chore: add `lint-fix` script --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 71b381632..730de6c3d 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "add-assertions": "babel-node ./src/bin/readme-assertions", "build": "rm -fr ./dist && NODE_ENV=production babel ./src --out-dir ./dist --copy-files --source-maps", "create-readme": "gitdown ./.README/README.md --output-file ./README.md && npm run add-assertions", + "lint-fix": "eslint --fix ./src ./test", "lint": "eslint ./src ./test", "test-cov": "BABEL_ENV=test nyc mocha --recursive --require @babel/register --reporter progress --timeout 9000", "test-no-cov": "BABEL_ENV=test mocha --recursive --require @babel/register --reporter progress --timeout 9000", From cd35adbbfb83df87b4687110b2c874eb61c9efb7 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Jul 2019 12:26:17 -0700 Subject: [PATCH 29/46] refactor: apply (jsdoc-related) eslint rule fixes --- src/eslint/getJSDocComment.js | 1 + src/iterateJsdoc.js | 6 +++--- src/jsdocUtils.js | 4 ++-- src/warnRemovedSettings.js | 8 ++++---- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/eslint/getJSDocComment.js b/src/eslint/getJSDocComment.js index a453c8208..49fef7ca1 100644 --- a/src/eslint/getJSDocComment.js +++ b/src/eslint/getJSDocComment.js @@ -1,5 +1,6 @@ /** * Obtained originally from {@link https://github.com/eslint/eslint/blob/master/lib/util/source-code.js#L313} + * * @license MIT */ diff --git a/src/iterateJsdoc.js b/src/iterateJsdoc.js index 2efeeebdd..ca65d35c4 100644 --- a/src/iterateJsdoc.js +++ b/src/iterateJsdoc.js @@ -250,8 +250,8 @@ const getSettings = (context) => { /** * Create the report function * - * @param {Object} context - * @param {Object} commentNode + * @param {object} context + * @param {object} commentNode */ const makeReport = (context, commentNode) => { const report = (message, fix = null, jsdocLoc = null, data = null) => { @@ -378,7 +378,7 @@ export default function iterateJsdoc (iterator, ruleConfig) { * @param {*} context * a reference to the context which hold all important information * like settings and the sourcecode to check. - * @returns {Object} + * @returns {object} * a list with parser callback function. */ create (context) { diff --git a/src/jsdocUtils.js b/src/jsdocUtils.js index 0af1829ad..43e195fc4 100644 --- a/src/jsdocUtils.js +++ b/src/jsdocUtils.js @@ -444,9 +444,9 @@ const lookupTable = { * It traverses the parsed source code and returns as * soon as it stumbles upon the first return statement. * - * @param {Object} node + * @param {object} node * the node which should be checked. - * @param {Object} context + * @param {object} context * @param {boolean} ignoreAsync * ignore implicit async return. * @returns {boolean} diff --git a/src/warnRemovedSettings.js b/src/warnRemovedSettings.js index b826baa4b..833cf98e1 100644 --- a/src/warnRemovedSettings.js +++ b/src/warnRemovedSettings.js @@ -8,13 +8,13 @@ * )} RulesWithMovedSettings */ -/** @type {WeakMap>} */ +/** @type {WeakMap>} */ const warnedSettings = new WeakMap(); /** * Warn only once for each context and setting * - * @param {Object} context + * @param {object} context * @param {string} setting */ const hasBeenWarned = (context, setting) => { @@ -30,7 +30,7 @@ const markSettingAsWarned = (context, setting) => { }; /** - * @param {Object} obj + * @param {object} obj * @param {string} property * @returns {boolean} */ @@ -73,7 +73,7 @@ const getMovedSettings = (ruleName) => { }; /** - * @param {Object} context + * @param {object} context * @param {RulesWithMovedSettings} ruleName */ export default function warnRemovedSettings (context, ruleName) { From 4b9715789b09a21860cd8594d29c744ee545dd48 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Jul 2019 17:27:14 -0700 Subject: [PATCH 30/46] refactor: use `String.prototype.repeat` over lodash `repeat` --- src/iterateJsdoc.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/iterateJsdoc.js b/src/iterateJsdoc.js index ca65d35c4..f6e8a0f9d 100644 --- a/src/iterateJsdoc.js +++ b/src/iterateJsdoc.js @@ -184,7 +184,7 @@ const getUtils = ( const classJsdocNode = getJSDocComment(sourceCode, classNode); if (classJsdocNode) { - const indent = _.repeat(' ', classJsdocNode.loc.start.column); + const indent = ' '.repeat(classJsdocNode.loc.start.column); return parseComment(classJsdocNode, indent); } @@ -321,7 +321,7 @@ const iterateAllJsdocs = (iterator, ruleConfig) => { return; } - const indent = _.repeat(' ', comment.loc.start.column); + const indent = ' '.repeat(comment.loc.start.column); const jsdoc = parseComment(comment, indent); const settings = getSettings(context); const report = makeReport(context, comment); @@ -393,7 +393,7 @@ export default function iterateJsdoc (iterator, ruleConfig) { return; } - const indent = _.repeat(' ', jsdocNode.loc.start.column); + const indent = ' '.repeat(jsdocNode.loc.start.column); const jsdoc = parseComment(jsdocNode, indent); From e52d763cf9e72b564425afce716f3df11aed2ac0 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Jul 2019 18:30:58 -0700 Subject: [PATCH 31/46] docs(require-returns, require-returns-check): indicate that these will report if multiple `returns` tags are present --- .README/rules/require-returns-check.md | 2 ++ .README/rules/require-returns.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.README/rules/require-returns-check.md b/.README/rules/require-returns-check.md index 0b645ec3b..f28a5a740 100644 --- a/.README/rules/require-returns-check.md +++ b/.README/rules/require-returns-check.md @@ -2,6 +2,8 @@ Checks if the return expression exists in function body and in the comment. +Will also report if multiple `@returns` tags are present. + ||| |---|---| |Context|`ArrowFunctionExpression`, `FunctionDeclaration`, `FunctionExpression`| diff --git a/.README/rules/require-returns.md b/.README/rules/require-returns.md index f9bf25e3e..d36bc23f9 100644 --- a/.README/rules/require-returns.md +++ b/.README/rules/require-returns.md @@ -2,6 +2,8 @@ Requires returns are documented. +Will also report if multiple `@returns` tags are present. + #### Options - `exemptedBy` - Array of tags (e.g., `['type']`) whose presence on the document From 44a353e565f42b13a4962386fb51995b095f7f08 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Sat, 13 Jul 2019 00:28:10 -0700 Subject: [PATCH 32/46] chore(travis): fix Travis to use older unicorn version that supports Node 6 (and the canonical config that requires the earlier unicorn version) --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ec61ccf23..30b17c663 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,8 @@ node_js: before_install: - npm config set depth 0 before_script: - - 'if [ -n "${ESLINT-}" ]; then npm install --no-save "eslint@${ESLINT}" ; fi' + - 'if [ "${ESLINT-}" == "6" ]; then npm install --no-save "eslint@${ESLINT}" ; fi' + - 'if [ "${ESLINT-}" == "5" ]; then npm install --no-save "eslint@${ESLINT}" eslint-config-canonical@17.1.2 eslint-plugin-unicorn@8.0.2 ; fi' notifications: email: false sudo: false From cea3560952803613588ef3ac862d13cf530a30ff Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Jul 2019 17:41:03 -0700 Subject: [PATCH 33/46] fix(check-examples): preserve whitespace so as to report issues with whitespace-related rules such as `indent` (fixes #211) --- package.json | 1 - src/iterateJsdoc.js | 52 +++++++++++++++++++++++--- src/rules/checkExamples.js | 20 ++++------ test/rules/assertions/checkExamples.js | 29 ++++++++++++-- 4 files changed, 78 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 730de6c3d..7edea057a 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,6 @@ "dependencies": { "comment-parser": "^0.5.5", "debug": "^4.1.1", - "escape-regex-string": "^1.0.6", "flat-map-polyfill": "^0.3.8", "jsdoctypeparser": "5.0.1", "lodash": "^4.17.14", diff --git a/src/iterateJsdoc.js b/src/iterateJsdoc.js index f6e8a0f9d..42de5b5b7 100644 --- a/src/iterateJsdoc.js +++ b/src/iterateJsdoc.js @@ -3,7 +3,13 @@ import commentParser from 'comment-parser'; import jsdocUtils from './jsdocUtils'; import getJSDocComment from './eslint/getJSDocComment'; -const parseComment = (commentNode, indent) => { +/** + * + * @param {object} commentNode + * @param {string} indent Whitespace + * @returns {object} + */ +const parseComment = (commentNode, indent, trim = true) => { // Preserve JSDoc block start/end indentation. return commentParser(`${indent}/*${commentNode.value}${indent}*/`, { // @see https://github.com/yavorskiy/comment-parser/issues/21 @@ -11,14 +17,45 @@ const parseComment = (commentNode, indent) => { commentParser.PARSERS.parse_tag, commentParser.PARSERS.parse_type, (str, data) => { - if (['return', 'returns', 'throws', 'exception'].includes(data.tag)) { + if (['example', 'return', 'returns', 'throws', 'exception'].includes(data.tag)) { return null; } return commentParser.PARSERS.parse_name(str, data); }, - commentParser.PARSERS.parse_description - ] + trim ? + commentParser.PARSERS.parse_description : + + // parse_description + (str, data) => { + // Only expected throw in previous step is if bad name (i.e., + // missing end bracket on optional name), but `@example` + // skips name parsing + /* istanbul ignore next */ + if (data.errors && data.errors.length) { + return null; + } + + // Tweak original regex to capture only single optional space + const result = str.match(/^\s?((.|\s)+)?/); + + // Always has at least whitespace due to `indent` we've added + /* istanbul ignore next */ + if (result) { + return { + data: { + description: result[1] === undefined ? '' : result[1] + }, + source: result[0] + }; + } + + // Always has at least whitespace due to `indent` we've added + /* istanbul ignore next */ + return null; + } + ], + trim })[0] || {}; }; @@ -322,7 +359,7 @@ const iterateAllJsdocs = (iterator, ruleConfig) => { } const indent = ' '.repeat(comment.loc.start.column); - const jsdoc = parseComment(comment, indent); + const jsdoc = parseComment(comment, indent, !ruleConfig.noTrim); const settings = getSettings(context); const report = makeReport(context, comment); const jsdocNode = comment; @@ -368,7 +405,10 @@ export default function iterateJsdoc (iterator, ruleConfig) { } if (ruleConfig.iterateAllJsdocs) { - return iterateAllJsdocs(iterator, {meta: ruleConfig.meta}); + return iterateAllJsdocs(iterator, { + meta: ruleConfig.meta, + noTrim: ruleConfig.noTrim + }); } return { diff --git a/src/rules/checkExamples.js b/src/rules/checkExamples.js index 483cabb44..be8d6396a 100644 --- a/src/rules/checkExamples.js +++ b/src/rules/checkExamples.js @@ -1,5 +1,4 @@ import {CLIEngine, Linter} from 'eslint'; -import escapeRegexString from 'escape-regex-string'; import iterateJsdoc from '../iterateJsdoc'; import warnRemovedSettings from '../warnRemovedSettings'; @@ -72,13 +71,9 @@ export default iterateJsdoc(({ utils.forEachPreferredTag('example', (tag, targetTagName) => { // If a space is present, we should ignore it - const initialTag = tag.source.match( - new RegExp(`^@${escapeRegexString(targetTagName)} ?`, 'u') - ); - const initialTagLength = initialTag[0].length; - const firstLinePrefixLength = preTagSpaceLength + initialTagLength; + const firstLinePrefixLength = preTagSpaceLength; - let source = tag.source.slice(initialTagLength); + let source = tag.description; const match = source.match(hasCaptionRegex); if (captionRequired && (!match || !match[1].trim())) { @@ -101,16 +96,14 @@ export default iterateJsdoc(({ const idx = source.search(exampleCodeRegex); // Strip out anything preceding user regex match (can affect line numbering) - let preMatchLines = 0; - const preMatch = source.slice(0, idx); - preMatchLines = countChars(preMatch, '\n'); + const preMatchLines = countChars(preMatch, '\n'); nonJSPrefacingLines = preMatchLines; const colDelta = preMatchLines ? - preMatch.slice(preMatch.lastIndexOf('\n') + 1).length - initialTagLength : + preMatch.slice(preMatch.lastIndexOf('\n') + 1).length : preMatch.length; // Get rid of text preceding user regex match (even if it leaves valid JS, it @@ -135,7 +128,7 @@ export default iterateJsdoc(({ if (nonJSPrefaceLineCount) { const charsInLastLine = nonJSPreface.slice(nonJSPreface.lastIndexOf('\n') + 1).length; - nonJSPrefacingCols += charsInLastLine - initialTagLength; + nonJSPrefacingCols += charsInLastLine; } else { nonJSPrefacingCols += colDelta + nonJSPreface.length; } @@ -277,5 +270,6 @@ export default iterateJsdoc(({ } ], type: 'suggestion' - } + }, + noTrim: true }); diff --git a/test/rules/assertions/checkExamples.js b/test/rules/assertions/checkExamples.js index fe25703ab..3b498e2ed 100644 --- a/test/rules/assertions/checkExamples.js +++ b/test/rules/assertions/checkExamples.js @@ -86,6 +86,7 @@ export default { code: ` /** * @example + * * \`\`\`js alert('hello'); \`\`\` */ function quux () { @@ -184,7 +185,7 @@ export default { } }, eslintrcForExamples: false, - rejectExampleCodeRegex: '^\\s*<.*>$' + rejectExampleCodeRegex: '^\\s*<.*>\\s*$' }] }, { @@ -305,7 +306,7 @@ export default { code: ` /** * @example const i = 5; - * quux2() + * quux2() */ function quux2 () { @@ -327,7 +328,7 @@ export default { code: ` /** * @example const i = 5; - * quux2() + * quux2() */ function quux2 () { @@ -346,7 +347,7 @@ export default { code: ` /** * @example const i = 5; - * quux2() + * quux2() */ function quux2 () { @@ -608,6 +609,26 @@ export default { eslintrcForExamples: false, exampleCodeRegex: '```js([\\s\\S]*)```' }] + }, + { + code: ` + /** + * @example + * foo(function (err) { + * throw err; + * }); + */ + function quux () {} +`, + options: [{ + baseConfig: { + rules: { + indent: ['error'] + } + }, + eslintrcForExamples: false, + noDefaultExampleRules: false + }] } ] }; From 4c11c39c1b641a27f4d83292a5baa48a629df09d Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Jul 2019 17:44:40 -0700 Subject: [PATCH 34/46] feat(check-examples): add `paddedIndent` option --- .README/rules/check-examples.md | 18 ++++++++++++++++++ src/rules/checkExamples.js | 9 +++++++++ test/rules/assertions/checkExamples.js | 25 +++++++++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/.README/rules/check-examples.md b/.README/rules/check-examples.md index 49d4818bf..c420759e9 100644 --- a/.README/rules/check-examples.md +++ b/.README/rules/check-examples.md @@ -37,6 +37,24 @@ If neither is in use, all examples will be matched. Note also that even if `captionRequired` is not set, any initial `` will be stripped out before doing the regex matching. +#### `paddedIndent` + +This integer property allows one to add a fixed amount of whitespace at the +beginning of the second or later lines of the example to be stripped so as +to avoid linting issues with the decorative whitespace. For example, if set +to a value of `4`, the initial whitespace below will not trigger `indent` +rule errors as the extra 4 spaces on each subsequent line will be stripped +out before evaluation. + +```js +/** + * @example + * anArray.filter((a) => { + * return a.b; + * }); + */ +``` + #### `reportUnusedDisableDirectives` If not set to `false`, `reportUnusedDisableDirectives` will report disabled diff --git a/src/rules/checkExamples.js b/src/rules/checkExamples.js index be8d6396a..87ab72de7 100644 --- a/src/rules/checkExamples.js +++ b/src/rules/checkExamples.js @@ -29,6 +29,7 @@ export default iterateJsdoc(({ noDefaultExampleRules = false, eslintrcForExamples = true, matchingFileName: filename = null, + paddedIndent = 0, baseConfig = {}, configFile, allowInlineConfig = true, @@ -150,6 +151,10 @@ export default iterateJsdoc(({ let messages; + if (paddedIndent) { + source = source.replace(new RegExp(`(^|\n) {${paddedIndent}}(?!$)`, 'g'), '\n'); + } + if (filename) { const config = cli.getConfigForFile(filename); @@ -258,6 +263,10 @@ export default iterateJsdoc(({ default: false, type: 'boolean' }, + paddedIndent: { + default: 0, + type: 'integer' + }, rejectExampleCodeRegex: { type: 'string' }, diff --git a/test/rules/assertions/checkExamples.js b/test/rules/assertions/checkExamples.js index 3b498e2ed..2cc6fc69a 100644 --- a/test/rules/assertions/checkExamples.js +++ b/test/rules/assertions/checkExamples.js @@ -328,6 +328,31 @@ export default { code: ` /** * @example const i = 5; + * quux2() + */ + function quux2 () { + + } + `, + errors: [ + { + message: '@example warning (id-length): Identifier name \'i\' is too short (< 2).' + }, + { + message: '@example error (semi): Missing semicolon.' + } + ], + options: [ + { + paddedIndent: 2 + } + ] + }, + { + code: ` + /** + * @example + * const i = 5; * quux2() */ function quux2 () { From 2bf4caacfd37a4f1b0b7827e70d51fe2033e2102 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Jul 2019 18:25:43 -0700 Subject: [PATCH 35/46] docs(check-examples): allow for whitespace at end --- .README/rules/check-examples.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.README/rules/check-examples.md b/.README/rules/check-examples.md index c420759e9..dd47ccef0 100644 --- a/.README/rules/check-examples.md +++ b/.README/rules/check-examples.md @@ -26,7 +26,7 @@ syntax highlighting). The following options determine whether a given so you may wish to use `(?:...)` groups where you do not wish the first such group treated as one to include. If no parenthetical group exists or matches, the whole matching expression will be used. - An example might be ````"^```(?:js|javascript)([\\s\\S]*)```$"```` + An example might be ````"^```(?:js|javascript)([\\s\\S]*)```\s*$"```` to only match explicitly fenced JavaScript blocks. * `rejectExampleCodeRegex` - Regex blacklist which rejects non-lintable examples (has priority over `exampleCodeRegex`). An example From e9145c44c5366ba11f2b3a8d59f48c4f2bea5c09 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Sat, 13 Jul 2019 07:32:18 -0700 Subject: [PATCH 36/46] refactor(iterateJsdoc): reduce retrieval calls --- src/iterateJsdoc.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/iterateJsdoc.js b/src/iterateJsdoc.js index 42de5b5b7..c533427a2 100644 --- a/src/iterateJsdoc.js +++ b/src/iterateJsdoc.js @@ -351,10 +351,11 @@ const iterateAllJsdocs = (iterator, ruleConfig) => { create (context) { return { 'Program:exit' () { - const comments = context.getSourceCode().getAllComments(); + const sourceCode = context.getSourceCode(); + const comments = sourceCode.getAllComments(); comments.forEach((comment) => { - if (!context.getSourceCode().getText(comment).startsWith('/**')) { + if (!sourceCode.getText(comment).startsWith('/**')) { return; } @@ -371,8 +372,8 @@ const iterateAllJsdocs = (iterator, ruleConfig) => { jsdocNode, node: null, report, - settings: getSettings(context), - sourceCode: context.getSourceCode(), + settings, + sourceCode, utils: getUtils(null, jsdoc, jsdocNode, settings, report, context) }); }); From a1c9824be2bbbf1766e04145a4e4c7a52bd9935d Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Sat, 13 Jul 2019 00:25:17 -0700 Subject: [PATCH 37/46] fix(no-undefined-types): ensure working in all contexts; fixes #324 --- src/iterateJsdoc.js | 8 ++++++-- src/jsdocUtils.js | 13 +++++++++++++ src/rules/noUndefinedTypes.js | 1 + test/rules/assertions/noUndefinedTypes.js | 19 +++++++++++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/iterateJsdoc.js b/src/iterateJsdoc.js index c533427a2..aa88f5a7b 100644 --- a/src/iterateJsdoc.js +++ b/src/iterateJsdoc.js @@ -206,7 +206,11 @@ const getUtils = ( }; utils.getClassNode = () => { - const greatGrandParent = ancestors.slice(-3)[0]; + // Ancestors missing in `Program` comment iteration + const greatGrandParent = ancestors.length ? + ancestors.slice(-3)[0] : + jsdocUtils.getAncestor(sourceCode, jsdocNode, 3); + const greatGrandParentValue = greatGrandParent && sourceCode.getFirstToken(greatGrandParent).value; if (greatGrandParentValue === 'class') { @@ -350,7 +354,7 @@ const iterateAllJsdocs = (iterator, ruleConfig) => { return { create (context) { return { - 'Program:exit' () { + 'Program' () { const sourceCode = context.getSourceCode(); const comments = sourceCode.getAllComments(); diff --git a/src/jsdocUtils.js b/src/jsdocUtils.js index 43e195fc4..fbb65d524 100644 --- a/src/jsdocUtils.js +++ b/src/jsdocUtils.js @@ -545,9 +545,22 @@ const getTagsByType = (tags, tagPreference) => { }; }; +const getAncestor = (sourceCode, nde, depth, idx = 0) => { + if (idx === depth) { + return nde; + } + const prevToken = sourceCode.getTokenBefore(nde); + if (prevToken) { + return getAncestor(sourceCode, prevToken, depth, idx + 1); + } + + return null; +}; + export default { enforcedContexts, filterTags, + getAncestor, getContextObject, getFunctionParameterNames, getJsdocParameterNames, diff --git a/src/rules/noUndefinedTypes.js b/src/rules/noUndefinedTypes.js index cec6ea600..860026915 100644 --- a/src/rules/noUndefinedTypes.js +++ b/src/rules/noUndefinedTypes.js @@ -132,6 +132,7 @@ export default iterateJsdoc(({ }); }); }, { + iterateAllJsdocs: true, meta: { schema: [ { diff --git a/test/rules/assertions/noUndefinedTypes.js b/test/rules/assertions/noUndefinedTypes.js index 14d304540..5afe23872 100644 --- a/test/rules/assertions/noUndefinedTypes.js +++ b/test/rules/assertions/noUndefinedTypes.js @@ -208,6 +208,25 @@ export default { message: 'The type \'TEMPLATE_TYPE\' is undefined.' } ] + }, + { + code: ` + /** + * @type {strnig} + */ + var quux = { + + }; + `, + errors: [ + { + line: 3, + message: 'The type \'strnig\' is undefined.' + } + ], + rules: { + 'no-undef': 'error' + } } ], valid: [ From 45b244345da74b95d32995a5eca5366b4ac14340 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Sat, 13 Jul 2019 10:06:42 -0700 Subject: [PATCH 38/46] chore: update devDeps (eslint-config-canonical, gitdown) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7edea057a..c780bfc41 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,8 @@ "babel-plugin-istanbul": "^5.1.4", "chai": "^4.2.0", "eslint": "^6.0.1", - "eslint-config-canonical": "^17.1.3", - "gitdown": "^2.6.1", + "eslint-config-canonical": "^17.1.4", + "gitdown": "^2.6.2", "glob": "^7.1.4", "husky": "^3.0.0", "mocha": "^6.1.4", From b3801006cf3c983292bed79d1213d863d6c810eb Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Sat, 13 Jul 2019 10:06:42 -0700 Subject: [PATCH 39/46] docs: generate docs --- README.md | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 949cf0b16..511559691 100644 --- a/README.md +++ b/README.md @@ -491,7 +491,7 @@ syntax highlighting). The following options determine whether a given so you may wish to use `(?:...)` groups where you do not wish the first such group treated as one to include. If no parenthetical group exists or matches, the whole matching expression will be used. - An example might be ````"^```(?:js|javascript)([\\s\\S]*)```$"```` + An example might be ````"^```(?:js|javascript)([\\s\\S]*)```\s*$"```` to only match explicitly fenced JavaScript blocks. * `rejectExampleCodeRegex` - Regex blacklist which rejects non-lintable examples (has priority over `exampleCodeRegex`). An example @@ -502,6 +502,25 @@ If neither is in use, all examples will be matched. Note also that even if `captionRequired` is not set, any initial `` will be stripped out before doing the regex matching. + +#### paddedIndent + +This integer property allows one to add a fixed amount of whitespace at the +beginning of the second or later lines of the example to be stripped so as +to avoid linting issues with the decorative whitespace. For example, if set +to a value of `4`, the initial whitespace below will not trigger `indent` +rule errors as the extra 4 spaces on each subsequent line will be stripped +out before evaluation. + +```js +/** + * @example + * anArray.filter((a) => { + * return a.b; + * }); + */ +``` + #### reportUnusedDisableDirectives @@ -604,6 +623,7 @@ function quux () { /** * @example + * * ```js alert('hello'); ``` */ function quux () { @@ -644,7 +664,7 @@ function quux () { function quux2 () { } -// Options: [{"baseConfig":{"rules":{"semi":["error","never"]}},"eslintrcForExamples":false,"rejectExampleCodeRegex":"^\\s*<.*>$"}] +// Options: [{"baseConfig":{"rules":{"semi":["error","never"]}},"eslintrcForExamples":false,"rejectExampleCodeRegex":"^\\s*<.*>\\s*$"}] // Message: @example error (semi): Extra semicolon. /** @@ -696,7 +716,7 @@ function quux () {} /** * @example const i = 5; - * quux2() + * quux2() */ function quux2 () { @@ -706,7 +726,18 @@ function quux2 () { /** * @example const i = 5; - * quux2() + * quux2() + */ +function quux2 () { + +} +// Options: [{"paddedIndent":2}] +// Message: @example warning (id-length): Identifier name 'i' is too short (< 2). + +/** + * @example + * const i = 5; + * quux2() */ function quux2 () { @@ -715,7 +746,7 @@ function quux2 () { /** * @example const i = 5; - * quux2() + * quux2() */ function quux2 () { @@ -825,6 +856,15 @@ var quux = { }; // Options: [{"baseConfig":{"rules":{"semi":["error","never"]}},"eslintrcForExamples":false,"exampleCodeRegex":"```js([\\s\\S]*)```"}] + +/** + * @example + * foo(function (err) { + * throw err; + * }); + */ +function quux () {} +// Options: [{"baseConfig":{"rules":{"indent":["error"]}},"eslintrcForExamples":false,"noDefaultExampleRules":false}] ```` @@ -3139,6 +3179,23 @@ function quux () { } // Options: ["never"] // Message: There must be no newline after the description of the JSDoc block. + +/** +* A. +* +* @typedef {Object} A +* @prop {boolean} a A. +*/ +// Options: ["never"] +// Message: There must be no newline after the description of the JSDoc block. + +/** +* A. +* @typedef {Object} A +* @prop {boolean} a A. +*/ +// Options: ["always"] +// Message: There must be a newline after the description of the JSDoc block. ```` The following patterns are not considered problems: @@ -3370,6 +3427,14 @@ class Bar { } } // Message: The type 'TEMPLATE_TYPE' is undefined. + +/** + * @type {strnig} + */ +var quux = { + +}; +// Message: The type 'strnig' is undefined. ```` The following patterns are not considered problems: @@ -5981,6 +6046,8 @@ export class SomeClass { Checks if the return expression exists in function body and in the comment. +Will also report if multiple `@returns` tags are present. + ||| |---|---| |Context|`ArrowFunctionExpression`, `FunctionDeclaration`, `FunctionExpression`| @@ -6440,6 +6507,8 @@ function quux () { Requires returns are documented. +Will also report if multiple `@returns` tags are present. + #### Options From be771ec578257fbd5bdcca5e4afa037a39c9544e Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Sat, 13 Jul 2019 10:33:11 -0700 Subject: [PATCH 40/46] testing(require-param): fix test expectation (part of #332) --- README.md | 2 +- test/rules/assertions/requireParam.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 511559691..6902a573b 100644 --- a/README.md +++ b/README.md @@ -5719,7 +5719,7 @@ export class SomeClass { // Message: Missing JSDoc @param "foo" declaration. /** - * + * @param */ function quux (foo) { diff --git a/test/rules/assertions/requireParam.js b/test/rules/assertions/requireParam.js index 14a6c6da5..be64f9e65 100644 --- a/test/rules/assertions/requireParam.js +++ b/test/rules/assertions/requireParam.js @@ -240,7 +240,7 @@ export default { { code: ` /** - * + * @param */ function quux (foo) { From 9fab87c9a34e394ec516f956b813a500c6c724ea Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Sat, 13 Jul 2019 10:39:11 -0700 Subject: [PATCH 41/46] testing(require-param): fix test source (part of #332) --- README.md | 2 +- test/rules/assertions/requireParam.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6902a573b..e461d93cf 100644 --- a/README.md +++ b/README.md @@ -5606,7 +5606,7 @@ function quux (foo) { // Message: Missing JSDoc @param "foo" declaration. /** - * + * @param */ function quux (foo) { diff --git a/test/rules/assertions/requireParam.js b/test/rules/assertions/requireParam.js index be64f9e65..603df9023 100644 --- a/test/rules/assertions/requireParam.js +++ b/test/rules/assertions/requireParam.js @@ -24,7 +24,7 @@ export default { { code: ` /** - * + * @param */ function quux (foo) { From 71f35e2a832cad1502f445e7c3c7fecf27387331 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Sat, 13 Jul 2019 19:04:41 -0700 Subject: [PATCH 42/46] fix(match-description): ensure `prop` and `property` matching excludes name --- README.md | 21 +++++++++++ src/jsdocUtils.js | 4 ++- test/rules/assertions/matchDescription.js | 44 +++++++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e461d93cf..72b63182e 100644 --- a/README.md +++ b/README.md @@ -2655,6 +2655,17 @@ function quux (foo) { // Options: [{"tags":{"param":true}}] // Message: JSDoc description does not satisfy the regex pattern. +/** + * Foo. + * + * @prop foo foo. + */ +function quux (foo) { + +} +// Options: [{"tags":{"prop":true}}] +// Message: JSDoc description does not satisfy the regex pattern. + /** * Foo. * @@ -3122,6 +3133,16 @@ function quux () { } // Options: [{"tags":{"x-tag":".+"}}] + +/** + * Foo. + * + * @prop foo Foo. + */ +function quux (foo) { + +} +// Options: [{"tags":{"prop":true}}] ```` diff --git a/src/jsdocUtils.js b/src/jsdocUtils.js index fbb65d524..b8abfc52a 100644 --- a/src/jsdocUtils.js +++ b/src/jsdocUtils.js @@ -524,7 +524,9 @@ const filterTags = (tags = [], filter) => { return tags.filter(filter); }; -const tagsWithNamesAndDescriptions = ['param', 'arg', 'argument', 'returns', 'return']; +const tagsWithNamesAndDescriptions = [ + 'param', 'arg', 'argument', 'property', 'prop', 'returns', 'return' +]; const getTagsByType = (tags, tagPreference) => { const descName = getPreferredTagName('description', tagPreference); diff --git a/test/rules/assertions/matchDescription.js b/test/rules/assertions/matchDescription.js index fc2bc3fed..e3f45d5ed 100644 --- a/test/rules/assertions/matchDescription.js +++ b/test/rules/assertions/matchDescription.js @@ -160,6 +160,31 @@ export default { } ] }, + { + code: ` + /** + * Foo. + * + * @prop foo foo. + */ + function quux (foo) { + + } + `, + errors: [ + { + line: 5, + message: 'JSDoc description does not satisfy the regex pattern.' + } + ], + options: [ + { + tags: { + prop: true + } + } + ] + }, { code: ` /** @@ -1086,6 +1111,25 @@ export default { } } ] + }, + { + code: ` + /** + * Foo. + * + * @prop foo Foo. + */ + function quux (foo) { + + } + `, + options: [ + { + tags: { + prop: true + } + } + ] } ] }; From 4d5400f14342beca979f2e1c3e2b3dcfb4d6005a Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Sat, 13 Jul 2019 19:42:04 -0700 Subject: [PATCH 43/46] docs(match-description): indicate application by default to `description`/`desc` and allowance for `property`/`prop`; clarify --- .README/rules/match-description.md | 13 +++++++++---- README.md | 13 +++++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/.README/rules/match-description.md b/.README/rules/match-description.md index 0abd943e3..8ad0616a0 100644 --- a/.README/rules/match-description.md +++ b/.README/rules/match-description.md @@ -8,6 +8,9 @@ by our supported Node versions): ``^([A-Z]|[`\\d_])[\\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. + #### Options ##### `matchDescription` @@ -50,8 +53,10 @@ tag should be linted with the `matchDescription` value (or the default). } ``` -The tags `@param`/`@arg`/`@argument` will be properly parsed to ensure that -the matched "description" text includes only the text after the name. +The tags `@param`/`@arg`/`@argument` and `@property`/`@prop` will be properly +parsed to ensure that the matched "description" text includes only the text +after the name. + All other tags will treat the text following the tag name, a space, and an optional curly-bracketed type expression (and another space) as part of its "description" (e.g., for `@returns {someType} some description`, the @@ -88,8 +93,8 @@ Overrides the default contexts (see below). ||| |---|---| |Context|`ArrowFunctionExpression`, `FunctionDeclaration`, `FunctionExpression`; others when `contexts` option enabled| -|Tags|N/A by default but see `tags` options| +|Tags|docblock and `@description` (or `@desc`) by default but more with `tags`| |Settings|| -|Options|`contexts`, `tags` (allows for 'param', 'arg', 'argument', 'description', 'desc', and any added to `tags` option, e.g., 'returns', 'return'), `mainDescription`, `matchDescription`| +|Options|`contexts`, `tags` (accepts tags with names and optional type such as 'param', 'arg', 'argument', 'property', and 'prop', and accepts arbitrary list of other tags with an optional type (but without names), e.g., 'returns', 'return'), `mainDescription`, `matchDescription`| diff --git a/README.md b/README.md index 72b63182e..aa9d61572 100644 --- a/README.md +++ b/README.md @@ -2492,6 +2492,9 @@ by our supported Node versions): ``^([A-Z]|[`\\d_])[\\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. + #### Options @@ -2537,8 +2540,10 @@ tag should be linted with the `matchDescription` value (or the default). } ``` -The tags `@param`/`@arg`/`@argument` will be properly parsed to ensure that -the matched "description" text includes only the text after the name. +The tags `@param`/`@arg`/`@argument` and `@property`/`@prop` will be properly +parsed to ensure that the matched "description" text includes only the text +after the name. + All other tags will treat the text following the tag name, a space, and an optional curly-bracketed type expression (and another space) as part of its "description" (e.g., for `@returns {someType} some description`, the @@ -2577,9 +2582,9 @@ Overrides the default contexts (see below). ||| |---|---| |Context|`ArrowFunctionExpression`, `FunctionDeclaration`, `FunctionExpression`; others when `contexts` option enabled| -|Tags|N/A by default but see `tags` options| +|Tags|docblock and `@description` (or `@desc`) by default but more with `tags`| |Settings|| -|Options|`contexts`, `tags` (allows for 'param', 'arg', 'argument', 'description', 'desc', and any added to `tags` option, e.g., 'returns', 'return'), `mainDescription`, `matchDescription`| +|Options|`contexts`, `tags` (accepts tags with names and optional type such as 'param', 'arg', 'argument', 'property', and 'prop', and accepts arbitrary list of other tags with an optional type (but without names), e.g., 'returns', 'return'), `mainDescription`, `matchDescription`| The following patterns are considered problems: From b396832a90e0129a1d156f3b1948e8466a818426 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Sat, 13 Jul 2019 19:52:38 -0700 Subject: [PATCH 44/46] docs(match-description): add alias `desc` to separate column --- .README/rules/match-description.md | 3 ++- README.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.README/rules/match-description.md b/.README/rules/match-description.md index 8ad0616a0..bf6a5e14b 100644 --- a/.README/rules/match-description.md +++ b/.README/rules/match-description.md @@ -93,7 +93,8 @@ Overrides the default contexts (see below). ||| |---|---| |Context|`ArrowFunctionExpression`, `FunctionDeclaration`, `FunctionExpression`; others when `contexts` option enabled| -|Tags|docblock and `@description` (or `@desc`) by default but more with `tags`| +|Tags|docblock and `@description` by default but more with `tags`| +|Aliases|`@desc`| |Settings|| |Options|`contexts`, `tags` (accepts tags with names and optional type such as 'param', 'arg', 'argument', 'property', and 'prop', and accepts arbitrary list of other tags with an optional type (but without names), e.g., 'returns', 'return'), `mainDescription`, `matchDescription`| diff --git a/README.md b/README.md index aa9d61572..c7de9ab41 100644 --- a/README.md +++ b/README.md @@ -2582,7 +2582,8 @@ Overrides the default contexts (see below). ||| |---|---| |Context|`ArrowFunctionExpression`, `FunctionDeclaration`, `FunctionExpression`; others when `contexts` option enabled| -|Tags|docblock and `@description` (or `@desc`) by default but more with `tags`| +|Tags|docblock and `@description` by default but more with `tags`| +|Aliases|`@desc`| |Settings|| |Options|`contexts`, `tags` (accepts tags with names and optional type such as 'param', 'arg', 'argument', 'property', and 'prop', and accepts arbitrary list of other tags with an optional type (but without names), e.g., 'returns', 'return'), `mainDescription`, `matchDescription`| From 2e2af0d1c795698fc24de41ef5576c2ec1de943f Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Sat, 13 Jul 2019 20:04:50 -0700 Subject: [PATCH 45/46] docs(newline-after-description): indicate applies on doc block --- .README/rules/newline-after-description.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.README/rules/newline-after-description.md b/.README/rules/newline-after-description.md index 20432e364..b01e383d5 100644 --- a/.README/rules/newline-after-description.md +++ b/.README/rules/newline-after-description.md @@ -10,6 +10,6 @@ This rule allows one optional string argument. If it is `"always"` then a proble |---|---| |Context|everywhere| |Options|(a string matching `"always"|"never"`)| -|Tags|N/A| +|Tags|N/A (doc block)| diff --git a/README.md b/README.md index c7de9ab41..160047f38 100644 --- a/README.md +++ b/README.md @@ -3166,7 +3166,7 @@ This rule allows one optional string argument. If it is `"always"` then a proble |---|---| |Context|everywhere| |Options|(a string matching `"always"|"never"`)| -|Tags|N/A| +|Tags|N/A (doc block)| The following patterns are considered problems: From 0eb7a0c3bf18ac92a4cde392ee64f29a30804639 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Sat, 13 Jul 2019 19:42:04 -0700 Subject: [PATCH 46/46] feat(require-description-complete-sentence): limit checking to certain default tags likely to have descriptions or by `tags` array for additional choices; fixes #337 Tags that are checked for description by default are: 'param', 'arg', 'argument', 'property', 'prop', 'returns', 'return', 'summary', 'file', 'fileoverview', 'overview', 'classdesc', 'todo', 'deprecated', 'throws', 'exception', 'yields', 'yield'. Note that `see` and `copyright` are not included by default because of potentially allowing a non-description or potential sensitivity, respectively. --- .../require-description-complete-sentence.md | 29 ++++- README.md | 111 +++++++++++++++--- src/jsdocUtils.js | 5 +- .../requireDescriptionCompleteSentence.js | 39 +++++- .../requireDescriptionCompleteSentence.js | 104 +++++++++++++++- 5 files changed, 267 insertions(+), 21 deletions(-) diff --git a/.README/rules/require-description-complete-sentence.md b/.README/rules/require-description-complete-sentence.md index 30a016ebf..eed1985cb 100644 --- a/.README/rules/require-description-complete-sentence.md +++ b/.README/rules/require-description-complete-sentence.md @@ -9,10 +9,33 @@ tag descriptions are written in complete sentences, i.e., * Every line in a paragraph (except the first) which starts with an uppercase character must be preceded by a line ending with a period. +#### Options + +##### `tags` + +If you want additional tags to be checked for their descriptions, you may +add them within this option. + +```js +{ + 'jsdoc/require-description-complete-sentence': ['error', {tags: ['see', 'copyright']}] +} +``` + +The tags `@param`/`@arg`/`@argument` and `@property`/`@prop` will be properly +parsed to ensure that the checked "description" text includes only the text +after the name. + +All other tags will treat the text following the tag name, a space, and +an optional curly-bracketed type expression (and another space) as part of +its "description" (e.g., for `@returns {someType} some description`, the +description is `some description` while for `@some-tag xyz`, the description +is `xyz`). + ||| |---|---| |Context|everywhere| -|Tags|`param`, `returns`, `description`| -|Aliases|`arg`, `argument`, `return`, `desc`| - +|Tags|doc block, `param`, `returns`, `description`, `property`, `summary`, `file`, `classdesc`, `todo`, `deprecated`, `throws`, 'yields' and others added by `tags`| +|Aliases|`arg`, `argument`, `return`, `desc`, `prop`, `fileoverview`, `overview`, `exception`, `yield`| +|Options|`tags`| diff --git a/README.md b/README.md index 160047f38..35ebe5a63 100644 --- a/README.md +++ b/README.md @@ -3666,12 +3666,37 @@ tag descriptions are written in complete sentences, i.e., * Every line in a paragraph (except the first) which starts with an uppercase character must be preceded by a line ending with a period. + +#### Options + + +##### tags + +If you want additional tags to be checked for their descriptions, you may +add them within this option. + +```js +{ + 'jsdoc/require-description-complete-sentence': ['error', {tags: ['see', 'copyright']}] +} +``` + +The tags `@param`/`@arg`/`@argument` and `@property`/`@prop` will be properly +parsed to ensure that the checked "description" text includes only the text +after the name. + +All other tags will treat the text following the tag name, a space, and +an optional curly-bracketed type expression (and another space) as part of +its "description" (e.g., for `@returns {someType} some description`, the +description is `some description` while for `@some-tag xyz`, the description +is `xyz`). + ||| |---|---| |Context|everywhere| -|Tags|`param`, `returns`, `description`| -|Aliases|`arg`, `argument`, `return`, `desc`| - +|Tags|doc block, `param`, `returns`, `description`, `property`, `summary`, `file`, `classdesc`, `todo`, `deprecated`, `throws`, 'yields' and others added by `tags`| +|Aliases|`arg`, `argument`, `return`, `desc`, `prop`, `fileoverview`, `overview`, `exception`, `yield`| +|Options|`tags`| The following patterns are considered problems: ````js @@ -3846,10 +3871,35 @@ function quux (foo) { // Message: Sentence should start with an uppercase character. /** - * @typedef {Object} Hello World + * @throws {Object} Hello World * hello world */ // Message: Sentence must end with a period. + +/** + * @summary Foo + */ +function quux () { + +} +// Message: Sentence must end with a period. + +/** + * @throws {SomeType} Foo + */ +function quux () { + +} +// Message: Sentence must end with a period. + +/** + * @see Foo + */ +function quux () { + +} +// Options: [{"tags":["see"]}] +// Message: Sentence must end with a period. ```` The following patterns are not considered problems: @@ -4001,6 +4051,39 @@ function quux () { function quux () { } + +/** + * @example Foo + */ +function quux () { + +} + +/** + * @see Foo + */ +function quux () { + +} + +/** + * Foo. + * + * @param foo Foo. + */ +function quux (foo) { + +} + +/** + * Foo. + * + * @param foo Foo. + */ +function quux (foo) { + +} +// Options: [{"tags":["param"]}] ```` @@ -4015,7 +4098,7 @@ Requires that all functions have a description. `"tag"`) must have a non-empty description that explains the purpose of the method. - + #### Options An options object may have any of the following properties: @@ -4274,25 +4357,25 @@ Requires that all functions have examples. * All functions must have one or more `@example` tags. * Every example tag must have a non-empty description that explains the method's usage. - + #### Options This rule has an object option. - + ##### exemptedBy Array of tags (e.g., `['type']`) whose presence on the document block avoids the need for an `@example`. Defaults to an empty array. - + ##### avoidExampleOnConstructors Set to `true` to avoid the need for an example on a constructor (whether indicated as such by a jsdoc tag or by being within an ES6 `class`). Defaults to `false`. - + ##### contexts Set this to an array of strings representing the AST context @@ -4456,7 +4539,7 @@ function quux () { Requires a hyphen before the `@param` description. - + #### Options This rule takes one optional string argument. If it is `"always"` then a problem is raised when there is no hyphen before the description. If it is `"never"` then a problem is raised when there is a hyphen before the description. The default value is `"always"`. @@ -4562,7 +4645,7 @@ function quux () { Checks for presence of jsdoc comments, on class declarations as well as functions. - + #### Options Accepts one optional options object with the following optional keys. @@ -5605,7 +5688,7 @@ function quux (foo) { Requires that all function parameters are documented. - + #### Options An options object accepts one optional property: @@ -6536,7 +6619,7 @@ Requires returns are documented. Will also report if multiple `@returns` tags are present. - + #### Options - `exemptedBy` - Array of tags (e.g., `['type']`) whose presence on the document @@ -6992,7 +7075,7 @@ Also impacts behaviors on namepath (or event)-defining and pointing tags: allow `#`, `.`, or `~` at the end (which is not allowed at the end of normal paths). - + #### Options - `allowEmptyNamepaths` (default: true) - Set to `false` to disallow diff --git a/src/jsdocUtils.js b/src/jsdocUtils.js index b8abfc52a..b1624aff0 100644 --- a/src/jsdocUtils.js +++ b/src/jsdocUtils.js @@ -525,7 +525,10 @@ const filterTags = (tags = [], filter) => { }; const tagsWithNamesAndDescriptions = [ - 'param', 'arg', 'argument', 'property', 'prop', 'returns', 'return' + 'param', 'arg', 'argument', 'property', 'prop', + + // These two are parsed by our custom parser as though having a `name` + 'returns', 'return' ]; const getTagsByType = (tags, tagPreference) => { diff --git a/src/rules/requireDescriptionCompleteSentence.js b/src/rules/requireDescriptionCompleteSentence.js index c6542c71c..1f0543eec 100644 --- a/src/rules/requireDescriptionCompleteSentence.js +++ b/src/rules/requireDescriptionCompleteSentence.js @@ -124,6 +124,7 @@ export default iterateJsdoc(({ jsdoc, report, jsdocNode, + context, utils }) => { if (!jsdoc.tags || @@ -139,7 +140,27 @@ export default iterateJsdoc(({ validateDescription(description, report, jsdocNode, sourceCode, matchingJsdocTag); }); - const {tagsWithNames, tagsWithoutNames} = utils.getTagsByType(jsdoc.tags); + const options = context.options[0] || {}; + + const hasOptionTag = (tagName) => { + return Boolean(options.tags && options.tags.includes(tagName)); + }; + + const {tagsWithNames} = utils.getTagsByType(jsdoc.tags); + const tagsWithoutNames = utils.filterTags(({tag: tagName}) => { + return [ + // 'copyright' and 'see' might be good addition, but as the former may be + // sensitive text, and the latter may have just a link, they are not + // included by default + 'summary', 'file', 'fileoverview', 'overview', 'classdesc', 'todo', + 'deprecated', 'throws', 'exception', 'yields', 'yield' + ].includes(tagName) || + hasOptionTag(tagName) && !tagsWithNames.some(({tag}) => { + // If user accidentally adds tags with names (or like `returns` + // get parsed as having names), do not add to this list + return tag === tagName; + }); + }); tagsWithNames.some((tag) => { const description = _.trimStart(tag.description, '- '); @@ -148,7 +169,7 @@ export default iterateJsdoc(({ }); tagsWithoutNames.some((tag) => { - const description = (tag.name + ' ' + tag.description).trim(); + const description = `${tag.name} ${tag.description}`.trim(); return validateDescription(description, report, jsdocNode, sourceCode, tag); }); @@ -156,6 +177,20 @@ export default iterateJsdoc(({ iterateAllJsdocs: true, meta: { fixable: 'code', + schema: [ + { + additionalProperties: false, + properties: { + tags: { + items: { + type: 'string' + }, + type: 'array' + } + }, + type: 'object' + } + ], type: 'suggestion' } }); diff --git a/test/rules/assertions/requireDescriptionCompleteSentence.js b/test/rules/assertions/requireDescriptionCompleteSentence.js index 1505f692a..256eeed1a 100644 --- a/test/rules/assertions/requireDescriptionCompleteSentence.js +++ b/test/rules/assertions/requireDescriptionCompleteSentence.js @@ -504,7 +504,7 @@ export default { { code: ` /** - * @typedef {Object} Hello World + * @throws {Object} Hello World * hello world */ `, @@ -514,6 +514,59 @@ export default { message: 'Sentence must end with a period.' } ] + }, + { + code: ` + /** + * @summary Foo + */ + function quux () { + + } + `, + errors: [ + { + line: 3, + message: 'Sentence must end with a period.' + } + ] + }, + { + code: ` + /** + * @throws {SomeType} Foo + */ + function quux () { + + } + `, + errors: [ + { + line: 3, + message: 'Sentence must end with a period.' + } + ] + }, + { + code: ` + /** + * @see Foo + */ + function quux () { + + } + `, + errors: [ + { + line: 3, + message: 'Sentence must end with a period.' + } + ], + options: [ + { + tags: ['see'] + } + ] } ], valid: [ @@ -721,6 +774,55 @@ export default { } ` + }, + { + code: ` + /** + * @example Foo + */ + function quux () { + + } + ` + }, + { + code: ` + /** + * @see Foo + */ + function quux () { + + } + ` + }, + { + code: ` + /** + * Foo. + * + * @param foo Foo. + */ + function quux (foo) { + + } + ` + }, + { + code: ` + /** + * Foo. + * + * @param foo Foo. + */ + function quux (foo) { + + } + `, + options: [ + { + tags: ['param'] + } + ] } ] };