diff --git a/.README/rules/check-examples.md b/.README/rules/check-examples.md
index 49d4818bf..c420759e9 100644
--- a/.README/rules/check-examples.md
+++ b/.README/rules/check-examples.md
@@ -37,6 +37,24 @@ If neither is in use, all examples will be matched. Note also that even if
`captionRequired` is not set, any initial `
` will be stripped out
before doing the regex matching.
+#### `paddedIndent`
+
+This integer property allows one to add a fixed amount of whitespace at the
+beginning of the second or later lines of the example to be stripped so as
+to avoid linting issues with the decorative whitespace. For example, if set
+to a value of `4`, the initial whitespace below will not trigger `indent`
+rule errors as the extra 4 spaces on each subsequent line will be stripped
+out before evaluation.
+
+```js
+/**
+ * @example
+ * anArray.filter((a) => {
+ * return a.b;
+ * });
+ */
+```
+
#### `reportUnusedDisableDirectives`
If not set to `false`, `reportUnusedDisableDirectives` will report disabled
diff --git a/package.json b/package.json
index 730de6c3d..7edea057a 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,6 @@
"dependencies": {
"comment-parser": "^0.5.5",
"debug": "^4.1.1",
- "escape-regex-string": "^1.0.6",
"flat-map-polyfill": "^0.3.8",
"jsdoctypeparser": "5.0.1",
"lodash": "^4.17.14",
diff --git a/src/iterateJsdoc.js b/src/iterateJsdoc.js
index f6e8a0f9d..42de5b5b7 100644
--- a/src/iterateJsdoc.js
+++ b/src/iterateJsdoc.js
@@ -3,7 +3,13 @@ import commentParser from 'comment-parser';
import jsdocUtils from './jsdocUtils';
import getJSDocComment from './eslint/getJSDocComment';
-const parseComment = (commentNode, indent) => {
+/**
+ *
+ * @param {object} commentNode
+ * @param {string} indent Whitespace
+ * @returns {object}
+ */
+const parseComment = (commentNode, indent, trim = true) => {
// Preserve JSDoc block start/end indentation.
return commentParser(`${indent}/*${commentNode.value}${indent}*/`, {
// @see https://github.com/yavorskiy/comment-parser/issues/21
@@ -11,14 +17,45 @@ const parseComment = (commentNode, indent) => {
commentParser.PARSERS.parse_tag,
commentParser.PARSERS.parse_type,
(str, data) => {
- if (['return', 'returns', 'throws', 'exception'].includes(data.tag)) {
+ if (['example', 'return', 'returns', 'throws', 'exception'].includes(data.tag)) {
return null;
}
return commentParser.PARSERS.parse_name(str, data);
},
- commentParser.PARSERS.parse_description
- ]
+ trim ?
+ commentParser.PARSERS.parse_description :
+
+ // parse_description
+ (str, data) => {
+ // Only expected throw in previous step is if bad name (i.e.,
+ // missing end bracket on optional name), but `@example`
+ // skips name parsing
+ /* istanbul ignore next */
+ if (data.errors && data.errors.length) {
+ return null;
+ }
+
+ // Tweak original regex to capture only single optional space
+ const result = str.match(/^\s?((.|\s)+)?/);
+
+ // Always has at least whitespace due to `indent` we've added
+ /* istanbul ignore next */
+ if (result) {
+ return {
+ data: {
+ description: result[1] === undefined ? '' : result[1]
+ },
+ source: result[0]
+ };
+ }
+
+ // Always has at least whitespace due to `indent` we've added
+ /* istanbul ignore next */
+ return null;
+ }
+ ],
+ trim
})[0] || {};
};
@@ -322,7 +359,7 @@ const iterateAllJsdocs = (iterator, ruleConfig) => {
}
const indent = ' '.repeat(comment.loc.start.column);
- const jsdoc = parseComment(comment, indent);
+ const jsdoc = parseComment(comment, indent, !ruleConfig.noTrim);
const settings = getSettings(context);
const report = makeReport(context, comment);
const jsdocNode = comment;
@@ -368,7 +405,10 @@ export default function iterateJsdoc (iterator, ruleConfig) {
}
if (ruleConfig.iterateAllJsdocs) {
- return iterateAllJsdocs(iterator, {meta: ruleConfig.meta});
+ return iterateAllJsdocs(iterator, {
+ meta: ruleConfig.meta,
+ noTrim: ruleConfig.noTrim
+ });
}
return {
diff --git a/src/rules/checkExamples.js b/src/rules/checkExamples.js
index 483cabb44..87ab72de7 100644
--- a/src/rules/checkExamples.js
+++ b/src/rules/checkExamples.js
@@ -1,5 +1,4 @@
import {CLIEngine, Linter} from 'eslint';
-import escapeRegexString from 'escape-regex-string';
import iterateJsdoc from '../iterateJsdoc';
import warnRemovedSettings from '../warnRemovedSettings';
@@ -30,6 +29,7 @@ export default iterateJsdoc(({
noDefaultExampleRules = false,
eslintrcForExamples = true,
matchingFileName: filename = null,
+ paddedIndent = 0,
baseConfig = {},
configFile,
allowInlineConfig = true,
@@ -72,13 +72,9 @@ export default iterateJsdoc(({
utils.forEachPreferredTag('example', (tag, targetTagName) => {
// If a space is present, we should ignore it
- const initialTag = tag.source.match(
- new RegExp(`^@${escapeRegexString(targetTagName)} ?`, 'u')
- );
- const initialTagLength = initialTag[0].length;
- const firstLinePrefixLength = preTagSpaceLength + initialTagLength;
+ const firstLinePrefixLength = preTagSpaceLength;
- let source = tag.source.slice(initialTagLength);
+ let source = tag.description;
const match = source.match(hasCaptionRegex);
if (captionRequired && (!match || !match[1].trim())) {
@@ -101,16 +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);
@@ -265,6 +263,10 @@ export default iterateJsdoc(({
default: false,
type: 'boolean'
},
+ paddedIndent: {
+ default: 0,
+ type: 'integer'
+ },
rejectExampleCodeRegex: {
type: 'string'
},
@@ -277,5 +279,6 @@ export default iterateJsdoc(({
}
],
type: 'suggestion'
- }
+ },
+ noTrim: true
});
diff --git a/test/rules/assertions/checkExamples.js b/test/rules/assertions/checkExamples.js
index fe25703ab..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 () {
@@ -184,7 +185,7 @@ export default {
}
},
eslintrcForExamples: false,
- rejectExampleCodeRegex: '^\\s*<.*>$'
+ rejectExampleCodeRegex: '^\\s*<.*>\\s*$'
}]
},
{
@@ -305,7 +306,7 @@ export default {
code: `
/**
* @example const i = 5;
- * quux2()
+ * quux2()
*/
function quux2 () {
@@ -327,7 +328,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 () {
@@ -346,7 +372,7 @@ export default {
code: `
/**
* @example const i = 5;
- * quux2()
+ * quux2()
*/
function quux2 () {
@@ -608,6 +634,26 @@ export default {
eslintrcForExamples: false,
exampleCodeRegex: '```js([\\s\\S]*)```'
}]
+ },
+ {
+ code: `
+ /**
+ * @example
+ * foo(function (err) {
+ * throw err;
+ * });
+ */
+ function quux () {}
+`,
+ options: [{
+ baseConfig: {
+ rules: {
+ indent: ['error']
+ }
+ },
+ eslintrcForExamples: false,
+ noDefaultExampleRules: false
+ }]
}
]
};