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