8000 chore: check rule examples for syntax errors by fasttime · Pull Request #17718 · eslint/eslint · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
check rule examples for syntax errors
  • Loading branch information
fasttime committed Nov 20, 2023
commit 538e7e8126abb53e7b8744baff38d10f914c90d3
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ jobs:
run: npm run lint:scss
- name: Lint Docs JS Files
run: node Makefile lintDocsJS
- name: Check Rule Examples
run: node Makefile checkRuleExamples
- name: Build Docs Website
working-directory: docs
run: npm run build
Expand Down
4 changes: 4 additions & 0 deletions Makefile.js
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,10 @@ target.checkRuleFiles = function() {

};

target.checkRuleExamples = function() {
exec(`${NODE}tools/check-rule-examples.js docs/src/rules/*.md`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to work on Windows, probably because exec() runs commands through cmd.exe which doesn't expand *.md.

$ node Makefile.js checkRuleExamples

docs/src/rules/*.md
  0:0  error  Error checking file: ENOENT: no such file or directory, open 'C:\projects\eslint\docs\src\rules\*.md'

✖ 1 problem (1 error, 0 warnings)

C:\projects\eslint\node_modules\shelljs\src\common.js:401
      if (config.fatal) throw e;
                        ^

Error: exec:
docs/src/rules/*.md
  0:0  error  Error checking file: ENOENT: no such file or directory, open 'C:/projects/eslint/docs/src/rules/*.md'

✖ 1 problem (1 error, 0 warnings)


    at Object.error (C:\projects\eslint\node_modules\shelljs\src\common.js:110:27)
    at execSync (C:\projects\eslint\node_modules\shelljs\src\exec.js:120:12)
    at _exec (C:\projects\eslint\node_modules\shelljs\src\exec.js:223:12)
    at C:\projects\eslint\node_modules\shelljs\src\common.js:335:23
    at target.checkRuleExamples (C:\projects\eslint\Makefile.js:869:5)
    at global.target.<computed> [as checkRuleExamples] (C:\projects\eslint\node_modules\shelljs\make.js:36:40)
    at C:\projects\eslint\node_modules\shelljs\make.js:48:27
    at Array.forEach (<anonymous>)
    at Timeout._onTimeout (C:\projects\eslint\node_modules\shelljs\make.js:46:10)
    at listOnTimeout (node:internal/timers:573:17)

Node.js v20.7.0

Perhaps we could pass no arguments and in that case find all rule docs files in the tool? Something like fetch-docs-links is doing:

// determine which files to check
let filenames = process.argv.slice(2);
if (filenames.length === 0) {
filenames = glob.sync("docs/src/rules/*.md", { cwd: BASE_DIR });
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested this on GithHub Actions with windows-latest and it seemed to be working, so it's possible that I did a mistake, or maybe the containers have a special configuration that would let the task run. Now I'm testing on a real Windows machine, and I can reproduce the error above.

But I noticed that there is also another glitch: when shelljs' exec is used to run the command, color and underlining are not rendered in the terminal.

This is how it looks like when I run the script directly with node:

image

And this is what I'm getting when I run it with exec in the Makefile:

image

Probably because stout is not a TTY stream in the second example. It would be nice to fix that as well. I'll see what I can do.

Copy link
Member Author
@fasttime fasttime Nov 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed the problem by using glob to expand the patterns passed on the command line into file names, and I stopped using exec because its behavior depends on the system shell and is not the consistent on Windows and Unix. I'm using now Node's built-in execFileSync which correctly renders colors and underlining in th 10BC0 e terminal and does not automatically expand glob patterns.

I've also updated the unit tests to ensure that the sequences for colors and underlining are inserted in the output (only for check-rule-example.js, because there are no tests for the Makefile). We could also revert this change if it makes the output in the tests difficult to read.

Commit: 0f64bf9

};

target.checkLicenses = function() {

/**
Expand Down
70 changes: 27 additions & 43 deletions docs/.eleventy.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const { highlighter, lineNumberPlugin } = require("./src/_plugins/md-syntax-high
const {
DateTime
} = require("luxon");
const markdownIt = require("markdown-it");
const markdownItRuleExample = require("./tools/markdown-it-rule-example");

module.exports = function(eleventyConfig) {

Expand Down Expand Up @@ -113,7 +115,7 @@ module.exports = function(eleventyConfig) {
* Source: https://github.com/11ty/eleventy/issues/658
*/
eleventyConfig.addFilter("markdown", value => {
const markdown = require("markdown-it")({
const markdown = markdownIt({
html: true
});

Expand Down Expand Up @@ -191,57 +193,39 @@ module.exports = function(eleventyConfig) {
return btoa(unescape(encodeURIComponent(text)));
}

/**
* Creates markdownItContainer settings for a playground-linked codeblock.
* @param {string} name Plugin name and class name to add to the code block.
* @returns {[string, object]} Plugin name and options for markdown-it.
*/
function withPlaygroundRender(name) {
return [
name,
{
render(tokens, index) {
if (tokens[index].nesting !== 1) {
return "</div>";
}

// See https://github.com/eslint/eslint.org/blob/ac38ab41f99b89a8798d374f74e2cce01171be8b/src/playground/App.js#L44
const parserOptionsJSON = tokens[index].info?.split("correct ")[1]?.trim();
const parserOptions = { sourceType: "module", ...(parserOptionsJSON && JSON.parse(parserOptionsJSON)) };

// Remove trailing newline and presentational `⏎` characters (https://github.com/eslint/eslint/issues/17627):
const content = tokens[index + 1].content
.replace(/\n$/u, "")
.replace(/⏎(?=\n)/gu, "");
const state = encodeToBase64(
JSON.stringify({
options: { parserOptions },
text: content
})
);
const prefix = process.env.CONTEXT && process.env.CONTEXT !== "deploy-preview"
? ""
: "https://eslint.org";

return `
<div class="${name}">
// markdown-it plugin options for playground-linked code blocks in rule examples.
const ruleExampleOptions = markdownItRuleExample({
open(type, code, parserOptions) {

// See https://github.com/eslint/eslint.org/blob/ac38ab41f99b89a8798d374f74e2cce01171be8b/src/playground/App.js#L44
const state = encodeToBase64(< 6187 /td>
JSON.stringify({
options: { parserOptions },
text: code
})
);
const prefix = process.env.CONTEXT && process.env.CONTEXT !== "deploy-preview"
? ""
: "https://eslint.org";

return `
<div class="${type}">
<a class="c-btn c-btn--secondary c-btn--playground" href="${prefix}/play#${state}" target="_blank">
Open in Playground
</a>
`.trim();
}
}
];
}
`.trim();
},
close() {
return "</div>";
}
});

const markdownIt = require("markdown-it");
const md = markdownIt({ html: true, linkify: true, typographer: true, highlight: (str, lang) => highlighter(md, str, lang) })
.use(markdownItAnchor, {
slugify: s => slug(s)
})
.use(markdownItContainer, "img-container", {})
.use(markdownItContainer, ...withPlaygroundRender("correct"))
.use(markdownItContainer, ...withPlaygroundRender("incorrect"))
.use(markdownItContainer, "rule-example", ruleExampleOptions)
.use(markdownItContainer, "warning", {
render(tokens, idx) {
return generateAlertMarkup("warning", tokens, idx);
Expand Down
28 changes: 28 additions & 0 deletions docs/tools/markdown-it-rule-example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use strict";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add some comments in this file explaining how this works?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added some comments in b5b247a.


module.exports =
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) {
return close ? close() : void 0;
}

const { type, parserOptionsJSON } = /^\s*(?<type>\S+)(\s+(?<parserOptionsJSON>.+?))?\s*$/u.exec(tagToken.info).groups;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const { type, parserOptionsJSON } = /^\s*(?<type>\S+)(\s+(?<parserOptionsJSON>.+?))?\s*$/u.exec(tagToken.info).groups;
const { type, parserOptionsJSON } = /^\s*(?<type>\S+)(\s+(?<parserOptionsJSON>\S.*?))?\s*$/u.exec(tagToken.info).groups;

To avoid crashing on multiple trailing spaces.

const { type, parserOptionsJSON } = /^\s*(?<type>\S+)(\s+(?<parserOptionsJSON>.+?))?\s*$/u.exec("correct  ").groups;

console.log({ type, parserOptionsJSON }); // { type: 'correct', parserOptionsJSON: ' ' }

parserOptionsJSON && JSON.parse(parserOptionsJSON); // SyntaxError: Unexpected end of JSON input

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Fixed in 512334f.

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, "");

return open(type, code, parserOptions, codeBlockToken);
}
};
};
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"./use-at-your-own-risk": "./lib/unsupported-api.js"
},
"scripts": {
"build:docs:check-rule-examples": "node Makefile.js checkRuleExamples",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per https://eslint.org/docs/latest/contribute/package-json-conventions, build scripts generate files, while this only checks existing files. Maybe lint:?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the link. I've renamed the script to lint:docs:rule-examples because lint:docs:check-rule-examples sounds pleonastic: 5933582.

"build:docs:update-links": "node tools/fetch-docs-links.js",
"build:site": "node Makefile.js gensite",
"build:webpack": "node Makefile.js webpack",
Expand Down Expand Up @@ -42,6 +43,7 @@
"git add packages/js/src/configs/eslint-all.js"
],
"docs/src/rules/*.md": [
"node tools/check-rule-examples.js",
"node tools/fetch-docs-links.js",
"git add docs/src/_data/further_reading_links.json"
],
Expand Down Expand Up @@ -132,6 +134,8 @@
"gray-matter": "^4.0.3",
"lint-staged": "^11.0.0",
"load-perf": "^0.2.0",
"markdown-it": "^12.2.0",
"markdown-it-container": "^3.0.0",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two are the same versions as in docs/package.json.

"markdownlint": "^0.31.1",
"markdownlint-cli": "^0.37.0",
"marked": "^4.0.8",
Expand Down
26 changes: 26 additions & 0 deletions tests/fixtures/bad-examples.md
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";
```

:::
24 changes: 24 additions & 0 deletions tests/fixtures/good-examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
This file contains rule example code without syntax errors.

::: incorrect

```js
export default⏎
"foo";
```

:::

::: correct { "ecmaFeatures": { "jsx": true } }

```jsx
const foo = <bar></bar>;
```

:::

The following code block is not a rule example, so it won't be checked:

```js
!@#$%^&*()
```
88 changes: 88 additions & 0 deletions tests/tools/check-rule-examples.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"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]
);
}

//------------------------------------------------------------------------------
// 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:
"\n" +
"tests/fixtures/bad-examples.md\n" +
" 11:4 error Missing language tag: use one of 'javascript', 'js' or 'jsx'\n" +
" 12:1 error Syntax error: 'import' and 'export' may appear only with 'sourceType: module'\n" +
" 20:4 error Nonstandard language tag 'ts': use one of 'javascript', 'js' or 'jsx'\n" +
" 23:7 error Syntax error: Identifier 'foo' has already been declared\n" +
"\n" +
"✖ 4 problems (4 errors, 0 warnings)\n" +
"\n"
}
);
});

it("fails when a file cannot be processed", async () => {
const promise = runCheckRuleExamples("tests/fixtures/non-existing-examples.md");

await assert.rejects(
promise,
{
code: 1,
stdout: "",
stderr:
"\n" +
"tests/fixtures/non-existing-examples.md\n" +
" 0:0 error Error checking file: ENOENT: no such file or directory, open 'tests/fixtures/non-existing-examples.md'\n" +
"\n" +
"✖ 1 problem (1 error, 0 warnings)\n" +
"\n"
}
);
});
});
Loading
0