Skip to content

Commit 62ed39e

Browse files
committed
fix(check-examples): preserve whitespace so as to report issues with whitespace-related rules such as indent (fixes gajus#211)
feat(check-examples): add `paddedIndent` option
1 parent 4b97157 commit 62ed39e

File tree

5 files changed

+130
-24
lines changed

5 files changed

+130
-24
lines changed

.README/rules/check-examples.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,24 @@ If neither is in use, all examples will be matched. Note also that even if
3737
`captionRequired` is not set, any initial `<caption>` will be stripped out
3838
before doing the regex matching.
3939

40+
#### `paddedIndent`
41+
42+
This integer property allows one to add a fixed amount of whitespace at the
43+
beginning of the second or later lines of the example to be stripped so as
44+
to avoid linting issues with the decorative whitespace. For example, if set
45+
to a value of `4`, the initial whitespace below will not trigger `indent`
46+
rule errors as the extra 4 spaces on each subsequent line will be stripped
47+
out before evaluation.
48+
49+
```js
50+
/**
51+
* @example
52+
* anArray.filter((a) => {
53+
* return a.b;
54+
* });
55+
*/
56+
```
57+
4058
#### `reportUnusedDisableDirectives`
4159

4260
If not set to `false`, `reportUnusedDisableDirectives` will report disabled

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
"dependencies": {
88
"comment-parser": "^0.5.5",
99
"debug": "^4.1.1",
10-
"escape-regex-string": "^1.0.6",
1110
"flat-map-polyfill": "^0.3.8",
1211
"jsdoctypeparser": "5.0.1",
1312
"lodash": "^4.17.14",

src/iterateJsdoc.js

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,59 @@ import commentParser from 'comment-parser';
33
import jsdocUtils from './jsdocUtils';
44
import getJSDocComment from './eslint/getJSDocComment';
55

6-
const parseComment = (commentNode, indent) => {
6+
/**
7+
*
8+
* @param {object} commentNode
9+
* @param {string} indent Whitespace
10+
* @returns {object}
11+
*/
12+
const parseComment = (commentNode, indent, trim = true) => {
713
// Preserve JSDoc block start/end indentation.
814
return commentParser(`${indent}/*${commentNode.value}${indent}*/`, {
915
// @see https://github.com/yavorskiy/comment-parser/issues/21
1016
parsers: [
1117
commentParser.PARSERS.parse_tag,
1218
commentParser.PARSERS.parse_type,
1319
(str, data) => {
14-
if (['return', 'returns', 'throws', 'exception'].includes(data.tag)) {
20+
if (['example', 'return', 'returns', 'throws', 'exception'].includes(data.tag)) {
1521
return null;
1622
}
1723

1824
return commentParser.PARSERS.parse_name(str, data);
1925
},
20-
commentParser.PARSERS.parse_description
21-
]
26+
trim ?
27+
commentParser.PARSERS.parse_description :
28+
29+
// parse_description
30+
(str, data) => {
31+
// Only expected throw in previous step is if bad name (i.e.,
32+
// missing end bracket on optional name), but `@example`
33+
// skips name parsing
34+
/* istanbul ignore next */
35+
if (data.errors && data.errors.length) {
36+
return null;
37+
}
38+
39+
// Tweak original regex to capture only single optional space
40+
const result = str.match(/^\s?((.|\s)+)?/);
41+
42+
// Always has at least whitespace due to `indent` we've added
43+
/* istanbul ignore next */
44+
if (result) {
45+
return {
46+
data: {
47+
description: result[1] === undefined ? '' : result[1]
48+
},
49+
source: result[0]
50+
};
51+
}
52+
53+
// Always has at least whitespace due to `indent` we've added
54+
/* istanbul ignore next */
55+
return null;
56+
}
57+
],
58+
trim
2259
})[0] || {};
2360
};
2461

@@ -322,7 +359,7 @@ const iterateAllJsdocs = (iterator, ruleConfig) => {
322359
}
323360

324361
const indent = ' '.repeat(comment.loc.start.column);
325-
const jsdoc = parseComment(comment, indent);
362+
const jsdoc = parseComment(comment, indent, !ruleConfig.noTrim);
326363
const settings = getSettings(context);
327364
const report = makeReport(context, comment);
328365
const jsdocNode = comment;
@@ -368,7 +405,10 @@ export default function iterateJsdoc (iterator, ruleConfig) {
368405
}
369406

370407
if (ruleConfig.iterateAllJsdocs) {
371-
return iterateAllJsdocs(iterator, {meta: ruleConfig.meta});
408+
return iterateAllJsdocs(iterator, {
409+
meta: ruleConfig.meta,
410+
noTrim: ruleConfig.noTrim
411+
});
372412
}
373413

374414
return {

src/rules/checkExamples.js

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {CLIEngine, Linter} from 'eslint';
2-
import escapeRegexString from 'escape-regex-string';
32
import iterateJsdoc from '../iterateJsdoc';
43
import warnRemovedSettings from '../warnRemovedSettings';
54

@@ -30,6 +29,7 @@ export default iterateJsdoc(({
3029
noDefaultExampleRules = false,
3130
eslintrcForExamples = true,
3231
matchingFileName: filename = null,
32+
paddedIndent = 0,
3333
baseConfig = {},
3434
configFile,
3535
allowInlineConfig = true,
@@ -72,13 +72,9 @@ export default iterateJsdoc(({
7272

7373
utils.forEachPreferredTag('example', (tag, targetTagName) => {
7474
// If a space is present, we should ignore it
75-
const initialTag = tag.source.match(
76-
new RegExp(`^@${escapeRegexString(targetTagName)} ?`, 'u')
77-
);
78-
const initialTagLength = initialTag[0].length;
79-
const firstLinePrefixLength = preTagSpaceLength + initialTagLength;
75+
const firstLinePrefixLength = preTagSpaceLength;
8076

81-
let source = tag.source.slice(initialTagLength);
77+
let source = tag.description;
8278
const match = source.match(hasCaptionRegex);
8379

8480
if (captionRequired && (!match || !match[1].trim())) {
@@ -101,16 +97,14 @@ export default iterateJsdoc(({
10197
const idx = source.search(exampleCodeRegex);
10298

10399
// Strip out anything preceding user regex match (can affect line numbering)
104-
let preMatchLines = 0;
105-
106100
const preMatch = source.slice(0, idx);
107101

108-
preMatchLines = countChars(preMatch, '\n');
102+
const preMatchLines = countChars(preMatch, '\n');
109103

110104
nonJSPrefacingLines = preMatchLines;
111105

112106
const colDelta = preMatchLines ?
113-
preMatch.slice(preMatch.lastIndexOf('\n') + 1).length - initialTagLength :
107+
preMatch.slice(preMatch.lastIndexOf('\n') + 1).length :
114108
preMatch.length;
115109

116110
// Get rid of text preceding user regex match (even if it leaves valid JS, it
@@ -135,7 +129,7 @@ export default iterateJsdoc(({
135129
if (nonJSPrefaceLineCount) {
136130
const charsInLastLine = nonJSPreface.slice(nonJSPreface.lastIndexOf('\n') + 1).length;
137131

138-
nonJSPrefacingCols += charsInLastLine - initialTagLength;
132+
nonJSPrefacingCols += charsInLastLine;
139133
} else {
140134
nonJSPrefacingCols += colDelta + nonJSPreface.length;
141135
}
@@ -157,6 +151,10 @@ export default iterateJsdoc(({
157151

158152
let messages;
159153

154+
if (paddedIndent) {
155+
source = source.replace(new RegExp(`(^|\n) {${paddedIndent}}(?!$)`, 'g'), '\n');
156+
}
157+
160158
if (filename) {
161159
const config = cli.getConfigForFile(filename);
162160

@@ -265,6 +263,10 @@ export default iterateJsdoc(({
265263
default: false,
266264
type: 'boolean'
267265
},
266+
paddedIndent: {
267+
default: 0,
268+
type: 'integer'
269+
},
268270
rejectExampleCodeRegex: {
269271
type: 'string'
270272
},
@@ -277,5 +279,6 @@ export default iterateJsdoc(({
277279
}
278280
],
279281
type: 'suggestion'
280-
}
282+
},
283+
noTrim: true
281284
});

test/rules/assertions/checkExamples.js

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export default {
8686
code: `
8787
/**
8888
* @example
89+
*
8990
* \`\`\`js alert('hello'); \`\`\`
9091
*/
9192
function quux () {
@@ -184,7 +185,7 @@ export default {
184185
}
185186
},
186187
eslintrcForExamples: false,
187-
rejectExampleCodeRegex: '^\\s*<.*>$'
188+
rejectExampleCodeRegex: '^\\s*<.*>\\s*$'
188189
}]
189190
},
190191
{
@@ -305,7 +306,7 @@ export default {
305306
code: `
306307
/**
307308
* @example const i = 5;
308-
* quux2()
309+
* quux2()
309310
*/
310311
function quux2 () {
311312
@@ -327,7 +328,32 @@ export default {
327328
code: `
328329
/**
329330
* @example const i = 5;
330-
* quux2()
331+
* quux2()
332+
*/
333+
function quux2 () {
334+
335+
}
336+
`,
337+
errors: [
338+
{
339+
message: '@example warning (id-length): Identifier name \'i\' is too short (< 2).'
340+
},
341+
{
342+
message: '@example error (semi): Missing semicolon.'
343+
}
344+
],
345+
options: [
346+
{
347+
paddedIndent: 2
348+
}
349+
]
350+
},
351+
{
352+
code: `
353+
/**
354+
* @example
355+
* const i = 5;
356+
* quux2()
331357
*/
332358
function quux2 () {
333359
@@ -346,7 +372,7 @@ export default {
346372
code: `
347373
/**
348374
* @example const i = 5;
349-
* quux2()
375+
* quux2()
350376
*/
351377
function quux2 () {
352378
@@ -608,6 +634,26 @@ export default {
608634
eslintrcForExamples: false,
609635
exampleCodeRegex: '```js([\\s\\S]*)```'
610636
}]
637+
},
638+
{
639+
code: `
640+
/**
641+
* @example
642+
* foo(function (err) {
643+
* throw err;
644+
* });
645+
*/
646+
function quux () {}
647+
`,
648+
options: [{
649+
baseConfig: {
650+
rules: {
651+
indent: ['error']
652+
}
653+
},
654+
eslintrcForExamples: false,
655+
noDefaultExampleRules: false
656+
}]
611657
}
612658
]
613659
};

0 commit comments

Comments
 (0)