Skip to content

Commit

Permalink
docs: autogenerate rules table on website (#5116)
Browse files Browse the repository at this point in the history
* 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 <brad.zacher@gmail.com>

* ok, use classes

* vertially arrange icons

* Remove rules table from README

* minor refactors

* Accessibility labels

Co-authored-by: Brad Zacher <brad.zacher@gmail.com>
Co-authored-by: Josh Goldberg <me@joshuakgoldberg.com>
  • Loading branch information
3 people committed Jun 25, 2022
1 parent 2de7223 commit 507629a
Show file tree
Hide file tree
Showing 14 changed files with 457 additions and 573 deletions.
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;

0 comments on commit 507629a

Please sign in to comment.