diff --git a/.README/rules/check-examples.md b/.README/rules/check-examples.md index d038624ec..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 @@ -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 @@ -98,7 +116,7 @@ decreasing precedence: ||| |---|---| -|Context|`ArrowFunctionExpression`, `ClassDeclaration`, `FunctionDeclaration`, `FunctionExpression`| +|Context|everywhere| |Tags|`example`| |Options| *See above* | diff --git a/.README/rules/match-description.md b/.README/rules/match-description.md index 0abd943e3..bf6a5e14b 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,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` by default but more with `tags`| +|Aliases|`@desc`| |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/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/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/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/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 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 diff --git a/README.md b/README.md index d165998db..56c388ff4 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 @@ -566,7 +585,7 @@ decreasing precedence: ||| |---|---| -|Context|`ArrowFunctionExpression`, `ClassDeclaration`, `FunctionDeclaration`, `FunctionExpression`| +|Context|everywhere| |Tags|`example`| |Options| *See above* | @@ -604,6 +623,7 @@ function quux () { /** * @example + * * ```js alert('hello'); ``` */ function quux () { @@ -612,6 +632,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'); ``` @@ -634,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. /** @@ -686,7 +716,7 @@ function quux () {} /** * @example const i = 5; - * quux2() + * quux2() */ function quux2 () { @@ -696,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 () { @@ -705,7 +746,7 @@ function quux2 () { /** * @example const i = 5; - * quux2() + * quux2() */ function quux2 () { @@ -732,6 +773,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: @@ -797,6 +846,25 @@ 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]*)```"}] + +/** + * @example + * foo(function (err) { + * throw err; + * }); + */ +function quux () {} +// Options: [{"baseConfig":{"rules":{"indent":["error"]}},"eslintrcForExamples":false,"noDefaultExampleRules":false}] ```` @@ -813,6 +881,12 @@ Reports invalid padding inside JSDoc block. The following patterns are considered problems: ````js +/*** foo */ +function quux () { + +} +// Message: There must be no indentation. + /** * foo * @@ -843,6 +917,11 @@ The following patterns are not considered problems: */ function quux () { +} + +/*** foo */ +function quux () { + } ```` @@ -1348,6 +1427,36 @@ 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". + +/** + * @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: @@ -2383,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 @@ -2428,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 @@ -2468,9 +2582,10 @@ 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` by default but more with `tags`| +|Aliases|`@desc`| |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: @@ -2546,6 +2661,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. * @@ -3013,6 +3139,16 @@ function quux () { } // Options: [{"tags":{"x-tag":".+"}}] + +/** + * Foo. + * + * @prop foo Foo. + */ +function quux (foo) { + +} +// Options: [{"tags":{"prop":true}}] ```` @@ -3030,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: @@ -3070,6 +3206,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: @@ -3301,6 +3454,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: @@ -3505,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 @@ -3522,6 +3708,14 @@ function quux () { } // Message: Sentence should start with an uppercase character. +/** + * foo? + */ +function quux () { + +} +// Message: Sentence should start with an uppercase character. + /** * @description foo. */ @@ -3538,6 +3732,14 @@ function quux () { } // Message: Sentence must end with a period. +/** + * `foo` is a variable + */ +function quux () { + +} +// Message: Sentence must end with a period. + /** * Foo. * @@ -3667,6 +3869,37 @@ function quux (foo) { } // Message: Sentence should start with an uppercase character. + +/** + * @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: @@ -3780,6 +4013,77 @@ 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 () { + +} + +/** + * @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"]}] ```` @@ -3794,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: @@ -4053,23 +4357,36 @@ 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: +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 +4433,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 +4515,22 @@ function quux () { } // Options: [{"exemptedBy":["type"]}] + +/** + * @example Some example code + */ +class quux { + +} +// Options: [{"contexts":["ClassDeclaration"]}] + +/** + * + */ +function quux () { + +} +// Options: [{"contexts":["ClassDeclaration"]}] ```` @@ -4197,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"`. @@ -4303,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. @@ -5346,7 +5688,7 @@ function quux (foo) { Requires that all function parameters are documented. - + #### Options An options object accepts one optional property: @@ -5374,7 +5716,7 @@ function quux (foo) { // Message: Missing JSDoc @param "foo" declaration. /** - * + * @param */ function quux (foo) { @@ -5487,7 +5829,7 @@ export class SomeClass { // Message: Missing JSDoc @param "foo" declaration. /** - * + * @param */ function quux (foo) { @@ -5814,6 +6156,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`| @@ -6273,7 +6617,9 @@ function quux () { 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 @@ -6729,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/package.json b/package.json index 90d927af8..c780bfc41 100644 --- a/package.json +++ b/package.json @@ -7,33 +7,33 @@ "dependencies": { "comment-parser": "^0.5.5", "debug": "^4.1.1", - "escape-regex-string": "^1.0.6", "flat-map-polyfill": "^0.3.8", - "jsdoctypeparser": "4.0.0", - "lodash": "^4.17.11" + "jsdoctypeparser": "5.0.1", + "lodash": "^4.17.14", + "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.0", + "@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.1", - "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", "nyc": "^14.1.1", "semantic-release": "^15.13.18", - "typescript": "^3.5.2" + "typescript": "^3.5.3" }, "engines": { "node": ">=6" @@ -63,6 +63,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", diff --git a/src/eslint/getJSDocComment.js b/src/eslint/getJSDocComment.js index 1065be6f0..49fef7ca1 100644 --- a/src/eslint/getJSDocComment.js +++ b/src/eslint/getJSDocComment.js @@ -1,5 +1,6 @@ /** - * 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 */ diff --git a/src/iterateJsdoc.js b/src/iterateJsdoc.js index ea5c2e39e..8ddc94edf 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 @@ -18,7 +24,7 @@ const parseComment = (commentNode, indent) => { return commentParser.PARSERS.parse_type(str, data); }, (str, data) => { - if (['return', 'returns', 'throws', 'exception'].includes(data.tag)) { + if (['example', 'return', 'returns', 'throws', 'exception'].includes(data.tag)) { return null; } if (data.tag === 'see' && str.match(/{@link.+?}/)) { @@ -27,8 +33,39 @@ const parseComment = (commentNode, indent) => { 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] || {}; }; @@ -178,11 +215,19 @@ 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 = () => { - 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') { @@ -197,7 +242,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); } @@ -225,6 +270,18 @@ const getUtils = ( }); }; + utils.reportSettings = (message) => { + context.report({ + loc: { + start: { + column: 1, + line: 1 + } + }, + message + }); + }; + return utils; }; @@ -251,8 +308,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) => { @@ -314,16 +371,17 @@ const iterateAllJsdocs = (iterator, ruleConfig) => { return { create (context) { return { - 'Program:exit' () { - const comments = context.getSourceCode().getAllComments(); + 'Program' () { + const sourceCode = context.getSourceCode(); + const comments = sourceCode.getAllComments(); comments.forEach((comment) => { - if (!context.getSourceCode().getText(comment).startsWith('/**')) { + if (!sourceCode.getText(comment).startsWith('/**')) { return; } - const indent = _.repeat(' ', comment.loc.start.column); - const jsdoc = parseComment(comment, indent); + const indent = ' '.repeat(comment.loc.start.column); + const jsdoc = parseComment(comment, indent, !ruleConfig.noTrim); const settings = getSettings(context); const report = makeReport(context, comment); const jsdocNode = comment; @@ -335,8 +393,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) }); }); @@ -356,7 +414,6 @@ export { * @param {{ * meta: any, * contextDefaults?: true | string[], - * returns?: string[], * iterateAllJsdocs?: true, * }} ruleConfig */ @@ -365,12 +422,15 @@ 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) { - return iterateAllJsdocs(iterator, {meta: ruleConfig.meta}); + return iterateAllJsdocs(iterator, { + meta: ruleConfig.meta, + noTrim: ruleConfig.noTrim + }); } return { @@ -380,7 +440,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) { @@ -388,11 +448,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); @@ -400,7 +455,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); @@ -434,15 +489,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 }; diff --git a/src/jsdocUtils.js b/src/jsdocUtils.js index 67c91271a..4ba3445a6 100644 --- a/src/jsdocUtils.js +++ b/src/jsdocUtils.js @@ -475,9 +475,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} @@ -551,13 +551,58 @@ const getContextObject = (contexts, checkJsdoc) => { }, {}); }; +const filterTags = (tags = [], filter) => { + return tags.filter(filter); +}; + +const tagsWithNamesAndDescriptions = [ + 'param', 'arg', 'argument', 'property', 'prop', + + // These two are parsed by our custom parser as though having a `name` + '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 + }; +}; + +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, getJsdocParameterNamesDeep, getPreferredTagName, + getTagsByType, hasATag, hasDefinedTypeReturnTag, hasReturnValue, 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/checkExamples.js b/src/rules/checkExamples.js index f735160cd..87ab72de7 100644 --- a/src/rules/checkExamples.js +++ b/src/rules/checkExamples.js @@ -1,12 +1,11 @@ import {CLIEngine, Linter} from 'eslint'; -import escapeRegexString from 'escape-regex-string'; import iterateJsdoc from '../iterateJsdoc'; 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, '\\$&'); @@ -30,6 +29,7 @@ export default iterateJsdoc(({ noDefaultExampleRules = false, eslintrcForExamples = true, matchingFileName: filename = null, + paddedIndent = 0, baseConfig = {}, configFile, allowInlineConfig = true, @@ -72,16 +72,12 @@ 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) { + if (captionRequired && (!match || !match[1].trim())) { report('Caption is expected for examples.', null, tag); } @@ -101,16 +97,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 +129,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; } @@ -157,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); @@ -231,6 +229,7 @@ export default iterateJsdoc(({ }); }); }, { + iterateAllJsdocs: true, meta: { schema: [ { @@ -264,6 +263,10 @@ export default iterateJsdoc(({ default: false, type: 'boolean' }, + paddedIndent: { + default: 0, + type: 'integer' + }, rejectExampleCodeRegex: { type: 'string' }, @@ -277,10 +280,5 @@ export default iterateJsdoc(({ ], type: 'suggestion' }, - returns: [ - 'ArrowFunctionExpression', - 'ClassDeclaration', - 'FunctionDeclaration', - 'FunctionExpression' - ] + noTrim: true }); diff --git a/src/rules/checkIndentation.js b/src/rules/checkIndentation.js index 732e61f07..d0e2d2c98 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/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/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/checkTagNames.js b/src/rules/checkTagNames.js index 04526bf17..529901ee2 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') { @@ -28,7 +31,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.' ); } @@ -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, @@ -57,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/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/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/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, '- '); diff --git a/src/rules/newlineAfterDescription.js b/src/rules/newlineAfterDescription.js index 6d8960ea5..e06471ea1 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.replace(/^\s*\*\s*/, '') === _.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.replace(/^\s*\*\s*/, '') === _.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/src/rules/noUndefinedTypes.js b/src/rules/noUndefinedTypes.js index 8c2e12eed..860026915 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.' ); } @@ -132,6 +132,7 @@ export default iterateJsdoc(({ }); }); }, { + iterateAllJsdocs: true, meta: { schema: [ { diff --git a/src/rules/requireDescriptionCompleteSentence.js b/src/rules/requireDescriptionCompleteSentence.js index bf7f759ac..1f0543eec 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, ''); - // Ignore sentences with only whitespaces. - .filter((sentence) => { - return !/^\s*$/.test(sentence); - }) + const sentenceEndGrouping = /([.?!])(?:\s+|$)/; + const puncts = RegExtras(sentenceEndGrouping).map(txt, (punct) => { + return punct; + }); + + return txt + + .split(/[.?!](?:\s+|$)/) // Re-add the dot. - .map((sentence) => { - return `${sentence}.`; + .map((sentence, idx) => { + return /^\s*$/.test(sentence) ? sentence : `${sentence}${puncts[idx] || ''}`; }); }; @@ -47,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) => { @@ -67,12 +71,12 @@ 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]; - 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); @@ -85,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 !isCapitalized(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; } @@ -112,32 +124,73 @@ export default iterateJsdoc(({ jsdoc, report, jsdocNode, + context, 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) => { - return ['param', 'arg', 'argument', 'returns', 'return'].includes(tag.tag); + 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; + }); }); - tags.some((tag) => { + tagsWithNames.some((tag) => { const description = _.trimStart(tag.description, '- '); - return validateDescription(description, report, jsdocNode, sourceCode, tag.tag); + 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: { fixable: 'code', + schema: [ + { + additionalProperties: false, + properties: { + tags: { + items: { + type: 'string' + }, + type: 'array' + } + }, + type: 'object' + } + ], type: 'suggestion' } }); 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/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) { 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(); - }); }); }); }); 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/checkExamples.js b/test/rules/assertions/checkExamples.js index d6bd7138c..2cc6fc69a 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 () { @@ -107,6 +108,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: ` /** @@ -159,7 +185,7 @@ export default { } }, eslintrcForExamples: false, - rejectExampleCodeRegex: '^\\s*<.*>$' + rejectExampleCodeRegex: '^\\s*<.*>\\s*$' }] }, { @@ -280,7 +306,7 @@ export default { code: ` /** * @example const i = 5; - * quux2() + * quux2() */ function quux2 () { @@ -302,7 +328,32 @@ export default { code: ` /** * @example const i = 5; - * quux2() + * 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 () { @@ -321,7 +372,7 @@ export default { code: ` /** * @example const i = 5; - * quux2() + * quux2() */ function quux2 () { @@ -418,6 +469,24 @@ export default { reportUnusedDisableDirectives: true } } + }, + { + code: ` + /** + * @typedef {string} Foo + * @example + * 'foo' + */ + `, + errors: [ + { + message: 'Caption is expected for examples.' + } + ], + options: [{ + captionRequired: true, + eslintrcForExamples: false + }] } ], valid: [ @@ -544,6 +613,47 @@ 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]*)```' + }] + }, + { + code: ` + /** + * @example + * foo(function (err) { + * throw err; + * }); + */ + function quux () {} +`, + options: [{ + baseConfig: { + rules: { + indent: ['error'] + } + }, + eslintrcForExamples: false, + noDefaultExampleRules: false + }] } ] }; diff --git a/test/rules/assertions/checkIndentation.js b/test/rules/assertions/checkIndentation.js index 0a18721c1..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: ` /** @@ -14,6 +28,7 @@ export default { `, errors: [ { + line: 6, message: 'There must be no indentation.' } ] @@ -28,6 +43,7 @@ export default { `, errors: [ { + line: 4, message: 'There must be no indentation.' } ] @@ -44,6 +60,14 @@ export default { */ function quux () { + } + ` + }, + { + code: ` + /*** foo */ + function quux () { + } ` } 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`' } ], 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/checkTagNames.js b/test/rules/assertions/checkTagNames.js index 9725364bf..fb09222e0 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.' }, { @@ -308,6 +309,103 @@ 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 () { + + } + ` + }, + { + 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' + } + } + } + }, + { + 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: [ 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/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' } ] 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 + } + } + ] } ] }; diff --git a/test/rules/assertions/newlineAfterDescription.js b/test/rules/assertions/newlineAfterDescription.js index cf2845205..fb2f99f96 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.' } ], @@ -95,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: [ diff --git a/test/rules/assertions/noUndefinedTypes.js b/test/rules/assertions/noUndefinedTypes.js index 60d98e47b..5afe23872 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.' }, { @@ -207,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: [ diff --git a/test/rules/assertions/requireDescriptionCompleteSentence.js b/test/rules/assertions/requireDescriptionCompleteSentence.js index e61b5438a..256eeed1a 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.' } ], @@ -23,6 +24,29 @@ export default { } ` }, + { + code: ` + /** + * foo? + */ + function quux () { + + } + `, + errors: [ + { + message: 'Sentence should start with an uppercase character.' + } + ], + output: ` + /** + * Foo? + */ + function quux () { + + } + ` + }, { code: ` /** @@ -34,6 +58,7 @@ export default { `, errors: [ { + line: 3, message: 'Sentence should start with an uppercase character.' } ], @@ -57,6 +82,7 @@ export default { `, errors: [ { + line: 3, message: 'Sentence must end with a period.' } ], @@ -69,6 +95,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: ` /** @@ -82,6 +131,7 @@ export default { `, errors: [ { + line: 5, message: 'Sentence should start with an uppercase character.' } ], @@ -107,6 +157,7 @@ export default { `, errors: [ { + line: 3, message: 'Sentence should start with an uppercase character.' } ], @@ -130,6 +181,7 @@ export default { `, errors: [ { + line: 3, message: 'Sentence must end with a period.' } ], @@ -154,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.' } ] @@ -171,6 +224,7 @@ export default { `, errors: [ { + line: 5, message: 'Sentence should start with an uppercase character.' } ], @@ -198,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.' } ], @@ -226,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.' } ], @@ -254,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.' } ], @@ -284,6 +344,7 @@ export default { `, errors: [ { + line: 5, message: 'Sentence should start with an uppercase character.' } ], @@ -314,6 +375,7 @@ export default { `, errors: [ { + line: 3, message: 'Sentence should start with an uppercase character.' } ], @@ -342,6 +404,7 @@ export default { `, errors: [ { + line: 3, message: 'Sentence must end with a period.' } ], @@ -365,6 +428,7 @@ export default { `, errors: [ { + line: 3, message: 'Sentence must end with a period.' } ], @@ -388,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.' } ], @@ -416,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.' } ], @@ -432,6 +500,73 @@ export default { } ` + }, + { + code: ` + /** + * @throws {Object} Hello World + * hello world + */ + `, + errors: [ + { + line: 3, + 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: [ @@ -586,6 +721,108 @@ export default { } ` + }, + { + 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 () { + + } + ` + }, + { + 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'] + } + ] } ] }; 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'] + } + ] } ] }; diff --git a/test/rules/assertions/requireParam.js b/test/rules/assertions/requireParam.js index 14a6c6da5..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) { @@ -240,7 +240,7 @@ export default { { code: ` /** - * + * @param */ function quux (foo) {