-
-
Notifications
You must be signed in to change notification settings - Fork 381
/
package.mjs
212 lines (181 loc) · 7.27 KB
/
package.mjs
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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
import fs, {promises as fsAsync} from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import test from 'ava';
import eslintExperimentalApis from 'eslint/use-at-your-own-risk';
import * as eslintrc from '@eslint/eslintrc';
import globals from 'globals';
import eslintPluginUnicorn from '../index.js';
const {FlatESLint} = eslintExperimentalApis;
let ruleFiles;
test.before(async () => {
const files = await fsAsync.readdir('rules');
ruleFiles = files.filter(file => path.extname(file) === '.js');
});
const ignoredRules = [
'no-nested-ternary',
'no-negated-condition',
];
const deprecatedRules = Object.entries(eslintPluginUnicorn.rules)
.filter(([, {meta: {deprecated}}]) => deprecated)
.map(([ruleId]) => ruleId);
const RULES_WITHOUT_PASS_FAIL_SECTIONS = new Set([
// Doesn't show code samples since it's just focused on filenames.
'filename-case',
// Intended to not use `pass`/`fail` section in this rule.
'prefer-modern-math-apis',
'prefer-math-min-max',
'consistent-existence-index-check',
'prefer-global-this',
]);
test('Every rule is defined in index file in alphabetical order', t => {
for (const file of ruleFiles) {
const name = path.basename(file, '.js');
t.truthy(eslintPluginUnicorn.rules[name], `'${name}' is not exported in 'index.js'`);
if (!deprecatedRules.includes(name)) {
t.truthy(
eslintPluginUnicorn.configs.recommended.rules[`unicorn/${name}`],
`'${name}' is not set in the recommended config`,
);
}
t.truthy(fs.existsSync(path.join('docs/rules', `${name}.md`)), `There is no documentation for '${name}'`);
t.truthy(fs.existsSync(path.join('test', file.replace(/\.js$/, '.mjs'))), `There are no tests for '${name}'`);
}
t.is(
Object.keys(eslintPluginUnicorn.rules).length - deprecatedRules.length,
ruleFiles.length,
'There are more exported rules than rule files.',
);
t.is(
Object.keys(eslintPluginUnicorn.configs.recommended.rules).length - deprecatedRules.length - ignoredRules.length,
ruleFiles.length - deprecatedRules.length,
'There are more exported rules in the recommended config than rule files.',
);
t.is(
Object.keys(eslintPluginUnicorn.configs.all.rules).length - deprecatedRules.length - ignoredRules.length,
ruleFiles.length - deprecatedRules.length,
'There are more rules than those exported in the all config.',
);
});
test('validate configuration', async t => {
const results = await Promise.all(
Object.entries(eslintPluginUnicorn.configs).filter(([name]) => name.startsWith('flat/')).map(async ([name, config]) => {
const eslint = new FlatESLint({
baseConfig: config,
overrideConfigFile: true,
});
const result = await eslint.calculateConfigForFile('dummy.js');
return {name, config, result};
}),
);
for (const {name, config, result} of results) {
t.deepEqual(
Object.keys(result.rules),
Object.keys(config.rules),
`Configuration for "${name}" is invalid.`,
);
}
});
test('Every rule has valid meta.type', t => {
const validTypes = ['problem', 'suggestion', 'layout'];
for (const file of ruleFiles) {
const name = path.basename(file, '.js');
const rule = eslintPluginUnicorn.rules[name];
t.true(rule.meta !== null && rule.meta !== undefined, `${name} has no meta`);
t.is(typeof rule.meta.type, 'string', `${name} meta.type is not string`);
t.true(validTypes.includes(rule.meta.type), `${name} meta.type is not one of [${validTypes.join(', ')}]`);
}
});
test('Every deprecated rules listed in docs/deprecated-rules.md', async t => {
const content = await fsAsync.readFile('docs/deprecated-rules.md', 'utf8');
const rulesInMarkdown = content.match(/(?<=^## ).*?$/gm);
t.deepEqual(deprecatedRules, rulesInMarkdown);
for (const name of deprecatedRules) {
const rule = eslintPluginUnicorn.rules[name];
t.is(typeof rule.create, 'function', `${name} create is not function`);
t.deepEqual(rule.create(), {}, `${name} create should return empty object`);
t.true(rule.meta.deprecated, `${name} meta.deprecated should be true`);
}
});
test('Every rule file has the appropriate contents', t => {
for (const ruleFile of ruleFiles) {
const ruleName = path.basename(ruleFile, '.js');
const rulePath = path.join('rules', `${ruleName}.js`);
const ruleContents = fs.readFileSync(rulePath, 'utf8');
t.true(ruleContents.includes('/** @type {import(\'eslint\').Rule.RuleModule} */'), `${ruleName} includes jsdoc comment for rule type`);
}
});
test('Every rule has a doc with the appropriate content', t => {
for (const ruleFile of ruleFiles) {
const ruleName = path.basename(ruleFile, '.js');
const documentPath = path.join('docs/rules', `${ruleName}.md`);
const documentContents = fs.readFileSync(documentPath, 'utf8');
// Check for examples.
if (!RULES_WITHOUT_PASS_FAIL_SECTIONS.has(ruleName)) {
t.true(documentContents.includes('## Pass'), `${ruleName} includes '## Pass' examples section`);
t.true(documentContents.includes('## Fail'), `${ruleName} includes '## Fail' examples section`);
}
}
});
test('Plugin should have metadata', t => {
t.is(typeof eslintPluginUnicorn.meta.name, 'string');
t.is(typeof eslintPluginUnicorn.meta.version, 'string');
});
function getCompactConfig(config) {
const compat = new eslintrc.FlatCompat({
baseDirectory: process.cwd(),
resolvePluginsRelativeTo: process.cwd(),
});
const result = {plugins: undefined};
for (const part of compat.config(config)) {
for (const [key, value] of Object.entries(part)) {
if (key === 'languageOptions') {
const languageOptions = {...result[key], ...value};
// ESLint uses same `ecmaVersion` and `sourceType` as we recommended in the new configuration system
// https://eslint.org/docs/latest/use/configure/configuration-files-new#configuration-objects
delete languageOptions.ecmaVersion;
delete languageOptions.sourceType;
languageOptions.globals = {
...languageOptions.globals,
// When use `env.es*: true` in legacy config, `es5` globals are not included
...globals.es5,
// `Intl` was added to ESLint https://github.com/eslint/eslint/pull/18318
// But `@eslint/eslintrc` choose not to update `globals` https://github.com/eslint/eslintrc/pull/164
Intl: false,
Iterator: false,
};
result[key] = languageOptions;
} else if (key === 'plugins') {
result[key] = undefined;
} else {
result[key] = value;
}
}
}
return result;
}
test('flat configs', t => {
t.deepEqual(
{...getCompactConfig(eslintPluginUnicorn.configs.recommended), name: 'unicorn/flat/recommended'},
{...eslintPluginUnicorn.configs['flat/recommended'], plugins: undefined},
);
t.deepEqual(
{...getCompactConfig(eslintPluginUnicorn.configs.all), name: 'unicorn/flat/all'},
{...eslintPluginUnicorn.configs['flat/all'], plugins: undefined},
);
});
test('rule.meta.docs.recommended should be synchronized with presets', t => {
for (const [name, rule] of Object.entries(eslintPluginUnicorn.rules)) {
if (deprecatedRules.includes(name)) {
continue;
}
const {recommended} = rule.meta.docs;
t.is(typeof recommended, 'boolean', `meta.docs.recommended in '${name}' rule should be a boolean.`);
const severity = eslintPluginUnicorn.configs.recommended.rules[`unicorn/${name}`];
if (recommended) {
t.is(severity, 'error', `'${name}' rule should set to 'error'.`);
} else {
t.is(severity, 'off', `'${name}' rule should set to 'off'.`);
}
}
});