Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
24 changes: 21 additions & 3 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,36 @@
"root": true,
"extends": "next/core-web-vitals",
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "eslint-plugin-react-compiler"],
"plugins": ["@typescript-eslint", "eslint-plugin-react-compiler", "local-rules"],
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", {"varsIgnorePattern": "^_"}],
"react-hooks/exhaustive-deps": "error",
"react/no-unknown-property": ["error", {"ignore": ["meta"]}],
"react-compiler/react-compiler": "error"
"react-compiler/react-compiler": "error",
"local-rules/lint-markdown-code-blocks": "error"
},
"env": {
"node": true,
"commonjs": true,
"browser": true,
"es6": true
}
},
"overrides": [
{
"files": ["src/content/**/*.md"],
"parser": "./eslint-local-rules/parser",
"parserOptions": {
"sourceType": "module"
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "off",
"react-hooks/exhaustive-deps": "off",
"react/no-unknown-property": "off",
"react-compiler/react-compiler": "off",
"local-rules/lint-markdown-code-blocks": "error"
}
}
]
}
3 changes: 3 additions & 0 deletions .github/workflows/site_lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ jobs:

- name: Install deps
run: yarn install --frozen-lockfile
- name: Install deps (eslint-local-rules)
run: yarn install --frozen-lockfile
working-directory: eslint-local-rules

- name: Lint codebase
run: yarn ci-check
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
node_modules
/.pnp
.pnp.js

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
```jsx
import {useState} from 'react';
function Counter() {
const [count, setCount] = useState(0);
setCount(count + 1);
return <div>{count}</div>;
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
```jsx title="Counter" {expectedErrors: {'react-compiler': [99]}} {expectedErrors: {'react-compiler': [2]}}
import {useState} from 'react';
function Counter() {
const [count, setCount] = useState(0);
setCount(count + 1);
return <div>{count}</div>;
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
```jsx {expectedErrors: {'react-compiler': 'invalid'}}
import {useState} from 'react';
function Counter() {
const [count, setCount] = useState(0);
setCount(count + 1);
return <div>{count}</div>;
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```bash
setCount()
```

```txt
import {useState} from 'react';
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
```jsx {expectedErrors: {'react-compiler': [3]}}
function Hello() {
return <h1>Hello</h1>;
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
```jsx {expectedErrors: {'react-compiler': [4]}}
import {useState} from 'react';
function Counter() {
const [count, setCount] = useState(0);
setCount(count + 1);
return <div>{count}</div>;
}
```
131 changes: 131 additions & 0 deletions eslint-local-rules/__tests__/lint-markdown-code-blocks.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

const assert = require('assert');
const fs = require('fs');
const path = require('path');
const {ESLint} = require('eslint');
const plugin = require('..');

const FIXTURES_DIR = path.join(
__dirname,
'fixtures',
'src',
'content'
);
const PARSER_PATH = path.join(__dirname, '..', 'parser.js');

function createESLint({fix = false} = {}) {
return new ESLint({
useEslintrc: false,
fix,
plugins: {
'local-rules': plugin,
},
overrideConfig: {
parser: PARSER_PATH,
plugins: ['local-rules'],
rules: {
'local-rules/lint-markdown-code-blocks': 'error',
},
parserOptions: {
sourceType: 'module',
},
},
});
}

function readFixture(name) {
return fs.readFileSync(path.join(FIXTURES_DIR, name), 'utf8');
}

async function lintFixture(name, {fix = false} = {}) {
const eslint = createESLint({fix});
const filePath = path.join(FIXTURES_DIR, name);
const markdown = readFixture(name);
const [result] = await eslint.lintText(markdown, {filePath});
return result;
}

async function run() {
const basicResult = await lintFixture('basic-error.md');
assert.strictEqual(
basicResult.messages.length,
1,
'expected one diagnostic'
);
assert(
basicResult.messages[0].message.includes('Calling setState during render'),
'expected message to mention setState during render'
);

const suppressedResult = await lintFixture('suppressed-error.md');
assert.strictEqual(
suppressedResult.messages.length,
0,
'expected suppression metadata to silence diagnostic'
);

const staleResult = await lintFixture('stale-expected-error.md');
assert.strictEqual(
staleResult.messages.length,
1,
'expected stale metadata error'
);
assert.strictEqual(
staleResult.messages[0].message,
'React Compiler expected error on line 3 was not triggered'
);

const duplicateResult = await lintFixture('duplicate-metadata.md');
assert.strictEqual(
duplicateResult.messages.length,
2,
'expected duplicate metadata to surface compiler diagnostic and stale metadata notice'
);
const duplicateFixed = await lintFixture('duplicate-metadata.md', {
fix: true,
});
assert(
duplicateFixed.output.includes(
"{expectedErrors: {'react-compiler': [4]}}"
),
'expected duplicates to be rewritten to a single canonical block'
);
assert(
!duplicateFixed.output.includes('[99]'),
'expected stale line numbers to be removed from metadata'
);

const mixedLanguageResult = await lintFixture('mixed-language.md');
assert.strictEqual(
mixedLanguageResult.messages.length,
0,
'expected non-js code fences to be ignored'
);

const malformedResult = await lintFixture('malformed-metadata.md');
assert.strictEqual(
malformedResult.messages.length,
1,
'expected malformed metadata to fall back to compiler diagnostics'
);
const malformedFixed = await lintFixture('malformed-metadata.md', {
fix: true,
});
assert(
malformedFixed.output.includes(
"{expectedErrors: {'react-compiler': [4]}}"
),
'expected malformed metadata to be replaced with canonical form'
);
}

run().catch(error => {
console.error(error);
process.exitCode = 1;
});
14 changes: 14 additions & 0 deletions eslint-local-rules/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

const lintMarkdownCodeBlocks = require('./rules/lint-markdown-code-blocks');

module.exports = {
rules: {
'lint-markdown-code-blocks': lintMarkdownCodeBlocks,
},
};
12 changes: 12 additions & 0 deletions eslint-local-rules/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "eslint-plugin-local-rules",
"version": "0.0.0",
"main": "index.js",
"private": "true",
"scripts": {
"test": "node __tests__/lint-markdown-code-blocks.test.js"
},
"devDependencies": {
"eslint-mdx": "^2"
}
}
8 changes: 8 additions & 0 deletions eslint-local-rules/parser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

module.exports = require('eslint-mdx');
77 changes: 77 additions & 0 deletions eslint-local-rules/rules/diagnostics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

function getRelativeLine(loc) {
return loc?.start?.line ?? loc?.line ?? 1;
}

function getRelativeColumn(loc) {
return loc?.start?.column ?? loc?.column ?? 0;
}

function getRelativeEndLine(loc, fallbackLine) {
if (loc?.end?.line != null) {
return loc.end.line;
}
if (loc?.line != null) {
return loc.line;
}
return fallbackLine;
}

function getRelativeEndColumn(loc, fallbackColumn) {
if (loc?.end?.column != null) {
return loc.end.column;
}
if (loc?.column != null) {
return loc.column;
}
return fallbackColumn;
}

/**
* @param {import('./markdown').MarkdownCodeBlock} block
* @param {Array<{detail: any, loc: any, message: string}>} diagnostics
* @returns {Array<{detail: any, message: string, relativeStartLine: number, markdownLoc: {start: {line: number, column: number}, end: {line: number, column: number}}}>}
*/
function normalizeDiagnostics(block, diagnostics) {
return diagnostics.map(({detail, loc, message}) => {
const relativeStartLine = Math.max(getRelativeLine(loc), 1);
const relativeStartColumn = Math.max(getRelativeColumn(loc), 0);
const relativeEndLine = Math.max(
getRelativeEndLine(loc, relativeStartLine),
relativeStartLine
);
const relativeEndColumn = Math.max(
getRelativeEndColumn(loc, relativeStartColumn),
relativeStartColumn
);

const markdownStartLine = block.codeStartLine + relativeStartLine - 1;
const markdownEndLine = block.codeStartLine + relativeEndLine - 1;

return {
detail,
message,
relativeStartLine,
markdownLoc: {
start: {
line: markdownStartLine,
column: relativeStartColumn,
},
end: {
line: markdownEndLine,
column: relativeEndColumn,
},
},
};
});
}

module.exports = {
normalizeDiagnostics,
};
Loading