Skip to content

Commit 5587e02

Browse files
committed
feat(check-examples): support global regexes and other flags besides now default "u" (i.e., any of gimys); fixes #331
1 parent 0e89cc8 commit 5587e02

File tree

4 files changed

+307
-97
lines changed

4 files changed

+307
-97
lines changed

.README/rules/check-examples.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,16 @@ syntax highlighting). The following options determine whether a given
2727
first such group treated as one to include. If no parenthetical group
2828
exists or matches, the whole matching expression will be used.
2929
An example might be ````"^```(?:js|javascript)([\\s\\S]*)```\s*$"````
30-
to only match explicitly fenced JavaScript blocks.
30+
to only match explicitly fenced JavaScript blocks. Defaults to only
31+
using the `u` flag, so to add your own flags, encapsulate your
32+
expression as a string, but like a literal, e.g., ````/```js.*```/gi````.
33+
Note that specifying a global regular expression (i.e., with `g`) will
34+
allow independent linting of matched blocks within a single `@example`.
3135
* `rejectExampleCodeRegex` - Regex blacklist which rejects
3236
non-lintable examples (has priority over `exampleCodeRegex`). An example
3337
might be ```"^`"``` to avoid linting fenced blocks which may indicate
34-
a non-JavaScript language.
38+
a non-JavaScript language. See `exampleCodeRegex` on how to add flags
39+
if the default `u` is not sufficient.
3540

3641
If neither is in use, all examples will be matched. Note also that even if
3742
`captionRequired` is not set, any initial `<caption>` will be stripped out

README.md

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -658,11 +658,16 @@ syntax highlighting). The following options determine whether a given
658658
first such group treated as one to include. If no parenthetical group
659659
exists or matches, the whole matching expression will be used.
660660
An example might be ````"^```(?:js|javascript)([\\s\\S]*)```\s*$"````
661-
to only match explicitly fenced JavaScript blocks.
661+
to only match explicitly fenced JavaScript blocks. Defaults to only
662+
using the `u` flag, so to add your own flags, encapsulate your
663+
expression as a string, but like a literal, e.g., ````/```js.*```/gi````.
664+
Note that specifying a global regular expression (i.e., with `g`) will
665+
allow independent linting of matched blocks within a single `@example`.
662666
* `rejectExampleCodeRegex` - Regex blacklist which rejects
663667
non-lintable examples (has priority over `exampleCodeRegex`). An example
664668
might be ```"^`"``` to avoid linting fenced blocks which may indicate
665-
a non-JavaScript language.
669+
a non-JavaScript language. See `exampleCodeRegex` on how to add flags
670+
if the default `u` is not sufficient.
666671

667672
If neither is in use, all examples will be matched. Note also that even if
668673
`captionRequired` is not set, any initial `<caption>` will be stripped out
@@ -958,6 +963,44 @@ function quux () {
958963
}
959964
// Options: [{"baseConfig":{"parser":"@typescript-eslint/parser","parserOptions":{"ecmaVersion":6},"rules":{"semi":["error","always"]}},"eslintrcForExamples":false}]
960965
// Message: @example error (semi): Missing semicolon.
966+
967+
/**
968+
* @example <caption>Say `Hello!` to the user.</caption>
969+
* First, import the function:
970+
*
971+
* ```js
972+
* import popup from './popup'
973+
* const aConstInSameScope = 5;
974+
* ```
975+
*
976+
* Then use it like this:
977+
*
978+
* ```js
979+
* const aConstInSameScope = 7;
980+
* popup('Hello!')
981+
* ```
982+
*
983+
* Here is the result on macOS:
984+
*
985+
* ![Screenshot](path/to/screenshot.jpg)
986+
*/
987+
// Options: [{"baseConfig":{"parserOptions":{"ecmaVersion":2015,"sourceType":"module"},"rules":{"semi":["error","always"]}},"eslintrcForExamples":false,"exampleCodeRegex":"/^```(?:js|javascript)\\n([\\s\\S]*?)```$/gm"}]
988+
// Message: @example error (semi): Missing semicolon.
989+
990+
/**
991+
* @example // begin
992+
alert('hello')
993+
// end
994+
* And here is another example:
995+
// begin
996+
alert('there')
997+
// end
998+
*/
999+
function quux () {
1000+
1001+
}
1002+
// Options: [{"baseConfig":{"rules":{"semi":["warn","always"]}},"eslintrcForExamples":false,"exampleCodeRegex":"/\\/\\/ begin[\\s\\S]*?// end/g","noDefaultExampleRules":true}]
1003+
// Message: @example warning (semi): Missing semicolon.
9611004
````
9621005

9631006
The following patterns are not considered problems:
@@ -973,6 +1016,16 @@ function quux () {
9731016
}
9741017
// Options: [{"baseConfig":{"rules":{"semi":["error","always"]}},"eslintrcForExamples":false,"exampleCodeRegex":"```js([\\s\\S]*)```"}]
9751018

1019+
/**
1020+
* @example ```js
1021+
alert('hello');
1022+
```
1023+
*/
1024+
function quux () {
1025+
1026+
}
1027+
// Options: [{"baseConfig":{"rules":{"semi":["error","always"]}},"eslintrcForExamples":false,"exampleCodeRegex":"/```js([\\s\\S]*)```/"}]
1028+
9761029
/**
9771030
* @example
9781031
* // arbitrary example content

src/rules/checkExamples.js

Lines changed: 138 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,22 @@ const countChars = (str, ch) => {
1414
return (str.match(new RegExp(escapeStringRegexp(ch), 'gu')) || []).length;
1515
};
1616

17+
const getRegexFromString = (regexString) => {
18+
const match = regexString.match(/^\/(.*)\/([gimyus]*)$/u);
19+
let flags = 'u';
20+
let regex = regexString;
21+
if (match) {
22+
[, regex, flags] = match;
23+
if (!flags) {
24+
flags = 'u';
25+
}
26+
const uniqueFlags = [...new Set(flags)];
27+
flags = uniqueFlags.join('');
28+
}
29+
30+
return new RegExp(regex, flags);
31+
};
32+
1733
export default iterateJsdoc(({
1834
report,
1935
utils,
@@ -67,8 +83,12 @@ export default iterateJsdoc(({
6783
'padded-blocks': 0,
6884
};
6985

70-
exampleCodeRegex = exampleCodeRegex && new RegExp(exampleCodeRegex, 'u');
71-
rejectExampleCodeRegex = rejectExampleCodeRegex && new RegExp(rejectExampleCodeRegex, 'u');
86+
if (exampleCodeRegex) {
87+
exampleCodeRegex = getRegexFromString(exampleCodeRegex);
88+
}
89+
if (rejectExampleCodeRegex) {
90+
rejectExampleCodeRegex = getRegexFromString(rejectExampleCodeRegex);
91+
}
7292

7393
utils.forEachPreferredTag('example', (tag, targetTagName) => {
7494
// If a space is present, we should ignore it
@@ -90,40 +110,40 @@ export default iterateJsdoc(({
90110
return;
91111
}
92112

93-
let nonJSPrefacingLines = 0;
94-
let nonJSPrefacingCols = 0;
95-
113+
const sources = [];
96114
if (exampleCodeRegex) {
97-
const idx = source.search(exampleCodeRegex);
115+
let nonJSPrefacingCols = 0;
116+
let nonJSPrefacingLines = 0;
98117

99-
// Strip out anything preceding user regex match (can affect line numbering)
100-
const preMatch = source.slice(0, idx);
118+
let startingIndex = 0;
119+
let lastStringCount = 0;
101120

102-
const preMatchLines = countChars(preMatch, '\n');
121+
let exampleCode;
122+
exampleCodeRegex.lastIndex = 0;
123+
while ((exampleCode = exampleCodeRegex.exec(source)) !== null) {
124+
const {index, 0: n0, 1: n1} = exampleCode;
103125

104-
nonJSPrefacingLines = preMatchLines;
126+
// Count anything preceding user regex match (can affect line numbering)
127+
const preMatch = source.slice(startingIndex, index);
105128

106-
const colDelta = preMatchLines ?
107-
preMatch.slice(preMatch.lastIndexOf('\n') + 1).length :
108-
preMatch.length;
129+
const preMatchLines = countChars(preMatch, '\n');
109130

110-
// Get rid of text preceding user regex match (even if it leaves valid JS, it
111-
// could cause us to count newlines twice)
112-
source = source.slice(idx);
131+
const colDelta = preMatchLines ?
132+
preMatch.slice(preMatch.lastIndexOf('\n') + 1).length :
133+
preMatch.length;
113134

114-
source = source.replace(exampleCodeRegex, (n0, n1) => {
115135
let nonJSPreface;
116136
let nonJSPrefaceLineCount;
117137
if (n1) {
118-
const index = n0.indexOf(n1);
119-
nonJSPreface = n0.slice(0, index);
138+
const idx = n0.indexOf(n1);
139+
nonJSPreface = n0.slice(0, idx);
120140
nonJSPrefaceLineCount = countChars(nonJSPreface, '\n');
121141
} else {
122142
nonJSPreface = '';
123143
nonJSPrefaceLineCount = 0;
124144
}
125145

126-
nonJSPrefacingLines += nonJSPrefaceLineCount;
146+
nonJSPrefacingLines += lastStringCount + preMatchLines + nonJSPrefaceLineCount;
127147

128148
// Ignore `preMatch` delta if newlines here
129149
if (nonJSPrefaceLineCount) {
@@ -134,100 +154,125 @@ export default iterateJsdoc(({
134154
nonJSPrefacingCols += colDelta + nonJSPreface.length;
135155
}
136156

137-
return n1 || n0;
157+
const string = n1 || n0;
158+
sources.push({
159+
nonJSPrefacingCols,
160+
nonJSPrefacingLines,
161+
string,
162+
});
163+
startingIndex = exampleCodeRegex.lastIndex;
164+
lastStringCount = countChars(string, '\n');
165+
if (!exampleCodeRegex.global) {
166+
break;
167+
}
168+
}
169+
} else {
170+
sources.push({
171+
nonJSPrefacingCols: 0,
172+
nonJSPrefacingLines: 0,
173+
string: source,
138174
});
139175
}
140176

141-
// Programmatic ESLint API: https://eslint.org/docs/developer-guide/nodejs-api
142-
const cli = new CLIEngine({
143-
allowInlineConfig,
144-
baseConfig,
145-
configFile,
146-
reportUnusedDisableDirectives,
147-
rulePaths,
148-
rules,
149-
useEslintrc: eslintrcForExamples,
150-
});
151-
152-
let messages;
153-
154-
if (paddedIndent) {
155-
source = source.replace(new RegExp(`(^|\n) {${paddedIndent}}(?!$)`, 'gu'), '\n');
156-
}
157-
158-
if (filename) {
159-
const config = cli.getConfigForFile(filename);
160-
161-
// We need a new instance to ensure that the rules that may only
162-
// be available to `filename` (if it has its own `.eslintrc`),
163-
// will be defined.
164-
const cliFile = new CLIEngine({
177+
// Todo: Make fixable
178+
// Todo: Fix whitespace indent
179+
const checkRules = function ({
180+
nonJSPrefacingCols,
181+
nonJSPrefacingLines,
182+
string,
183+
}) {
184+
// Programmatic ESLint API: https://eslint.org/docs/developer-guide/nodejs-api
185+
const cli = new CLIEngine({
165186
allowInlineConfig,
166-
baseConfig: config,
187+
baseConfig,
167188
configFile,
168189
reportUnusedDisableDirectives,
169190
rulePaths,
170191
rules,
171192
useEslintrc: eslintrcForExamples,
172193
});
173194

174-
const linter = new Linter();
195+
let messages;
175196

176-
// Force external rules to become available on `cli`
177-
try {
178-
cliFile.executeOnText('');
179-
} catch (error) {
180-
// Ignore
197+
let src = string;
198+
if (paddedIndent) {
199+
src = src.replace(new RegExp(`(^|\n) {${paddedIndent}}(?!$)`, 'gu'), '\n');
181200
}
182201

183-
const linterRules = [...cliFile.getRules().entries()].reduce((obj, [key, val]) => {
184-
obj[key] = val;
202+
if (filename) {
203+
const config = cli.getConfigForFile(filename);
204+
205+
// We need a new instance to ensure that the rules that may only
206+
// be available to `filename` (if it has its own `.eslintrc`),
207+
// will be defined.
208+
const cliFile = new CLIEngine({
209+
allowInlineConfig,
210+
baseConfig: config,
211+
configFile,
212+
reportUnusedDisableDirectives,
213+
rulePaths,
214+
rules,
215+
useEslintrc: eslintrcForExamples,
216+
});
217+
218+
const linter = new Linter();
219+
220+
// Force external rules to become available on `cli`
221+
try {
222+
cliFile.executeOnText('');
223+
} catch (error) {
224+
// Ignore
225+
}
226+
227+
const linterRules = [...cliFile.getRules().entries()].reduce((obj, [key, val]) => {
228+
obj[key] = val;
185229

186-
return obj;
187-
}, {});
230+
return obj;
231+
}, {});
188232

189-
linter.defineRules(linterRules);
233+
linter.defineRules(linterRules);
234+
235+
if (config.parser) {
236+
// eslint-disable-next-line global-require, import/no-dynamic-require
237+
linter.defineParser(config.parser, require(config.parser));
238+
}
190239

191-
if (config.parser) {
192-
// eslint-disable-next-line global-require, import/no-dynamic-require
193-
linter.defineParser(config.parser, require(config.parser));
240+
// Could also support `disableFixes` and `allowInlineConfig`
241+
messages = linter.verify(src, config, {
242+
filename,
243+
reportUnusedDisableDirectives,
244+
});
245+
} else {
246+
({results: [{messages}]} =
247+
cli.executeOnText(src));
194248
}
195249

196-
// Could also support `disableFixes` and `allowInlineConfig`
197-
messages = linter.verify(source, config, {
198-
filename,
199-
reportUnusedDisableDirectives,
250+
// NOTE: `tag.line` can be 0 if of form `/** @tag ... */`
251+
const codeStartLine = tag.line + nonJSPrefacingLines;
252+
const codeStartCol = likelyNestedJSDocIndentSpace;
253+
254+
messages.forEach(({message, line, column, severity, ruleId}) => {
255+
const startLine = codeStartLine + line + zeroBasedLineIndexAdjust;
256+
const startCol = codeStartCol + (
257+
258+
// This might not work for line 0, but line 0 is unlikely for examples
259+
line <= 1 ? nonJSPrefacingCols + firstLinePrefixLength : preTagSpaceLength
260+
) + column;
261+
262+
report(
263+
'@' + targetTagName + ' ' + (severity === 2 ? 'error' : 'warning') +
264+
(ruleId ? ' (' + ruleId + ')' : '') + ': ' +
265+
message,
266+
null,
267+
{
268+
column: startCol,
269+
line: startLine,
270+
},
271+
);
200272
});
201-
} else {
202-
({results: [{messages}]} =
203-
cli.executeOnText(source));
204-
}
273+
};
205274

206-
// Make fixable, fix whitespace indent, allow global regexes
207-
// NOTE: `tag.line` can be 0 if of form `/** @tag ... */`
208-
const codeStartLine = tag.line + nonJSPrefacingLines;
209-
const codeStartCol = likelyNestedJSDocIndentSpace;
210-
211-
messages.forEach(({message, line, column, severity, ruleId}) => {
212-
const startLine = codeStartLine + line + zeroBasedLineIndexAdjust;
213-
const startCol = codeStartCol + (
214-
215-
// This might not work for line 0, but line 0 is unlikely for examples
216-
line <= 1 ? nonJSPrefacingCols + firstLinePrefixLength : preTagSpaceLength
217-
) + column;
218-
219-
// Could perhaps make fixable
220-
report(
221-
'@' + targetTagName + ' ' + (severity === 2 ? 'error' : 'warning') +
222-
(ruleId ? ' (' + ruleId + ')' : '') + ': ' +
223-
message,
224-
null,
225-
{
226-
column: startCol,
227-
line: startLine,
228-
},
229-
);
230-
});
275+
sources.forEach(checkRules);
231276
});
232277
}, {
233278
iterateAllJsdocs: true,

0 commit comments

Comments
 (0)