-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
/
docs.test.ts
162 lines (138 loc) · 4.81 KB
/
docs.test.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
import fs from 'fs';
import { marked } from 'marked';
import path from 'path';
import { titleCase } from 'title-case';
import rules from '../src/rules';
const docsRoot = path.resolve(__dirname, '../docs/rules');
const rulesData = Object.entries(rules);
function parseMarkdownFile(filePath: string): marked.TokensList {
const file = fs.readFileSync(filePath, 'utf-8');
return marked.lexer(file, {
gfm: true,
silent: false,
});
}
type TokenType = marked.Token['type'];
function tokenIs<Type extends TokenType>(
token: marked.Token,
type: Type,
): token is marked.Token & { type: Type } {
return token.type === type;
}
function tokenIsHeading(token: marked.Token): token is marked.Tokens.Heading {
return tokenIs(token, 'heading');
}
function tokenIsH2(
token: marked.Token,
): token is marked.Tokens.Heading & { depth: 2 } {
return (
tokenIsHeading(token) && token.depth === 2 && !/[a-z]+: /.test(token.text)
);
}
describe('Validating rule docs', () => {
const ignoredFiles = new Set([
'README.md',
'TEMPLATE.md',
// these rule docs were left behind on purpose for legacy reasons
'camelcase.md',
'no-duplicate-imports.md',
]);
it('All rules must have a corresponding rule doc', () => {
const files = fs
.readdirSync(docsRoot)
.filter(rule => !ignoredFiles.has(rule));
const ruleFiles = Object.keys(rules)
.map(rule => `${rule}.md`)
.sort();
expect(files.sort()).toEqual(ruleFiles);
});
for (const [ruleName, rule] of rulesData) {
const { description } = rule.meta.docs!;
describe(`${ruleName}.md`, () => {
const filePath = path.join(docsRoot, `${ruleName}.md`);
const tokens = parseMarkdownFile(filePath);
test(`${ruleName}.md must start with frontmatter description`, () => {
expect(tokens[0]).toMatchObject({
raw: '---\n',
type: 'hr',
});
expect(tokens[1]).toMatchObject({
text: description.includes("'")
? `description: "${description}."`
: `description: '${description}.'`,
depth: 2,
type: 'heading',
});
});
test(`${ruleName}.md must next have a blockquote directing to website`, () => {
expect(tokens[2]).toMatchObject({
text: [
`🛑 This file is source code, not the primary documentation location! 🛑`,
``,
`See **https://typescript-eslint.io/rules/${ruleName}** for documentation.`,
``,
].join('\n'),
type: 'blockquote',
});
});
test(`headers must be title-cased`, () => {
// Get all H2 headers objects as the other levels are variable by design.
const headers = tokens.filter(tokenIsH2);
headers.forEach(header =>
expect(header.text).toBe(titleCase(header.text)),
);
});
const importantHeadings = new Set([
'How to Use',
'Options',
'Related To',
'When Not To Use It',
]);
test('important headings must be h2s', () => {
const headers = tokens.filter(tokenIsHeading);
for (const header of headers) {
if (importantHeadings.has(header.raw.replace(/#/g, '').trim())) {
expect(header.depth).toBe(2);
}
}
});
});
}
});
describe('Validating rule metadata', () => {
const rulesThatRequireTypeInformationInAWayThatsHardToDetect = new Set([
// the core rule file doesn't use type information, instead it's used in `src/rules/naming-convention-utils/validator.ts`
'naming-convention',
]);
function requiresFullTypeInformation(content: string): boolean {
return /getParserServices(\(\s*[^,\s)]+)\s*(,\s*false\s*)?\)/.test(content);
}
for (const [ruleName, rule] of rulesData) {
describe(`${ruleName}`, () => {
it('`name` field in rule must match the filename', () => {
// validate if rule name is same as url
// there is no way to access this field but its used only in generation of docs url
expect(rule.meta.docs?.url).toBe(
`https://typescript-eslint.io/rules/${ruleName}`,
);
});
it('`requiresTypeChecking` should be set if the rule uses type information', () => {
if (
rulesThatRequireTypeInformationInAWayThatsHardToDetect.has(ruleName)
) {
expect(true).toEqual(rule.meta.docs?.requiresTypeChecking ?? false);
return;
}
// quick-and-dirty check to see if it uses parserServices
// not perfect but should be good enough
const ruleFileContents = fs.readFileSync(
path.resolve(__dirname, `../src/rules/${ruleName}.ts`),
'utf-8',
);
expect(requiresFullTypeInformation(ruleFileContents)).toEqual(
rule.meta.docs?.requiresTypeChecking ?? false,
);
});
});
}
});