/
index.js
194 lines (169 loc) · 7.98 KB
/
index.js
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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
/* eslint-env mocha */
'use strict';
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const plugin = require('..');
const ruleFiles = fs.readdirSync(path.resolve(__dirname, '../lib/rules/'))
.map((f) => path.basename(f, '.js'));
describe('all rule files should be exported by the plugin', () => {
ruleFiles.forEach((ruleName) => {
it(`should export ${ruleName}`, () => {
assert.equal(
plugin.rules[ruleName],
require(path.join('../lib/rules', ruleName)) // eslint-disable-line global-require, import/no-dynamic-require
);
});
});
});
describe('rule documentation files have the correct content', () => {
const MESSAGES = {
configs: '💼 This rule is enabled in the following [configs](https://github.com/jsx-eslint/eslint-plugin-react#shareable-configurations):',
configsOff: 'This rule is disabled in the following configs:',
deprecated: '❌ This rule is deprecated.',
fixable: '🔧 This rule is automatically fixable using the `--fix` [flag](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix) on the command line.',
hasSuggestions: '💡 This rule provides editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).',
};
function getConfigsForRule(ruleName, checkForEnabled) {
const configNames = [];
Object.keys(plugin.configs).forEach((configName) => {
const value = plugin.configs[configName].rules[`react/${ruleName}`];
const isOn = [2, 'error'].includes(value);
const isOff = [0, 'off'].includes(value);
if (value !== undefined && ((checkForEnabled && isOn) || (!checkForEnabled && isOff))) {
configNames.push(configName);
}
});
return configNames.sort();
}
function configNamesToList(configNames) {
return `\`${configNames.join('`, `')}\``;
}
ruleFiles.forEach((ruleName) => {
it(ruleName, () => {
const rule = plugin.rules[ruleName];
const documentPath = path.join('docs', 'rules', `${ruleName}.md`);
const documentContents = fs.readFileSync(documentPath, 'utf8');
const documentLines = documentContents.split('\n');
// Check title.
const expectedTitle = `# ${rule.meta.docs.description} (react/${ruleName})`;
assert.strictEqual(documentLines[0], expectedTitle, 'includes the rule description and name in title');
// Decide which notices should be shown at the top of the doc.
const expectedNotices = [];
const unexpectedNotices = [];
if (rule.meta.deprecated) {
expectedNotices.push('deprecated');
unexpectedNotices.push('configs');
} else {
unexpectedNotices.push('deprecated');
expectedNotices.push('configs');
}
if (rule.meta.fixable) {
expectedNotices.push('fixable');
} else {
unexpectedNotices.push('fixable');
}
if (rule.meta.hasSuggestions) {
expectedNotices.push('hasSuggestions');
} else {
unexpectedNotices.push('hasSuggestions');
}
// Ensure that expected notices are present in the correct order.
let currentLineNumber = 1;
expectedNotices.forEach((expectedNotice) => {
assert.strictEqual(documentLines[currentLineNumber], '', `includes blank line ahead of ${expectedNotice} notice`);
if (expectedNotice === 'deprecated' && documentLines[currentLineNumber + 1] !== MESSAGES[expectedNotice] && documentLines[currentLineNumber + 1].startsWith(MESSAGES[expectedNotice])) {
// Allow additional rule-specific information at the end of the deprecation notice line.
assert.ok(true, `includes ${expectedNotice} notice`);
} else if (expectedNotice === 'configs') {
// Check that the rule specifies its configs.
const configsOn = getConfigsForRule(ruleName, true);
let expectedMessage = `${MESSAGES.configs} ${configNamesToList(configsOn)}.`;
const configsOff = getConfigsForRule(ruleName, false);
if (configsOff.length > 0) {
expectedMessage += ` ${MESSAGES.configsOff} ${configNamesToList(configsOff)}.`;
}
assert.strictEqual(documentLines[currentLineNumber + 1], expectedMessage, 'includes configs notice');
} else {
// Otherwise, just check the whole line.
assert.strictEqual(documentLines[currentLineNumber + 1], MESSAGES[expectedNotice], `includes ${expectedNotice} notice`);
}
currentLineNumber += 2;
});
// Ensure that unexpected notices are not present.
unexpectedNotices.forEach((unexpectedNotice) => {
assert.ok(!documentContents.includes(MESSAGES[unexpectedNotice]), `does not include unexpected ${unexpectedNotice} notice`);
});
// Check for Rule Details section.
assert.ok(documentContents.includes('## Rule Details'), 'should have a "## Rule Details" section');
// Check if the rule has configuration options.
if (
(Array.isArray(rule.meta.schema) && rule.meta.schema.length > 0)
|| (typeof rule.meta.schema === 'object' && Object.keys(rule.meta.schema).length > 0)
) {
// Should have an options section header:
assert.ok(documentContents.includes('## Rule Options'), 'should have a "## Rule Options" section');
} else {
// Should NOT have any options section header:
assert.ok(!documentContents.includes('## Rule Options'), 'should not have a "## Rule Options" section');
}
});
});
});
describe('deprecated rules', () => {
it('marks all deprecated rules as deprecated', () => {
ruleFiles.forEach((ruleName) => {
const inDeprecatedRules = Boolean(plugin.deprecatedRules[ruleName]);
const isDeprecated = plugin.rules[ruleName].meta.deprecated;
if (inDeprecatedRules) {
assert(isDeprecated, `${ruleName} metadata should mark it as deprecated`);
} else {
assert(!isDeprecated, `${ruleName} metadata should not mark it as deprecated`);
}
});
});
});
describe('configurations', () => {
it('should export a ‘recommended’ configuration', () => {
const configName = 'recommended';
assert(plugin.configs[configName]);
Object.keys(plugin.configs[configName].rules).forEach((ruleName) => {
assert.ok(ruleName.startsWith('react/'));
const subRuleName = ruleName.slice('react/'.length);
assert(plugin.rules[subRuleName]);
});
ruleFiles.forEach((ruleName) => {
const inRecommendedConfig = !!plugin.configs[configName].rules[`react/${ruleName}`];
const isRecommended = plugin.rules[ruleName].meta.docs[configName];
if (inRecommendedConfig) {
assert(isRecommended, `${ruleName} metadata should mark it as recommended`);
} else {
assert(!isRecommended, `${ruleName} metadata should not mark it as recommended`);
}
});
});
it('should export an ‘all’ configuration', () => {
const configName = 'all';
assert(plugin.configs[configName]);
Object.keys(plugin.configs[configName].rules).forEach((ruleName) => {
assert.ok(ruleName.startsWith('react/'));
assert.equal(plugin.configs[configName].rules[ruleName], 2);
});
ruleFiles.forEach((ruleName) => {
const inDeprecatedRules = Boolean(plugin.deprecatedRules[ruleName]);
const inConfig = typeof plugin.configs[configName].rules[`react/${ruleName}`] !== 'undefined';
assert(inDeprecatedRules ^ inConfig); // eslint-disable-line no-bitwise
});
});
it('should export a \'jsx-runtime\' configuration', () => {
const configName = 'jsx-runtime';
assert(plugin.configs[configName]);
Object.keys(plugin.configs[configName].rules).forEach((ruleName) => {
assert.ok(ruleName.startsWith('react/'));
assert.equal(plugin.configs[configName].rules[ruleName], 0);
const inDeprecatedRules = Boolean(plugin.deprecatedRules[ruleName]);
const inConfig = typeof plugin.configs[configName].rules[ruleName] !== 'undefined';
assert(inDeprecatedRules ^ inConfig); // eslint-disable-line no-bitwise
});
});
});