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 + }] } ] };