Skip to content

Commit

Permalink
fix(check-examples): preserve whitespace so as to report issues with …
Browse files Browse the repository at this point in the history
…whitespace-related rules such as `indent` (fixes gajus#211)

feat(check-examples): add `paddedIndent` option
  • Loading branch information
brettz9 committed Jul 13, 2019
1 parent 4b97157 commit 62ed39e
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 24 deletions.
18 changes: 18 additions & 0 deletions .README/rules/check-examples.md
Expand Up @@ -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 `<caption>` 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
Expand Down
1 change: 0 additions & 1 deletion package.json
Expand Up @@ -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",
Expand Down
52 changes: 46 additions & 6 deletions src/iterateJsdoc.js
Expand Up @@ -3,22 +3,59 @@ 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
parsers: [
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] || {};
};

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
29 changes: 16 additions & 13 deletions 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';

Expand Down Expand Up @@ -30,6 +29,7 @@ export default iterateJsdoc(({
noDefaultExampleRules = false,
eslintrcForExamples = true,
matchingFileName: filename = null,
paddedIndent = 0,
baseConfig = {},
configFile,
allowInlineConfig = true,
Expand Down Expand Up @@ -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())) {
Expand All @@ -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
Expand All @@ -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;
}
Expand All @@ -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);

Expand Down Expand Up @@ -265,6 +263,10 @@ export default iterateJsdoc(({
default: false,
type: 'boolean'
},
paddedIndent: {
default: 0,
type: 'integer'
},
rejectExampleCodeRegex: {
type: 'string'
},
Expand All @@ -277,5 +279,6 @@ export default iterateJsdoc(({
}
],
type: 'suggestion'
}
},
noTrim: true
});
54 changes: 50 additions & 4 deletions test/rules/assertions/checkExamples.js
Expand Up @@ -86,6 +86,7 @@ export default {
code: `
/**
* @example
*
* \`\`\`js alert('hello'); \`\`\`
*/
function quux () {
Expand Down Expand Up @@ -184,7 +185,7 @@ export default {
}
},
eslintrcForExamples: false,
rejectExampleCodeRegex: '^\\s*<.*>$'
rejectExampleCodeRegex: '^\\s*<.*>\\s*$'
}]
},
{
Expand Down Expand Up @@ -305,7 +306,7 @@ export default {
code: `
/**
* @example const i = 5;
* quux2()
* quux2()
*/
function quux2 () {
Expand All @@ -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 () {
Expand All @@ -346,7 +372,7 @@ export default {
code: `
/**
* @example const i = 5;
* quux2()
* quux2()
*/
function quux2 () {
Expand Down Expand Up @@ -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
}]
}
]
};

0 comments on commit 62ed39e

Please sign in to comment.