Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 507629a

Browse files
Josh-CenabradzacherJoshuaKGoldberg
authored
docs: autogenerate rules table on website (typescript-eslint#5116)
* docs: autogenerate rules table on website * migrate rule attributes to global data * add mdlint ignore * add filter * avoid redirecting to main site * merge two columns * this is hard * refactor * tweak colors * ok - memoize this * refactors more * Apply suggestions from code review Co-authored-by: Brad Zacher <[email protected]> * ok, use classes * vertially arrange icons * Remove rules table from README * minor refactors * Accessibility labels Co-authored-by: Brad Zacher <[email protected]> Co-authored-by: Josh Goldberg <[email protected]>
1 parent 2de7223 commit 507629a

File tree

14 files changed

+457
-573
lines changed

14 files changed

+457
-573
lines changed

.markdownlint.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@
6969
"details",
7070
"summary",
7171
"Tabs",
72-
"TabItem"
72+
"TabItem",
73+
"RulesTable"
7374
]
7475
},
7576
// MD034/no-bare-urls - Bare URL used

packages/eslint-plugin/README.md

Lines changed: 1 addition & 146 deletions
Large diffs are not rendered by default.

packages/eslint-plugin/docs/rules/README.md

Lines changed: 3 additions & 139 deletions
Large diffs are not rendered by default.

packages/eslint-plugin/tests/docs.test.ts

Lines changed: 0 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,6 @@ import { titleCase } from 'title-case';
99
const docsRoot = path.resolve(__dirname, '../docs/rules');
1010
const rulesData = Object.entries(rules);
1111

12-
function createRuleLink(ruleName: string, readmePath: string): string {
13-
return `[\`@typescript-eslint/${ruleName}\`](${
14-
readmePath.includes('docs/rules') ? '.' : './docs/rules'
15-
}/${ruleName}.md)`;
16-
}
17-
1812
function parseMarkdownFile(filePath: string): marked.TokensList {
1913
const file = fs.readFileSync(filePath, 'utf-8');
2014

@@ -24,27 +18,6 @@ function parseMarkdownFile(filePath: string): marked.TokensList {
2418
});
2519
}
2620

27-
function parseReadme(readmePath: string): {
28-
base: marked.Tokens.Table;
29-
extension: marked.Tokens.Table;
30-
} {
31-
const readme = parseMarkdownFile(readmePath);
32-
33-
// find the table
34-
const rulesTables = readme.filter(
35-
(token): token is marked.Tokens.Table =>
36-
'type' in token && token.type === 'table',
37-
);
38-
if (rulesTables.length !== 2) {
39-
throw Error('Could not find both rules tables in README.md');
40-
}
41-
42-
return {
43-
base: rulesTables[0],
44-
extension: rulesTables[1],
45-
};
46-
}
47-
4821
function isEmptySchema(schema: JSONSchema4 | JSONSchema4[]): boolean {
4922
return Array.isArray(schema)
5023
? schema.length === 0
@@ -207,87 +180,3 @@ describe('Validating rule metadata', () => {
207180
});
208181
}
209182
});
210-
211-
describe.each([
212-
path.join(__dirname, '../README.md'),
213-
path.join(__dirname, '../docs/rules/README.md'),
214-
])('%s', readmePath => {
215-
const rulesTables = parseReadme(readmePath);
216-
const notDeprecated = rulesData.filter(([, rule]) => !rule.meta.deprecated);
217-
const baseRules = notDeprecated.filter(
218-
([, rule]) => !rule.meta.docs?.extendsBaseRule,
219-
);
220-
const extensionRules = notDeprecated.filter(
221-
([, rule]) => rule.meta.docs?.extendsBaseRule,
222-
);
223-
224-
it('All non-deprecated base rules should have a row in the base rules table, and the table should be ordered alphabetically', () => {
225-
const baseRuleNames = baseRules
226-
.map(([ruleName]) => ruleName)
227-
.sort()
228-
.map(ruleName => createRuleLink(ruleName, readmePath));
229-
230-
expect(rulesTables.base.rows.map(row => row[0].text)).toStrictEqual(
231-
baseRuleNames,
232-
);
233-
});
234-
it('All non-deprecated extension rules should have a row in the base rules table, and the table should be ordered alphabetically', () => {
235-
const extensionRuleNames = extensionRules
236-
.map(([ruleName]) => ruleName)
237-
.sort()
238-
.map(ruleName => createRuleLink(ruleName, readmePath));
239-
240-
expect(rulesTables.extension.rows.map(row => row[0].text)).toStrictEqual(
241-
extensionRuleNames,
242-
);
243-
});
244-
245-
for (const [ruleName, rule] of notDeprecated) {
246-
describe(`Checking rule ${ruleName}`, () => {
247-
const ruleRow: string[] | undefined = (
248-
rule.meta.docs?.extendsBaseRule
249-
? rulesTables.extension.rows
250-
: rulesTables.base.rows
251-
)
252-
.find(row => row[0].text.includes(`/${ruleName}.md`))
253-
?.map(cell => cell.text);
254-
if (!ruleRow) {
255-
// rule is in the wrong table, the first two tests will catch this, so no point in creating noise;
256-
// these tests will ofc fail in that case
257-
return;
258-
}
259-
260-
it('Link column should be correct', () => {
261-
expect(ruleRow[0]).toBe(createRuleLink(ruleName, readmePath));
262-
});
263-
264-
it('Description column should be correct', () => {
265-
expect(ruleRow[1]).toBe(rule.meta.docs?.description);
266-
});
267-
268-
it('Recommended column should be correct', () => {
269-
expect(ruleRow[2]).toBe(
270-
rule.meta.docs?.recommended === 'strict'
271-
? ':lock:'
272-
: rule.meta.docs?.recommended
273-
? ':white_check_mark:'
274-
: '',
275-
);
276-
});
277-
278-
it('Fixable column should be correct', () => {
279-
expect(ruleRow[3]).toBe(
280-
rule.meta.fixable !== undefined ? ':wrench:' : '',
281-
);
282-
});
283-
284-
it('Requiring type information column should be correct', () => {
285-
expect(ruleRow[4]).toBe(
286-
rule.meta.docs?.requiresTypeChecking === true
287-
? ':thought_balloon:'
288-
: '',
289-
);
290-
});
291-
});
292-
}
293-
});

packages/website/docusaurusConfig.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { UserThemeConfig as ThemeCommonConfig } from '@docusaurus/theme-com
55
import type { UserThemeConfig as AlgoliaThemeConfig } from '@docusaurus/theme-search-algolia';
66
import type { Config } from '@docusaurus/types';
77

8+
import { rulesMeta } from './rulesMeta';
89
import npm2yarnPlugin from '@docusaurus/remark-plugin-npm2yarn';
910
import tabsPlugin from 'remark-docusaurus-tabs';
1011
import { addRuleAttributesList } from './plugins/add-rule-attributes-list';
@@ -175,6 +176,9 @@ const config: Config = {
175176
projectName: 'typescript-eslint',
176177
clientModules: [require.resolve('./src/clientModules.js')],
177178
presets: [['classic', presetClassicOptions]],
179+
customFields: {
180+
rules: rulesMeta,
181+
},
178182
plugins: [
179183
require.resolve('./webpack.plugin'),
180184
['@docusaurus/plugin-content-docs', pluginContentDocsOptions],
Lines changed: 10 additions & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type * as unist from 'unist';
12
import type * as mdast from 'mdast';
23
import type { Plugin } from 'unified';
34

@@ -13,191 +14,25 @@ const addRuleAttributesList: Plugin = () => {
1314
if (rule == null) {
1415
return;
1516
}
16-
const config = ((): 'recommended' | 'strict' | null => {
17-
switch (rule.meta.docs?.recommended) {
18-
case 'error':
19-
case 'warn':
20-
return 'recommended';
21-
22-
case 'strict':
23-
return 'strict';
24-
25-
default:
26-
return null;
27-
}
28-
})();
29-
const autoFixable = rule.meta.fixable != null;
30-
const suggestionFixable = rule.meta.hasSuggestions === true;
31-
const requiresTypeInfo = rule.meta.docs?.requiresTypeChecking === true;
32-
33-
const parent = root as mdast.Parent;
34-
/*
35-
This just outputs a list with a heading like:
36-
37-
## Attributes
38-
39-
- [ ] Config
40-
- [ ] ✅ Recommended
41-
- [ ] 🔒 Strict
42-
- [ ] Fixable
43-
- [ ] 🔧 Automated Fixer (`--fix`)
44-
- [ ] 🛠 Suggestion Fixer
45-
- [ ] 💭 Requires type information
46-
*/
47-
const heading = Heading({
48-
depth: 2,
49-
text: 'Attributes',
50-
});
51-
const ruleAttributes = List({
52-
children: [
53-
NestedList({
54-
checked: config != null,
55-
children: [
56-
ListItem({
57-
checked: config === 'recommended',
58-
text: '✅ Recommended',
59-
}),
60-
ListItem({
61-
checked: config === 'strict' || config === 'recommended',
62-
text: '🔒 Strict',
63-
}),
64-
],
65-
text: 'Included in configs',
66-
}),
67-
NestedList({
68-
checked: autoFixable || suggestionFixable,
69-
children: [
70-
ListItem({
71-
checked: autoFixable,
72-
text: '🔧 Automated Fixer',
73-
}),
74-
ListItem({
75-
checked: suggestionFixable,
76-
text: '🛠 Suggestion Fixer',
77-
}),
78-
],
79-
text: 'Fixable',
80-
}),
81-
ListItem({
82-
checked: requiresTypeInfo,
83-
text: '💭 Requires type information',
84-
}),
85-
],
86-
});
8717

18+
const parent = root as unist.Parent;
8819
const h2Idx = parent.children.findIndex(
89-
child => child.type === 'heading' && child.depth === 2,
20+
child => child.type === 'heading' && (child as mdast.Heading).depth === 2,
9021
);
22+
// The actual content will be injected on client side.
23+
const attrNode = {
24+
type: 'jsx',
25+
value: `<rule-attributes name="${file.stem}" />`,
26+
};
9127
if (h2Idx != null) {
9228
// insert it just before the first h2 in the doc
9329
// this should be just after the rule's description
94-
parent.children.splice(h2Idx, 0, heading, ruleAttributes);
30+
parent.children.splice(h2Idx, 0, attrNode);
9531
} else {
9632
// failing that, add it to the end
97-
parent.children.push(heading, ruleAttributes);
33+
parent.children.push(attrNode);
9834
}
9935
};
10036
};
10137

102-
function Heading({
103-
depth,
104-
text,
105-
id = text.toLowerCase(),
106-
}: {
107-
depth: mdast.Heading['depth'];
108-
id?: string;
109-
text: string;
110-
}): mdast.Heading {
111-
return {
112-
type: 'heading',
113-
depth,
114-
children: [
115-
{
116-
type: 'text',
117-
value: text,
118-
},
119-
],
120-
data: {
121-
hProperties: {
122-
id,
123-
},
124-
id,
125-
},
126-
};
127-
}
128-
129-
function Paragraph({ text }: { text: string }): mdast.Paragraph {
130-
return {
131-
type: 'paragraph',
132-
children: [
133-
{
134-
type: 'text',
135-
value: text,
136-
},
137-
],
138-
};
139-
}
140-
141-
function ListItem({
142-
checked,
143-
text,
144-
}: {
145-
checked: boolean;
146-
text: string;
147-
}): mdast.ListItem {
148-
return {
149-
type: 'listItem',
150-
spread: false,
151-
checked: checked,
152-
children: [
153-
{
154-
type: 'paragraph',
155-
children: [
156-
{
157-
type: 'text',
158-
value: text,
159-
},
160-
],
161-
},
162-
],
163-
};
164-
}
165-
166-
function NestedList({
167-
children,
168-
checked,
169-
text,
170-
}: {
171-
children: mdast.ListItem[];
172-
checked: boolean;
173-
text: string;
174-
}): mdast.ListItem {
175-
return {
176-
type: 'listItem',
177-
spread: false,
178-
checked: checked,
179-
children: [
180-
Paragraph({
181-
text,
182-
}),
183-
List({
184-
children,
185-
}),
186-
],
187-
data: {
188-
className: 'test',
189-
},
190-
};
191-
}
192-
193-
function List({ children }: { children: mdast.ListItem[] }): mdast.List {
194-
return {
195-
type: 'list',
196-
ordered: false,
197-
start: null,
198-
spread: false,
199-
children,
200-
};
201-
}
202-
20338
export { addRuleAttributesList };

packages/website/rulesMeta.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as eslintPlugin from '@typescript-eslint/eslint-plugin';
2+
3+
export const rulesMeta = Object.entries(eslintPlugin.rules).map(
4+
([name, content]) => ({
5+
name,
6+
type: content.meta.type,
7+
docs: content.meta.docs,
8+
fixable: content.meta.fixable,
9+
hasSuggestions: content.meta.hasSuggestions,
10+
deprecated: content.meta.deprecated,
11+
replacedBy: content.meta.replacedBy,
12+
}),
13+
);
14+
15+
export type RulesMeta = typeof rulesMeta;

0 commit comments

Comments
 (0)