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

Skip to content

Commit 3256f19

Browse files
committed
WIP - documentation validator
1 parent 8a0a0dd commit 3256f19

File tree

6 files changed

+308
-10
lines changed

6 files changed

+308
-10
lines changed

packages/eslint-plugin/docs/README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Writing Rule Documentation
2+
3+
Before you read ahead, know that you don't need to know all of this off the top of your head.
4+
If you run the `docs:check` script, it will apply some validation to your documentation, and help you figure out what order things should be in, and what sections should be included.
5+
6+
Three quick things:
7+
8+
- Every single rule should have a markdown file in the docs/rules folder named `rule-name.md`.
9+
- Every _not deprecated_ rule should have a row within the ["Supported Rules"](../../README.md#supported-rules) table in the plugin's `README.md`.
10+
- Every rule based off a `tslint` rule should be appropriately marked up within `ROADMAP.md`.
11+
12+
## Documenting a Rule
13+
14+
A rule's documentation should have the following sections - in this order
15+
Note that sections marked with `(required)` are required, and will block your PR if you skip them.
16+
17+
### Title + Long Description (required)
18+
19+
The document title should be a level 1 heading matching the following pattern:
20+
`# {description from rule.meta.docs.description} ({rule name})`
21+
This should be the one and only level one header.
22+
23+
Immediately proceeding the header must be a long-form description of the rule. There's no hard-and-fast rule about how long this description should be, but you should try to avoid writing more than a few lines unless you think the rule needs the backstory. Your PR reviewer should help you out and ask you to shorten it if they think it's too long.
24+
25+
### Options (required)
26+
27+
This section should begin with a level 2 title matching the following pattern:
28+
`## Options`
29+
30+
If your rule has no options, then you should just include the text `None.`.
31+
If your rule has options, you should do two things:
32+
33+
1. Include a TypeScript code block which uses types/interfaces to describe the options accepted by the rule.
34+
- Everything should have `//` comments briefly describing what each
35+
1. Include a second TypeScript code block which shows the default config for the rule.
36+
37+
### How to Configure (optional)
38+
39+
This section should begin with a level 2 title matching the following pattern:
40+
`## How to Configure`
41+
42+
If your rule is a bit complicated to configure, you should consider adding this section.
43+
If your rule extends a base rule, you must add this section, and in the example you must explicitly show the base rule being disabled.
44+
45+
### Examples (required)
46+
47+
This section should begin with a level 2 title matching the following pattern:
48+
`## Examples`
49+
50+
In this section you should include two TypeScript code blocks; one showing cases your rule will report on, the other showing how to correct those same cases. These examples should be in the form of a TypeScript code block; you can include as many cases within each code block.
51+
52+
If your rule has no options, then you just need a one valid and one invalid block to demonstrate how the rule works.
53+
54+
If your rule has options, you should include one of each block for each option in your rule, demonstrating the effect of each option.

packages/eslint-plugin/docs/rules/adjacent-overload-signatures.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
Grouping overloaded members together can improve readability of the code.
44

5+
## Options
6+
7+
None.
8+
59
## Rule Details
610

711
This rule aims to standardise the way overloaded members are organized.

packages/eslint-plugin/tools/validate-docs/index.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,34 @@ import { checkForRuleDocs } from './check-for-rule-docs';
44
import { parseReadme } from './parse-readme';
55
import { validateTableStructure } from './validate-table-structure';
66
import { validateTableRules } from './validate-table-rules';
7+
import { validateRuleDocs } from './validate-rule-docs';
78

89
const { rules } = plugin;
910

1011
let hasErrors = false;
11-
console.log(chalk.underline('Checking for rule docs'));
12-
hasErrors = hasErrors || checkForRuleDocs(rules);
1312

1413
console.log();
15-
console.log(chalk.underline('Valdiating README.md'));
14+
console.log(chalk.underline('Checking rule docs'));
15+
16+
console.log();
17+
console.log(chalk.italic('Checking for existance'));
18+
hasErrors = checkForRuleDocs(rules) || hasErrors;
19+
20+
console.log();
21+
console.log(chalk.italic('Checking content'));
22+
hasErrors = validateRuleDocs(rules) || hasErrors;
23+
24+
console.log();
25+
console.log(chalk.underline('Validating README.md'));
1626
const rulesTable = parseReadme();
1727

1828
console.log();
1929
console.log(chalk.italic('Checking table structure...'));
20-
hasErrors = hasErrors || validateTableStructure(rules, rulesTable);
30+
hasErrors = validateTableStructure(rules, rulesTable) || hasErrors;
2131

2232
console.log();
2333
console.log(chalk.italic('Checking rules...'));
24-
hasErrors = hasErrors || validateTableRules(rules, rulesTable);
34+
hasErrors = validateTableRules(rules, rulesTable) || hasErrors;
2535

2636
if (hasErrors) {
2737
console.log('\n\n');
Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
11
import chalk from 'chalk';
22

3+
const TICK = chalk.bold.green('✔');
4+
const CROSS = chalk.bold.red('✗');
5+
6+
const STARTS_WITH_STRING = /^([ ]+)/;
37
function logRule(
48
success: boolean,
59
ruleName: string,
610
...messages: string[]
711
): void {
812
if (success) {
9-
console.log(chalk.bold.green('✔'), chalk.dim(ruleName));
13+
console.log(TICK, chalk.dim(ruleName));
1014
} else {
1115
logError(chalk.bold(ruleName));
12-
messages.forEach(m => console.error(chalk.bold.red(' -'), m));
16+
messages.forEach(m => {
17+
const messagePreIndent = STARTS_WITH_STRING.exec(m);
18+
const indent = messagePreIndent ? ` ${messagePreIndent[1]}` : ' ';
19+
console.error(chalk.bold.red(`${indent}-`), m.trimLeft());
20+
});
1321
}
1422
}
1523

1624
function logError(...messages: string[]): void {
17-
console.error(chalk.bold.red('✗'), ...messages);
25+
console.error(CROSS, ...messages);
1826
}
1927

20-
export { logError, logRule };
28+
export { logError, logRule, TICK, CROSS };
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import { TSESLint } from '@typescript-eslint/experimental-utils';
2+
import fs from 'fs';
3+
import path from 'path';
4+
import marked from 'marked';
5+
import { logError, logRule } from './log';
6+
7+
const docsFolder = path.resolve(__dirname, '../../docs/rules');
8+
const REQUIRED_TITLE = 1;
9+
const OPTIONAL_TITLE = 1 << 1;
10+
// defines all of the required sections and their expected ordering in the document
11+
let i = 1;
12+
enum TitleType {
13+
RuleTitle = (i++ << 2) | REQUIRED_TITLE,
14+
Options = (i++ << 2) | REQUIRED_TITLE,
15+
HowToConfigure = (i++ << 2) | OPTIONAL_TITLE,
16+
Examples = (i++ << 2) | REQUIRED_TITLE,
17+
WhenNotToUseIt = (i++ << 2) | REQUIRED_TITLE,
18+
RelatedTo = (i++ << 2) | OPTIONAL_TITLE,
19+
}
20+
export function getTitleTypeValue(key: string): number {
21+
return (TitleType[key as any] as any) as number;
22+
}
23+
24+
const expectedTitleOrder: TitleType[] = Object.keys(TitleType)
25+
.filter(k => typeof TitleType[k as any] === 'string')
26+
.map(k => parseInt(k));
27+
28+
type Rule = TSESLint.RuleModule<any, any> & { name: string };
29+
30+
function validateRuleDoc(rule: Rule, ruleDoc: marked.TokensList): boolean {
31+
let hasErrors = false;
32+
const titleOrder: TitleType[] = [];
33+
const sections: Map<TitleType, marked.Token[]> = new Map();
34+
let lastSeenTitle: TitleType;
35+
36+
function assertDepth(token: marked.Tokens.Heading, depth: number): void {
37+
if (token.depth !== depth) {
38+
hasErrors = true;
39+
logError(
40+
`Expected ${
41+
token.text
42+
} to have heading level ${depth}, but instead found ${token.depth}.`,
43+
);
44+
}
45+
}
46+
function assertOnlyOne(type: TitleType, name: string): boolean {
47+
if (sections.has(type)) {
48+
hasErrors = true;
49+
logError(
50+
`Detected multiple ${name} headings when there should be only one.`,
51+
);
52+
53+
return false;
54+
}
55+
56+
return true;
57+
}
58+
59+
ruleDoc.forEach(token => {
60+
if (token.type === 'heading') {
61+
if (token.depth === 1) {
62+
// assume it's the rule title
63+
if (!assertOnlyOne(TitleType.RuleTitle, 'level 1')) {
64+
return;
65+
}
66+
67+
titleOrder.push(TitleType.RuleTitle);
68+
lastSeenTitle = TitleType.RuleTitle;
69+
sections.set(TitleType.RuleTitle, []);
70+
71+
const expectedText = `${rule.meta.docs.description} (${rule.name})`;
72+
if (token.text !== expectedText) {
73+
hasErrors = true;
74+
logError(
75+
'Invalid rule title content found.',
76+
`- expected: "${expectedText}"`,
77+
`- received: "${token.text}"`,
78+
);
79+
}
80+
return;
81+
}
82+
83+
if (token.text === 'Options') {
84+
if (!assertOnlyOne(TitleType.Options, token.text)) {
85+
return;
86+
}
87+
88+
titleOrder.push(TitleType.Options);
89+
lastSeenTitle = TitleType.Options;
90+
sections.set(TitleType.Options, []);
91+
92+
assertDepth(token, 2);
93+
return;
94+
}
95+
96+
if (token.text === 'How to Configure') {
97+
if (!assertOnlyOne(TitleType.HowToConfigure, token.text)) {
98+
return;
99+
}
100+
101+
titleOrder.push(TitleType.HowToConfigure);
102+
lastSeenTitle = TitleType.HowToConfigure;
103+
sections.set(TitleType.HowToConfigure, []);
104+
105+
assertDepth(token, 2);
106+
return;
107+
}
108+
109+
if (token.text === 'Examples') {
110+
if (!assertOnlyOne(TitleType.Examples, token.text)) {
111+
return;
112+
}
113+
114+
titleOrder.push(TitleType.Examples);
115+
lastSeenTitle = TitleType.Examples;
116+
sections.set(TitleType.Examples, []);
117+
118+
assertDepth(token, 2);
119+
return;
120+
}
121+
122+
if (token.text === 'When Not To Use It') {
123+
if (!assertOnlyOne(TitleType.WhenNotToUseIt, token.text)) {
124+
return;
125+
}
126+
127+
titleOrder.push(TitleType.WhenNotToUseIt);
128+
lastSeenTitle = TitleType.WhenNotToUseIt;
129+
sections.set(TitleType.WhenNotToUseIt, []);
130+
131+
assertDepth(token, 2);
132+
return;
133+
}
134+
135+
if (token.text === 'RelatedTo') {
136+
if (!assertOnlyOne(TitleType.RelatedTo, token.text)) {
137+
return;
138+
}
139+
140+
titleOrder.push(TitleType.RelatedTo);
141+
lastSeenTitle = TitleType.RelatedTo;
142+
sections.set(TitleType.RelatedTo, []);
143+
144+
assertDepth(token, 2);
145+
return;
146+
}
147+
148+
// block other level 2 headers
149+
if (token.depth === 2) {
150+
hasErrors = true;
151+
logRule(false, rule.name, `Unexpected level 2 header: ${token.text}`);
152+
return;
153+
}
154+
}
155+
156+
sections.get(lastSeenTitle)!.push(token);
157+
});
158+
159+
// expect the section order is correct
160+
const sortedTitles = [...titleOrder].sort((a, b) => a - b);
161+
const isIncorrectlySorted = sortedTitles.some(
162+
(title, i) => titleOrder[i] !== title,
163+
);
164+
if (isIncorrectlySorted) {
165+
hasErrors = true;
166+
logRule(
167+
false,
168+
rule.name,
169+
'Sections are in the wrong order.',
170+
` Expected ${sortedTitles.map(t => TitleType[t]).join(', ')}.`,
171+
` Received ${titleOrder.map(t => TitleType[t]).join(', ')}.`,
172+
);
173+
}
174+
175+
expectedTitleOrder
176+
.filter(t => (t & OPTIONAL_TITLE) === 0)
177+
.forEach(title => {
178+
if (!titleOrder.includes(title)) {
179+
hasErrors = true;
180+
logRule(false, rule.name, `Missing title ${TitleType[title]}`);
181+
}
182+
});
183+
184+
if (!hasErrors) {
185+
logRule(true, rule.name);
186+
}
187+
188+
return hasErrors;
189+
}
190+
191+
function validateRuleDocs(
192+
rules: Record<string, TSESLint.RuleModule<any, any>>,
193+
): boolean {
194+
let hasErrors = false;
195+
Object.entries(rules).forEach(([ruleName, rule]) => {
196+
try {
197+
const fileContents = fs.readFileSync(
198+
path.resolve(docsFolder, `${ruleName}.md`),
199+
'utf8',
200+
);
201+
const parsed = marked.lexer(fileContents, {
202+
gfm: true,
203+
tables: true,
204+
silent: false,
205+
});
206+
207+
hasErrors =
208+
validateRuleDoc({ name: ruleName, ...rule }, parsed) || hasErrors;
209+
} catch (e) {
210+
hasErrors = true;
211+
console.error(`Error occurred whilst reading docs for ${ruleName}:`, e);
212+
}
213+
});
214+
215+
return hasErrors;
216+
}
217+
218+
export { validateRuleDocs };

packages/eslint-plugin/tools/validate-docs/validate-table-structure.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { TSESLint } from '@typescript-eslint/experimental-utils';
22
import chalk from 'chalk';
33
import marked from 'marked';
4-
import { logError } from './log';
4+
import { logError, TICK } from './log';
55

66
function validateTableStructure(
77
rules: Record<string, TSESLint.RuleModule<any, any>>,
@@ -42,6 +42,10 @@ function validateTableStructure(
4242
}
4343
});
4444

45+
if (!hasErrors) {
46+
console.log(TICK);
47+
}
48+
4549
return hasErrors;
4650
}
4751

0 commit comments

Comments
 (0)