Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: check rule examples for syntax errors (#17718)
* check rule examples for syntax errors * fix a test on Windows * more than three backticks allowed * add comments, minimal tweaks * fix Makefile task * fix for multiple trailing spaces after 'correct' * rename npm script
- Loading branch information
Showing
9 changed files
with
436 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
"use strict"; | ||
|
||
/** @typedef {import("../../lib/shared/types").ParserOptions} ParserOptions */ | ||
|
||
/** | ||
* A callback function to handle the opening of container blocks. | ||
* @callback OpenHandler | ||
* @param {"correct" | "incorrect"} type The type of the example. | ||
* @param {string} code The example code. | ||
* @param {ParserOptions} parserOptions The parser options to be passed to the Playground. | ||
* @param {Object} codeBlockToken The `markdown-it` token for the code block inside the container. | ||
* @returns {string | undefined} If a text is returned, it will be appended to the rendered output | ||
* of `markdown-it`. | ||
*/ | ||
|
||
/** | ||
* A callback function to handle the closing of container blocks. | ||
* @callback CloseHandler | ||
* @returns {string | undefined} If a text is returned, it will be appended to the rendered output | ||
* of `markdown-it`. | ||
*/ | ||
|
||
/** | ||
* This is a utility to simplify the creation of `markdown-it-container` options to handle rule | ||
* examples in the documentation. | ||
* It is designed to automate the following common tasks: | ||
* | ||
* - Ensure that the plugin instance only matches container blocks tagged with 'correct' or | ||
* 'incorrect'. | ||
* - Parse the optional `parserOptions` after the correct/incorrect tag. | ||
* - Apply common transformations to the code inside the code block, like stripping '⏎' at the end | ||
* of a line or the last newline character. | ||
* | ||
* Additionally, the opening and closing of the container blocks are handled by two distinct | ||
* callbacks, of which only the `open` callback is required. | ||
* @param {Object} options The options object. | ||
* @param {OpenHandler} options.open The open callback. | ||
* @param {CloseHandler} [options.close] The close callback. | ||
* @returns {Object} The `markdown-it-container` options. | ||
* @example | ||
* const markdownIt = require("markdown-it"); | ||
* const markdownItContainer = require("markdown-it-container"); | ||
* | ||
* markdownIt() | ||
* .use(markdownItContainer, "rule-example", markdownItRuleExample({ | ||
* open(type, code, parserOptions, codeBlockToken) { | ||
* // do something | ||
* } | ||
* close() { | ||
* // do something | ||
* } | ||
* })) | ||
* .render(text); | ||
* | ||
*/ | ||
function markdownItRuleExample({ open, close }) { | ||
return { | ||
validate(info) { | ||
return /^\s*(?:in)?correct(?!\S)/u.test(info); | ||
}, | ||
render(tokens, index) { | ||
const tagToken = tokens[index]; | ||
|
||
if (tagToken.nesting < 0) { | ||
const text = close ? close() : void 0; | ||
|
||
// Return an empty string to avoid appending unexpected text to the output. | ||
return typeof text === "string" ? text : ""; | ||
} | ||
|
||
const { type, parserOptionsJSON } = /^\s*(?<type>\S+)(\s+(?<parserOptionsJSON>\S.*?))?\s*$/u.exec(tagToken.info).groups; | ||
const parserOptions = { sourceType: "module", ...(parserOptionsJSON && JSON.parse(parserOptionsJSON)) }; | ||
const codeBlockToken = tokens[index + 1]; | ||
|
||
// Remove trailing newline and presentational `⏎` characters (https://github.com/eslint/eslint/issues/17627): | ||
const code = codeBlockToken.content | ||
.replace(/\n$/u, "") | ||
.replace(/⏎(?=\n)/gu, ""); | ||
|
||
const text = open(type, code, parserOptions, codeBlockToken); | ||
|
||
// Return an empty string to avoid appending unexpected text to the output. | ||
return typeof text === "string" ? text : ""; | ||
} | ||
}; | ||
} | ||
|
||
module.exports = markdownItRuleExample; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
--- | ||
title: Lorem Ipsum | ||
--- | ||
|
||
This file contains rule example code with syntax errors. | ||
|
||
<!-- markdownlint-capture --> | ||
<!-- markdownlint-disable MD040 --> | ||
::: incorrect { "sourceType": "script" } | ||
|
||
``` | ||
export default "foo"; | ||
``` | ||
|
||
::: | ||
<!-- markdownlint-restore --> | ||
|
||
:::correct | ||
|
||
````ts | ||
const foo = "bar"; | ||
|
||
const foo = "baz"; | ||
```` | ||
|
||
::: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
This file contains rule example code without syntax errors. | ||
|
||
::: incorrect | ||
|
||
```js | ||
export default⏎ | ||
"foo"; | ||
``` | ||
|
||
::: | ||
|
||
::: correct { "ecmaFeatures": { "jsx": true } } | ||
|
||
```jsx | ||
const foo = <bar></bar>; | ||
``` | ||
|
||
::: | ||
|
||
A test with multiple spaces after 'correct': | ||
<!-- markdownlint-disable-next-line no-trailing-spaces --> | ||
:::correct | ||
|
||
```js | ||
``` | ||
|
||
::: | ||
|
||
The following code block is not a rule example, so it won't be checked: | ||
|
||
```js | ||
!@#$%^&*() | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
"use strict"; | ||
|
||
//------------------------------------------------------------------------------ | ||
// Requirements | ||
//------------------------------------------------------------------------------ | ||
|
||
const assert = require("assert"); | ||
const { execFile } = require("child_process"); | ||
const { promisify } = require("util"); | ||
|
||
//------------------------------------------------------------------------------ | ||
// Helpers | ||
//------------------------------------------------------------------------------ | ||
|
||
/** | ||
* Runs check-rule-examples on the specified files. | ||
* @param {...string} filenames Files to be passed to check-rule-examples. | ||
* @returns {Promise<ChildProcess>} An object with properties `stdout` and `stderr` on success. | ||
* @throws An object with properties `code`, `stdout` and `stderr` on success. | ||
*/ | ||
async function runCheckRuleExamples(...filenames) { | ||
return await promisify(execFile)( | ||
process.execPath, | ||
["--no-deprecation", "tools/check-rule-examples.js", ...filenames], | ||
{ env: { FORCE_COLOR: "3" } } // 24-bit color mode | ||
); | ||
} | ||
|
||
//------------------------------------------------------------------------------ | ||
// Tests | ||
//------------------------------------------------------------------------------ | ||
|
||
describe("check-rule-examples", () => { | ||
|
||
it("succeeds when not passed any files", async () => { | ||
const childProcess = await runCheckRuleExamples(); | ||
|
||
assert.strictEqual(childProcess.stdout, ""); | ||
assert.strictEqual(childProcess.stderr, ""); | ||
}); | ||
|
||
it("succeeds when passed a syntax error free file", async () => { | ||
const childProcess = await runCheckRuleExamples("tests/fixtures/good-examples.md"); | ||
|
||
assert.strictEqual(childProcess.stdout, ""); | ||
assert.strictEqual(childProcess.stderr, ""); | ||
}); | ||
|
||
it("fails when passed a file with a syntax error", async () => { | ||
const promise = runCheckRuleExamples("tests/fixtures/good-examples.md", "tests/fixtures/bad-examples.md"); | ||
|
||
await assert.rejects( | ||
promise, | ||
{ | ||
code: 1, | ||
stdout: "", | ||
stderr: | ||
"\x1B[0m\x1B[0m\n" + | ||
"\x1B[0m\x1B[4mtests/fixtures/bad-examples.md\x1B[24m\x1B[0m\n" + | ||
"\x1B[0m \x1B[2m11:4\x1B[22m \x1B[31merror\x1B[39m Missing language tag: use one of 'javascript', 'js' or 'jsx'\x1B[0m\n" + | ||
"\x1B[0m \x1B[2m12:1\x1B[22m \x1B[31merror\x1B[39m Syntax error: 'import' and 'export' may appear only with 'sourceType: module'\x1B[0m\n" + | ||
"\x1B[0m \x1B[2m20:5\x1B[22m \x1B[31merror\x1B[39m Nonstandard language tag 'ts': use one of 'javascript', 'js' or 'jsx'\x1B[0m\n" + | ||
"\x1B[0m \x1B[2m23:7\x1B[22m \x1B[31merror\x1B[39m Syntax error: Identifier 'foo' has already been declared\x1B[0m\n" + | ||
"\x1B[0m\x1B[0m\n" + | ||
"\x1B[0m\x1B[31m\x1B[1m✖ 4 problems (4 errors, 0 warnings)\x1B[22m\x1B[39m\x1B[0m\n" + | ||
"\x1B[0m\x1B[31m\x1B[1m\x1B[22m\x1B[39m\x1B[0m\n" | ||
} | ||
); | ||
}); | ||
|
||
it("fails when a file cannot be processed", async () => { | ||
const promise = runCheckRuleExamples("tests/fixtures/non-existing-examples.md"); | ||
|
||
await assert.rejects( | ||
promise, | ||
({ code, stdout, stderr }) => { | ||
assert.strictEqual(code, 1); | ||
assert.strictEqual(stdout, ""); | ||
const expectedStderr = | ||
"\x1B[0m\x1B[0m\n" + | ||
"\x1B[0m\x1B[4mtests/fixtures/non-existing-examples.md\x1B[24m\x1B[0m\n" + | ||
"\x1B[0m \x1B[2m0:0\x1B[22m \x1B[31merror\x1B[39m Error checking file: ENOENT: no such file or directory, open <FILE>\x1B[0m\n" + | ||
"\x1B[0m\x1B[0m\n" + | ||
"\x1B[0m\x1B[31m\x1B[1m✖ 1 problem (1 error, 0 warnings)\x1B[22m\x1B[39m\x1B[0m\n" + | ||
"\x1B[0m\x1B[31m\x1B[1m\x1B[22m\x1B[39m\x1B[0m\n"; | ||
|
||
// Replace filename as it's OS-dependent. | ||
const normalizedStderr = stderr.replace(/'.+'/u, "<FILE>"); | ||
|
||
assert.strictEqual(normalizedStderr, expectedStderr); | ||
return true; | ||
} | ||
); | ||
}); | ||
}); |
Oops, something went wrong.