Skip to content

docs: autogenerate rules table on website #5116

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Jun 25, 2022
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
3 changes: 2 additions & 1 deletion .markdownlint.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@
"details",
"summary",
"Tabs",
"TabItem"
"TabItem",
"RulesTable"
]
},
// MD034/no-bare-urls - Bare URL used
Expand Down
147 changes: 1 addition & 146 deletions packages/eslint-plugin/README.md

Large diffs are not rendered by default.

142 changes: 3 additions & 139 deletions packages/eslint-plugin/docs/rules/README.md

Large diffs are not rendered by default.

111 changes: 0 additions & 111 deletions packages/eslint-plugin/tests/docs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,6 @@ import { titleCase } from 'title-case';
const docsRoot = path.resolve(__dirname, '../docs/rules');
const rulesData = Object.entries(rules);

function createRuleLink(ruleName: string, readmePath: string): string {
return `[\`@typescript-eslint/${ruleName}\`](${
readmePath.includes('docs/rules') ? '.' : './docs/rules'
}/${ruleName}.md)`;
}

function parseMarkdownFile(filePath: string): marked.TokensList {
const file = fs.readFileSync(filePath, 'utf-8');

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

function parseReadme(readmePath: string): {
base: marked.Tokens.Table;
extension: marked.Tokens.Table;
} {
const readme = parseMarkdownFile(readmePath);

// find the table
const rulesTables = readme.filter(
(token): token is marked.Tokens.Table =>
'type' in token && token.type === 'table',
);
if (rulesTables.length !== 2) {
throw Error('Could not find both rules tables in README.md');
}

return {
base: rulesTables[0],
extension: rulesTables[1],
};
}

function isEmptySchema(schema: JSONSchema4 | JSONSchema4[]): boolean {
return Array.isArray(schema)
? schema.length === 0
Expand Down Expand Up @@ -207,87 +180,3 @@ describe('Validating rule metadata', () => {
});
}
});

describe.each([
path.join(__dirname, '../README.md'),
path.join(__dirname, '../docs/rules/README.md'),
])('%s', readmePath => {
const rulesTables = parseReadme(readmePath);
const notDeprecated = rulesData.filter(([, rule]) => !rule.meta.deprecated);
const baseRules = notDeprecated.filter(
([, rule]) => !rule.meta.docs?.extendsBaseRule,
);
const extensionRules = notDeprecated.filter(
([, rule]) => rule.meta.docs?.extendsBaseRule,
);

it('All non-deprecated base rules should have a row in the base rules table, and the table should be ordered alphabetically', () => {
const baseRuleNames = baseRules
.map(([ruleName]) => ruleName)
.sort()
.map(ruleName => createRuleLink(ruleName, readmePath));

expect(rulesTables.base.rows.map(row => row[0].text)).toStrictEqual(
baseRuleNames,
);
});
it('All non-deprecated extension rules should have a row in the base rules table, and the table should be ordered alphabetically', () => {
const extensionRuleNames = extensionRules
.map(([ruleName]) => ruleName)
.sort()
.map(ruleName => createRuleLink(ruleName, readmePath));

expect(rulesTables.extension.rows.map(row => row[0].text)).toStrictEqual(
extensionRuleNames,
);
});

for (const [ruleName, rule] of notDeprecated) {
describe(`Checking rule ${ruleName}`, () => {
const ruleRow: string[] | undefined = (
rule.meta.docs?.extendsBaseRule
? rulesTables.extension.rows
: rulesTables.base.rows
)
.find(row => row[0].text.includes(`/${ruleName}.md`))
?.map(cell => cell.text);
if (!ruleRow) {
// rule is in the wrong table, the first two tests will catch this, so no point in creating noise;
// these tests will ofc fail in that case
return;
}

it('Link column should be correct', () => {
expect(ruleRow[0]).toBe(createRuleLink(ruleName, readmePath));
});

it('Description column should be correct', () => {
expect(ruleRow[1]).toBe(rule.meta.docs?.description);
});

it('Recommended column should be correct', () => {
expect(ruleRow[2]).toBe(
rule.meta.docs?.recommended === 'strict'
? ':lock:'
: rule.meta.docs?.recommended
? ':white_check_mark:'
: '',
);
});

it('Fixable column should be correct', () => {
expect(ruleRow[3]).toBe(
rule.meta.fixable !== undefined ? ':wrench:' : '',
);
});

it('Requiring type information column should be correct', () => {
expect(ruleRow[4]).toBe(
rule.meta.docs?.requiresTypeChecking === true
? ':thought_balloon:'
: '',
);
});
});
}
});
4 changes: 4 additions & 0 deletions packages/website/docusaurusConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { UserThemeConfig as ThemeCommonConfig } from '@docusaurus/theme-com
import type { UserThemeConfig as AlgoliaThemeConfig } from '@docusaurus/theme-search-algolia';
import type { Config } from '@docusaurus/types';

import { rulesMeta } from './rulesMeta';
import npm2yarnPlugin from '@docusaurus/remark-plugin-npm2yarn';
import tabsPlugin from 'remark-docusaurus-tabs';
import { addRuleAttributesList } from './plugins/add-rule-attributes-list';
Expand Down Expand Up @@ -175,6 +176,9 @@ const config: Config = {
projectName: 'typescript-eslint',
clientModules: [require.resolve('./src/clientModules.js')],
presets: [['classic', presetClassicOptions]],
customFields: {
rules: rulesMeta,
},
plugins: [
require.resolve('./webpack.plugin'),
['@docusaurus/plugin-content-docs', pluginContentDocsOptions],
Expand Down
185 changes: 10 additions & 175 deletions packages/website/plugins/add-rule-attributes-list.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type * as unist from 'unist';
import type * as mdast from 'mdast';
import type { Plugin } from 'unified';

Expand All @@ -13,191 +14,25 @@ const addRuleAttributesList: Plugin = () => {
if (rule == null) {
return;
}
const config = ((): 'recommended' | 'strict' | null => {
switch (rule.meta.docs?.recommended) {
case 'error':
case 'warn':
return 'recommended';

case 'strict':
return 'strict';

default:
return null;
}
})();
const autoFixable = rule.meta.fixable != null;
const suggestionFixable = rule.meta.hasSuggestions === true;
const requiresTypeInfo = rule.meta.docs?.requiresTypeChecking === true;

const parent = root as mdast.Parent;
/*
This just outputs a list with a heading like:

## Attributes

- [ ] Config
- [ ] ✅ Recommended
- [ ] 🔒 Strict
- [ ] Fixable
- [ ] 🔧 Automated Fixer (`--fix`)
- [ ] 🛠 Suggestion Fixer
- [ ] 💭 Requires type information
*/
const heading = Heading({
depth: 2,
text: 'Attributes',
});
const ruleAttributes = List({
children: [
NestedList({
checked: config != null,
children: [
ListItem({
checked: config === 'recommended',
text: '✅ Recommended',
}),
ListItem({
checked: config === 'strict' || config === 'recommended',
text: '🔒 Strict',
}),
],
text: 'Included in configs',
}),
NestedList({
checked: autoFixable || suggestionFixable,
children: [
ListItem({
checked: autoFixable,
text: '🔧 Automated Fixer',
}),
ListItem({
checked: suggestionFixable,
text: '🛠 Suggestion Fixer',
}),
],
text: 'Fixable',
}),
ListItem({
checked: requiresTypeInfo,
text: '💭 Requires type information',
}),
],
});

const parent = root as unist.Parent;
const h2Idx = parent.children.findIndex(
child => child.type === 'heading' && child.depth === 2,
child => child.type === 'heading' && (child as mdast.Heading).depth === 2,
);
// The actual content will be injected on client side.
const attrNode = {
type: 'jsx',
value: `<rule-attributes name="${file.stem}" />`,
};
if (h2Idx != null) {
// insert it just before the first h2 in the doc
// this should be just after the rule's description
parent.children.splice(h2Idx, 0, heading, ruleAttributes);
parent.children.splice(h2Idx, 0, attrNode);
} else {
// failing that, add it to the end
parent.children.push(heading, ruleAttributes);
parent.children.push(attrNode);
}
};
};

function Heading({
depth,
text,
id = text.toLowerCase(),
}: {
depth: mdast.Heading['depth'];
id?: string;
text: string;
}): mdast.Heading {
return {
type: 'heading',
depth,
children: [
{
type: 'text',
value: text,
},
],
data: {
hProperties: {
id,
},
id,
},
};
}

function Paragraph({ text }: { text: string }): mdast.Paragraph {
return {
type: 'paragraph',
children: [
{
type: 'text',
value: text,
},
],
};
}

function ListItem({
checked,
text,
}: {
checked: boolean;
text: string;
}): mdast.ListItem {
return {
type: 'listItem',
spread: false,
checked: checked,
children: [
{
type: 'paragraph',
children: [
{
type: 'text',
value: text,
},
],
},
],
};
}

function NestedList({
children,
checked,
text,
}: {
children: mdast.ListItem[];
checked: boolean;
text: string;
}): mdast.ListItem {
return {
type: 'listItem',
spread: false,
checked: checked,
children: [
Paragraph({
text,
}),
List({
children,
}),
],
data: {
className: 'test',
},
};
}

function List({ children }: { children: mdast.ListItem[] }): mdast.List {
return {
type: 'list',
ordered: false,
start: null,
spread: false,
children,
};
}

export { addRuleAttributesList };
15 changes: 15 additions & 0 deletions packages/website/rulesMeta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as eslintPlugin from '@typescript-eslint/eslint-plugin';

export const rulesMeta = Object.entries(eslintPlugin.rules).map(
([name, content]) => ({
name,
type: content.meta.type,
docs: content.meta.docs,
fixable: content.meta.fixable,
hasSuggestions: content.meta.hasSuggestions,
deprecated: content.meta.deprecated,
replacedBy: content.meta.replacedBy,
}),
);

export type RulesMeta = typeof rulesMeta;
Loading