xo 1.2.3 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +7 -2
- package/dist/lib/config.js +57 -41
- package/dist/lib/handle-ts-files.d.ts +11 -8
- package/dist/lib/handle-ts-files.js +88 -14
- package/dist/lib/open-report.js +2 -2
- package/dist/lib/resolve-config.js +2 -3
- package/dist/lib/rules/no-use-extend-native.d.ts +3 -0
- package/dist/lib/rules/no-use-extend-native.js +386 -0
- package/dist/lib/types.d.ts +3 -1
- package/dist/lib/utils.d.ts +3 -3
- package/dist/lib/utils.js +30 -15
- package/dist/lib/xo-to-eslint.js +59 -4
- package/dist/lib/xo.d.ts +25 -1
- package/dist/lib/xo.js +201 -26
- package/package.json +42 -37
- package/readme.md +72 -5
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
const isPropertyContainer = (value) => typeof value === 'function' || (typeof value === 'object' && value !== null);
|
|
2
|
+
const isIdentifierNode = (node) => node.type === 'Identifier' && 'name' in node && typeof node.name === 'string';
|
|
3
|
+
const isLiteralNode = (node) => node.type === 'Literal';
|
|
4
|
+
const isMemberExpressionNode = (node) => node.type === 'MemberExpression'
|
|
5
|
+
&& 'computed' in node
|
|
6
|
+
&& typeof node.computed === 'boolean'
|
|
7
|
+
&& 'object' in node
|
|
8
|
+
&& 'property' in node;
|
|
9
|
+
const isBinaryExpressionNode = (node) => node.type === 'BinaryExpression'
|
|
10
|
+
&& 'operator' in node
|
|
11
|
+
&& typeof node.operator === 'string'
|
|
12
|
+
&& 'left' in node
|
|
13
|
+
&& 'right' in node;
|
|
14
|
+
const isNewExpressionNode = (node) => node.type === 'NewExpression' && 'callee' in node;
|
|
15
|
+
const createPropertyInfo = (value, extraProperties = []) => {
|
|
16
|
+
const all = new Set();
|
|
17
|
+
const callable = new Set();
|
|
18
|
+
for (const propertyName of extraProperties) {
|
|
19
|
+
all.add(propertyName);
|
|
20
|
+
}
|
|
21
|
+
for (let currentValue = value; isPropertyContainer(currentValue); currentValue = Object.getPrototypeOf(currentValue)) {
|
|
22
|
+
for (const propertyName of Object.getOwnPropertyNames(currentValue)) {
|
|
23
|
+
all.add(propertyName);
|
|
24
|
+
const descriptor = Object.getOwnPropertyDescriptor(currentValue, propertyName);
|
|
25
|
+
if (typeof descriptor?.value === 'function') {
|
|
26
|
+
callable.add(propertyName);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
all,
|
|
32
|
+
callable,
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
const emptyFunction = () => undefined;
|
|
36
|
+
// Keep the checked native object list explicit so rule behavior stays predictable.
|
|
37
|
+
const nativeObjectDefinitions = [
|
|
38
|
+
{
|
|
39
|
+
typeName: 'Array',
|
|
40
|
+
instance: [],
|
|
41
|
+
static: Array,
|
|
42
|
+
prototype: Array.prototype,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
typeName: 'ArrayBuffer',
|
|
46
|
+
instance: new ArrayBuffer(0),
|
|
47
|
+
static: ArrayBuffer,
|
|
48
|
+
prototype: ArrayBuffer.prototype,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
typeName: 'Boolean',
|
|
52
|
+
instance: Boolean.prototype,
|
|
53
|
+
static: Boolean,
|
|
54
|
+
prototype: Boolean.prototype,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
typeName: 'DataView',
|
|
58
|
+
instance: new DataView(new ArrayBuffer(1)),
|
|
59
|
+
static: DataView,
|
|
60
|
+
prototype: DataView.prototype,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
typeName: 'Date',
|
|
64
|
+
instance: new Date(),
|
|
65
|
+
static: Date,
|
|
66
|
+
prototype: Date.prototype,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
typeName: 'Float32Array',
|
|
70
|
+
instance: new Float32Array(),
|
|
71
|
+
static: Float32Array,
|
|
72
|
+
prototype: Float32Array.prototype,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
typeName: 'Float64Array',
|
|
76
|
+
instance: new Float64Array(),
|
|
77
|
+
static: Float64Array,
|
|
78
|
+
prototype: Float64Array.prototype,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
typeName: 'Function',
|
|
82
|
+
instance: emptyFunction,
|
|
83
|
+
static: Function,
|
|
84
|
+
prototype: Function.prototype,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
typeName: 'Int8Array',
|
|
88
|
+
instance: new Int8Array(),
|
|
89
|
+
static: Int8Array,
|
|
90
|
+
prototype: Int8Array.prototype,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
typeName: 'Int16Array',
|
|
94
|
+
instance: new Int16Array(),
|
|
95
|
+
static: Int16Array,
|
|
96
|
+
prototype: Int16Array.prototype,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
typeName: 'Int32Array',
|
|
100
|
+
instance: new Int32Array(),
|
|
101
|
+
static: Int32Array,
|
|
102
|
+
prototype: Int32Array.prototype,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
typeName: 'Map',
|
|
106
|
+
instance: new Map(),
|
|
107
|
+
static: Map,
|
|
108
|
+
prototype: Map.prototype,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
typeName: 'Number',
|
|
112
|
+
instance: Number.prototype,
|
|
113
|
+
static: Number,
|
|
114
|
+
prototype: Number.prototype,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
typeName: 'Object',
|
|
118
|
+
instance: {},
|
|
119
|
+
static: Object,
|
|
120
|
+
prototype: Object.prototype,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
typeName: 'Promise',
|
|
124
|
+
instance: Promise.resolve(),
|
|
125
|
+
static: Promise,
|
|
126
|
+
prototype: Promise.prototype,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
typeName: 'RegExp',
|
|
130
|
+
instance: /./u,
|
|
131
|
+
static: RegExp,
|
|
132
|
+
prototype: RegExp.prototype,
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
typeName: 'Set',
|
|
136
|
+
instance: new Set(),
|
|
137
|
+
static: Set,
|
|
138
|
+
prototype: Set.prototype,
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
typeName: 'String',
|
|
142
|
+
instance: String.prototype,
|
|
143
|
+
instanceProperties: ['length'],
|
|
144
|
+
static: String,
|
|
145
|
+
prototype: String.prototype,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
typeName: 'Uint8Array',
|
|
149
|
+
instance: new Uint8Array(),
|
|
150
|
+
static: Uint8Array,
|
|
151
|
+
prototype: Uint8Array.prototype,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
typeName: 'Uint8ClampedArray',
|
|
155
|
+
instance: new Uint8ClampedArray(),
|
|
156
|
+
static: Uint8ClampedArray,
|
|
157
|
+
prototype: Uint8ClampedArray.prototype,
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
typeName: 'Uint16Array',
|
|
161
|
+
instance: new Uint16Array(),
|
|
162
|
+
static: Uint16Array,
|
|
163
|
+
prototype: Uint16Array.prototype,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
typeName: 'Uint32Array',
|
|
167
|
+
instance: new Uint32Array(),
|
|
168
|
+
static: Uint32Array,
|
|
169
|
+
prototype: Uint32Array.prototype,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
typeName: 'URL',
|
|
173
|
+
instance: new URL('https://example.com'),
|
|
174
|
+
static: URL,
|
|
175
|
+
prototype: URL.prototype,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
typeName: 'URLSearchParams',
|
|
179
|
+
instance: new URLSearchParams(),
|
|
180
|
+
static: URLSearchParams,
|
|
181
|
+
prototype: URLSearchParams.prototype,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
typeName: 'JSON',
|
|
185
|
+
static: JSON,
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
typeName: 'Math',
|
|
189
|
+
static: Math,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
typeName: 'Reflect',
|
|
193
|
+
static: Reflect,
|
|
194
|
+
},
|
|
195
|
+
];
|
|
196
|
+
const nativeObjects = new Map();
|
|
197
|
+
for (const nativeObjectDefinition of nativeObjectDefinitions) {
|
|
198
|
+
const nativeObjectInfo = {};
|
|
199
|
+
if (nativeObjectDefinition.instance) {
|
|
200
|
+
nativeObjectInfo.instance = createPropertyInfo(nativeObjectDefinition.instance, nativeObjectDefinition.instanceProperties);
|
|
201
|
+
}
|
|
202
|
+
if (nativeObjectDefinition.prototype) {
|
|
203
|
+
nativeObjectInfo.prototype = createPropertyInfo(nativeObjectDefinition.prototype);
|
|
204
|
+
}
|
|
205
|
+
if (nativeObjectDefinition.static) {
|
|
206
|
+
nativeObjectInfo.static = createPropertyInfo(nativeObjectDefinition.static);
|
|
207
|
+
}
|
|
208
|
+
nativeObjects.set(nativeObjectDefinition.typeName, nativeObjectInfo);
|
|
209
|
+
}
|
|
210
|
+
const getPropertyName = (memberExpression) => {
|
|
211
|
+
const { property } = memberExpression;
|
|
212
|
+
if (memberExpression.computed) {
|
|
213
|
+
return isLiteralNode(property) && typeof property.value === 'string' ? property.value : undefined;
|
|
214
|
+
}
|
|
215
|
+
return isIdentifierNode(property) ? property.name : undefined;
|
|
216
|
+
};
|
|
217
|
+
const resolveBinaryExpressionType = (binaryExpression) => {
|
|
218
|
+
if (binaryExpression.operator !== '+') {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
const leftReference = resolveNativeObjectReference(binaryExpression.left);
|
|
222
|
+
const rightReference = resolveNativeObjectReference(binaryExpression.right);
|
|
223
|
+
if (leftReference?.usage !== 'instance' || rightReference?.usage !== 'instance') {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
if (leftReference.typeName === 'String' || rightReference.typeName === 'String') {
|
|
227
|
+
return {
|
|
228
|
+
typeName: 'String',
|
|
229
|
+
usage: 'instance',
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return undefined;
|
|
233
|
+
};
|
|
234
|
+
const resolveIdentifierReference = (node) => {
|
|
235
|
+
if (!nativeObjects.has(node.name)) {
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
typeName: node.name,
|
|
240
|
+
usage: 'static',
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
const resolveLiteralReference = (node) => {
|
|
244
|
+
if (node.regex) {
|
|
245
|
+
return {
|
|
246
|
+
typeName: 'RegExp',
|
|
247
|
+
usage: 'instance',
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
if (typeof node.value === 'boolean') {
|
|
251
|
+
return {
|
|
252
|
+
typeName: 'Boolean',
|
|
253
|
+
usage: 'instance',
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (typeof node.value === 'number') {
|
|
257
|
+
return {
|
|
258
|
+
typeName: 'Number',
|
|
259
|
+
usage: 'instance',
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
if (typeof node.value === 'string') {
|
|
263
|
+
return {
|
|
264
|
+
typeName: 'String',
|
|
265
|
+
usage: 'instance',
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return undefined;
|
|
269
|
+
};
|
|
270
|
+
const resolvePrototypeReference = (node) => {
|
|
271
|
+
if (getPropertyName(node) !== 'prototype' || !isIdentifierNode(node.object) || !nativeObjects.has(node.object.name)) {
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
typeName: node.object.name,
|
|
276
|
+
usage: 'prototype',
|
|
277
|
+
};
|
|
278
|
+
};
|
|
279
|
+
const resolveNewExpressionReference = (node) => {
|
|
280
|
+
if (!isIdentifierNode(node.callee) || !nativeObjects.has(node.callee.name)) {
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
typeName: node.callee.name,
|
|
285
|
+
usage: 'instance',
|
|
286
|
+
};
|
|
287
|
+
};
|
|
288
|
+
function resolveNativeObjectReference(node) {
|
|
289
|
+
if (!node) {
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
if (isMemberExpressionNode(node)) {
|
|
293
|
+
return resolvePrototypeReference(node);
|
|
294
|
+
}
|
|
295
|
+
if (isBinaryExpressionNode(node)) {
|
|
296
|
+
return resolveBinaryExpressionType(node);
|
|
297
|
+
}
|
|
298
|
+
if (isLiteralNode(node)) {
|
|
299
|
+
return resolveLiteralReference(node);
|
|
300
|
+
}
|
|
301
|
+
if (isIdentifierNode(node)) {
|
|
302
|
+
return resolveIdentifierReference(node);
|
|
303
|
+
}
|
|
304
|
+
if (isNewExpressionNode(node)) {
|
|
305
|
+
return resolveNewExpressionReference(node);
|
|
306
|
+
}
|
|
307
|
+
switch (node.type) {
|
|
308
|
+
case 'ArrayExpression': {
|
|
309
|
+
return {
|
|
310
|
+
typeName: 'Array',
|
|
311
|
+
usage: 'instance',
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
case 'ArrowFunctionExpression':
|
|
315
|
+
case 'FunctionExpression': {
|
|
316
|
+
return {
|
|
317
|
+
typeName: 'Function',
|
|
318
|
+
usage: 'instance',
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
case 'ObjectExpression': {
|
|
322
|
+
return {
|
|
323
|
+
typeName: 'Object',
|
|
324
|
+
usage: 'instance',
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
case 'TemplateLiteral': {
|
|
328
|
+
return {
|
|
329
|
+
typeName: 'String',
|
|
330
|
+
usage: 'instance',
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
default: {
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const noUseExtendNativeRule = {
|
|
339
|
+
meta: {
|
|
340
|
+
type: 'problem',
|
|
341
|
+
docs: {
|
|
342
|
+
description: 'Disallow relying on non-standard properties on native objects',
|
|
343
|
+
},
|
|
344
|
+
messages: {
|
|
345
|
+
unexpected: 'Avoid relying on extended native objects.',
|
|
346
|
+
},
|
|
347
|
+
schema: [],
|
|
348
|
+
},
|
|
349
|
+
create(context) {
|
|
350
|
+
return {
|
|
351
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
352
|
+
MemberExpression(node) {
|
|
353
|
+
const propertyName = getPropertyName(node);
|
|
354
|
+
if (!propertyName) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const nativeObjectReference = resolveNativeObjectReference(node.object);
|
|
358
|
+
if (!nativeObjectReference) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const propertyInfo = nativeObjects.get(nativeObjectReference.typeName)?.[nativeObjectReference.usage];
|
|
362
|
+
if (!propertyInfo) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const isCall = node.parent.type === 'CallExpression' && node.parent.callee === node;
|
|
366
|
+
if (isCall) {
|
|
367
|
+
if (!propertyInfo.callable.has(propertyName)) {
|
|
368
|
+
context.report({
|
|
369
|
+
node,
|
|
370
|
+
messageId: 'unexpected',
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (!propertyInfo.all.has(propertyName)) {
|
|
376
|
+
context.report({
|
|
377
|
+
node,
|
|
378
|
+
messageId: 'unexpected',
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
export default noUseExtendNativeRule;
|
|
386
|
+
//# sourceMappingURL=no-use-extend-native.js.map
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -71,9 +71,11 @@ export type XoConfigItem = Simplify<XoConfigOptions & Omit<Linter.Config, 'files
|
|
|
71
71
|
/**
|
|
72
72
|
An array of glob patterns indicating the files that the configuration object should apply to. If not specified, the configuration object applies to all files.
|
|
73
73
|
|
|
74
|
+
Accepts a single glob string, an array of globs, or ESLint's native format where nested arrays create AND patterns.
|
|
75
|
+
|
|
74
76
|
@see [Ignore Patterns](https://eslint.org/docs/latest/user-guide/configuring/configuration-files-new#excluding-files-with-ignores)
|
|
75
77
|
*/
|
|
76
|
-
files?: string | string[] | undefined;
|
|
78
|
+
files?: string | Array<string | string[]> | undefined;
|
|
77
79
|
/**
|
|
78
80
|
An array of glob patterns indicating the files that the configuration object should not apply to. If not specified, the configuration object applies to all files matched by files.
|
|
79
81
|
|
package/dist/lib/utils.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Linter } from 'eslint';
|
|
2
|
-
import { type SetRequired } from 'type-fest';
|
|
3
2
|
import { type XoConfigItem } from './types.js';
|
|
3
|
+
export declare const typescriptParser: Linter.Parser | undefined;
|
|
4
4
|
/**
|
|
5
5
|
Convert a `xo` config item to an ESLint config item.
|
|
6
6
|
|
|
@@ -11,7 +11,7 @@ Files and rules will always be defined and all other ESLint config properties ar
|
|
|
11
11
|
@param xoConfig
|
|
12
12
|
@returns eslintConfig
|
|
13
13
|
*/
|
|
14
|
-
export declare const xoToEslintConfigItem: (xoConfig: XoConfigItem) =>
|
|
14
|
+
export declare const xoToEslintConfigItem: (xoConfig: XoConfigItem) => Linter.Config;
|
|
15
15
|
/**
|
|
16
16
|
Function used to match files which should be included in the `tsconfig.json` files.
|
|
17
17
|
|
|
@@ -21,7 +21,7 @@ Function used to match files which should be included in the `tsconfig.json` fil
|
|
|
21
21
|
@param ignores - The globs to ignore when matching the files.
|
|
22
22
|
@returns An array of file paths that match the globs and do not match the ignores.
|
|
23
23
|
*/
|
|
24
|
-
export declare const matchFilesForTsConfig: (cwd: string, files: string[], globs: string[], ignores: string[]) => string[];
|
|
24
|
+
export declare const matchFilesForTsConfig: (cwd: string, files: string[] | undefined, globs: string[], ignores: string[]) => string[];
|
|
25
25
|
/**
|
|
26
26
|
Once a config is resolved, it is pre-processed to ensure that all properties are set correctly.
|
|
27
27
|
|
package/dist/lib/utils.js
CHANGED
|
@@ -3,6 +3,14 @@ import micromatch from 'micromatch';
|
|
|
3
3
|
import arrify from 'arrify';
|
|
4
4
|
import configXoTypescript from 'eslint-config-xo-typescript';
|
|
5
5
|
import { allFilesGlob, jsExtensions, jsFilesGlob, } from './constants.js';
|
|
6
|
+
const typescriptParserConfig = configXoTypescript.find(config => {
|
|
7
|
+
const languageOptions = config.languageOptions;
|
|
8
|
+
return languageOptions?.parser;
|
|
9
|
+
});
|
|
10
|
+
export const typescriptParser = typescriptParserConfig?.languageOptions?.parser;
|
|
11
|
+
if (!typescriptParser) {
|
|
12
|
+
throw new Error('XO: Failed to locate TypeScript parser in eslint-config-xo-typescript');
|
|
13
|
+
}
|
|
6
14
|
/**
|
|
7
15
|
Convert a `xo` config item to an ESLint config item.
|
|
8
16
|
|
|
@@ -17,8 +25,8 @@ export const xoToEslintConfigItem = (xoConfig) => {
|
|
|
17
25
|
const { files, rules, space, prettier, ignores, semicolon, react, ..._xoConfig } = xoConfig;
|
|
18
26
|
const eslintConfig = {
|
|
19
27
|
..._xoConfig,
|
|
20
|
-
files: arrify(xoConfig.files
|
|
21
|
-
rules: xoConfig.rules
|
|
28
|
+
...(xoConfig.files ? { files: arrify(xoConfig.files) } : {}),
|
|
29
|
+
...(xoConfig.rules ? { rules: xoConfig.rules } : {}),
|
|
22
30
|
};
|
|
23
31
|
eslintConfig.ignores &&= arrify(xoConfig.ignores);
|
|
24
32
|
return eslintConfig;
|
|
@@ -32,7 +40,7 @@ Function used to match files which should be included in the `tsconfig.json` fil
|
|
|
32
40
|
@param ignores - The globs to ignore when matching the files.
|
|
33
41
|
@returns An array of file paths that match the globs and do not match the ignores.
|
|
34
42
|
*/
|
|
35
|
-
export const matchFilesForTsConfig = (cwd, files, globs, ignores) => micromatch(files
|
|
43
|
+
export const matchFilesForTsConfig = (cwd, files, globs, ignores) => micromatch(files?.map(file => path.normalize(path.relative(cwd, file))) ?? [],
|
|
36
44
|
// https://github.com/micromatch/micromatch/issues/217
|
|
37
45
|
globs.map(glob => path.normalize(glob)), {
|
|
38
46
|
dot: true,
|
|
@@ -47,11 +55,14 @@ This includes ensuring that user-defined properties can override XO defaults, an
|
|
|
47
55
|
@param xoConfig - The flat XO config to pre-process.
|
|
48
56
|
@returns The pre-processed flat XO config.
|
|
49
57
|
*/
|
|
58
|
+
// eslint-disable-next-line complexity
|
|
50
59
|
export const preProcessXoConfig = (xoConfig) => {
|
|
51
60
|
const tsFilesGlob = [];
|
|
52
61
|
const tsFilesIgnoresGlob = [];
|
|
53
62
|
const processedConfig = [];
|
|
54
63
|
for (const [idx, { ...config }] of xoConfig.entries()) {
|
|
64
|
+
const languageOptions = config.languageOptions;
|
|
65
|
+
const parserOptions = languageOptions?.parserOptions;
|
|
55
66
|
// We can skip the first config item, as it is the base config item.
|
|
56
67
|
if (idx === 0) {
|
|
57
68
|
processedConfig.push(config);
|
|
@@ -60,8 +71,10 @@ export const preProcessXoConfig = (xoConfig) => {
|
|
|
60
71
|
// Use TS parser/plugin for JS files if the config contains TypeScript rules which are applied to JS files.
|
|
61
72
|
// typescript-eslint rules set to "off" are ignored and not applied to JS files.
|
|
62
73
|
if (config.rules
|
|
63
|
-
|
|
64
|
-
&& !
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/dot-notation
|
|
75
|
+
&& !languageOptions?.['parser']
|
|
76
|
+
&& parserOptions?.project === undefined
|
|
77
|
+
&& parserOptions?.programs === undefined
|
|
65
78
|
&& !config.plugins?.['@typescript-eslint']) {
|
|
66
79
|
const hasTsRules = Object.entries(config.rules).some(rulePair => {
|
|
67
80
|
// If its not a @typescript-eslint rule, we don't care
|
|
@@ -77,7 +90,7 @@ export const preProcessXoConfig = (xoConfig) => {
|
|
|
77
90
|
if (hasTsRules) {
|
|
78
91
|
let isAppliedToJsFiles = false;
|
|
79
92
|
if (config.files) {
|
|
80
|
-
const normalizedFiles = arrify(config.files).map(file => path.normalize(file));
|
|
93
|
+
const normalizedFiles = arrify(config.files).flat().map(file => path.normalize(file));
|
|
81
94
|
// Strip the basename off any globs
|
|
82
95
|
const globs = normalizedFiles.map(file => micromatch.scan(file, { dot: true }).glob).filter(Boolean);
|
|
83
96
|
// Check if the files globs match a test file with a js extension
|
|
@@ -89,25 +102,27 @@ export const preProcessXoConfig = (xoConfig) => {
|
|
|
89
102
|
isAppliedToJsFiles = true;
|
|
90
103
|
}
|
|
91
104
|
if (isAppliedToJsFiles) {
|
|
92
|
-
|
|
105
|
+
const updatedLanguageOptions = languageOptions
|
|
106
|
+
? { ...languageOptions, parser: typescriptParser }
|
|
107
|
+
: { parser: typescriptParser };
|
|
108
|
+
config.languageOptions = updatedLanguageOptions;
|
|
93
109
|
config.plugins ??= {};
|
|
94
110
|
config.plugins = {
|
|
95
111
|
...config.plugins,
|
|
96
112
|
...configXoTypescript[1]?.plugins,
|
|
97
113
|
};
|
|
98
|
-
config.
|
|
99
|
-
tsFilesGlob.push(...arrify(config.files ?? allFilesGlob));
|
|
114
|
+
tsFilesGlob.push(...arrify(config.files ?? allFilesGlob).flat());
|
|
100
115
|
tsFilesIgnoresGlob.push(...arrify(config.ignores));
|
|
101
116
|
}
|
|
102
117
|
}
|
|
103
118
|
}
|
|
104
|
-
// If
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
||
|
|
108
|
-
||
|
|
119
|
+
// If the config sets `parserOptions.project`, `projectService`, `tsconfigRootDir`, or `programs`, treat those files as opt-out for XO's automatic program wiring.
|
|
120
|
+
if (parserOptions?.project !== undefined
|
|
121
|
+
|| parserOptions?.projectService !== undefined
|
|
122
|
+
|| parserOptions?.tsconfigRootDir !== undefined
|
|
123
|
+
|| parserOptions?.programs !== undefined) {
|
|
109
124
|
// The glob itself should NOT be negated
|
|
110
|
-
tsFilesIgnoresGlob.push(...arrify(config.files ?? allFilesGlob));
|
|
125
|
+
tsFilesIgnoresGlob.push(...arrify(config.files ?? allFilesGlob).flat());
|
|
111
126
|
}
|
|
112
127
|
processedConfig.push(config);
|
|
113
128
|
}
|
package/dist/lib/xo-to-eslint.js
CHANGED
|
@@ -4,13 +4,57 @@ import arrify from 'arrify';
|
|
|
4
4
|
import configReact from 'eslint-config-xo-react';
|
|
5
5
|
import pluginPrettier from 'eslint-plugin-prettier';
|
|
6
6
|
import eslintConfigPrettier from 'eslint-config-prettier';
|
|
7
|
+
import { fixupConfigRules, fixupPluginRules } from '@eslint/compat';
|
|
7
8
|
import { config } from './config.js';
|
|
8
9
|
import { xoToEslintConfigItem } from './utils.js';
|
|
9
10
|
/**
|
|
11
|
+
Merge all plugins from every config into a single config entry at the start of the array, ensuring user-provided plugins take precedence. This avoids ESLint's flat config rejecting duplicate plugin names.
|
|
12
|
+
*/
|
|
13
|
+
const hoistPlugins = (configs, userPluginOverrides) => {
|
|
14
|
+
const plugins = {};
|
|
15
|
+
const configsWithoutPlugins = [];
|
|
16
|
+
for (const configItem of configs) {
|
|
17
|
+
const { plugins: configPlugins } = configItem;
|
|
18
|
+
if (!configPlugins) {
|
|
19
|
+
configsWithoutPlugins.push(configItem);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
// ESLint flat config rejects duplicate plugin names, so merge all plugins into one config.
|
|
23
|
+
Object.assign(plugins, configPlugins);
|
|
24
|
+
const { plugins: _ignored, ...configWithoutPlugins } = configItem;
|
|
25
|
+
if (Object.keys(configWithoutPlugins).length > 0) {
|
|
26
|
+
configsWithoutPlugins.push(configWithoutPlugins);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
for (const [pluginName, plugin] of userPluginOverrides) {
|
|
30
|
+
plugins[pluginName] = plugin;
|
|
31
|
+
}
|
|
32
|
+
if (Object.keys(plugins).length === 0) {
|
|
33
|
+
return configsWithoutPlugins;
|
|
34
|
+
}
|
|
35
|
+
return [
|
|
36
|
+
{
|
|
37
|
+
name: 'xo/plugins',
|
|
38
|
+
plugins,
|
|
39
|
+
},
|
|
40
|
+
...configsWithoutPlugins,
|
|
41
|
+
];
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
10
44
|
Takes a XO flat config and returns an ESlint flat config.
|
|
11
45
|
*/
|
|
12
46
|
export function xoToEslintConfig(flatXoConfig, { prettierOptions = {} } = {}) {
|
|
13
47
|
const baseConfig = [...config];
|
|
48
|
+
const userPluginOverrides = new Map();
|
|
49
|
+
for (const xoConfigItem of flatXoConfig ?? []) {
|
|
50
|
+
const { plugins } = xoConfigItem;
|
|
51
|
+
if (!plugins) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
for (const [pluginName, plugin] of Object.entries(plugins)) {
|
|
55
|
+
userPluginOverrides.set(pluginName, plugin);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
14
58
|
/**
|
|
15
59
|
Since configs are merged and the last config takes precedence this means we need to handle both true AND false cases for each option. For example, we need to turn `prettier`, `space`, `semi`, etc. on or off for a specific file.
|
|
16
60
|
*/
|
|
@@ -35,6 +79,7 @@ export function xoToEslintConfig(flatXoConfig, { prettierOptions = {} } = {}) {
|
|
|
35
79
|
*/
|
|
36
80
|
const eslintConfigItem = xoToEslintConfigItem(xoConfigItem);
|
|
37
81
|
if (xoConfigItem.semicolon === false) {
|
|
82
|
+
eslintConfigItem.rules ??= {};
|
|
38
83
|
eslintConfigItem.rules['@stylistic/semi'] = ['error', 'never'];
|
|
39
84
|
eslintConfigItem.rules['@stylistic/semi-spacing'] = [
|
|
40
85
|
'error',
|
|
@@ -43,6 +88,7 @@ export function xoToEslintConfig(flatXoConfig, { prettierOptions = {} } = {}) {
|
|
|
43
88
|
}
|
|
44
89
|
if (xoConfigItem.space) {
|
|
45
90
|
const spaces = typeof xoConfigItem.space === 'number' ? xoConfigItem.space : 2;
|
|
91
|
+
eslintConfigItem.rules ??= {};
|
|
46
92
|
eslintConfigItem.rules['@stylistic/indent'] = [
|
|
47
93
|
'error',
|
|
48
94
|
spaces,
|
|
@@ -54,12 +100,14 @@ export function xoToEslintConfig(flatXoConfig, { prettierOptions = {} } = {}) {
|
|
|
54
100
|
else if (xoConfigItem.space === false) {
|
|
55
101
|
// If a user sets this to false for a small subset of files for some reason,
|
|
56
102
|
// then we need to set them back to their original values.
|
|
103
|
+
eslintConfigItem.rules ??= {};
|
|
57
104
|
eslintConfigItem.rules['@stylistic/indent'] = configXoTypescript[1]?.rules?.['@stylistic/indent'];
|
|
58
105
|
eslintConfigItem.rules['@stylistic/indent-binary-ops'] = configXoTypescript[1]?.rules?.['@stylistic/indent-binary-ops'];
|
|
59
106
|
}
|
|
60
107
|
if (xoConfigItem.react) {
|
|
61
108
|
// Ensure the files applied to the React config are the same as the config they are derived from
|
|
62
|
-
|
|
109
|
+
// TODO: Remove `fixupConfigRules` wrapping when eslint-config-xo-react supports ESLint 10 natively.
|
|
110
|
+
baseConfig.push({ ...fixupConfigRules(configReact)[0], files: eslintConfigItem.files, name: 'xo/react' });
|
|
63
111
|
}
|
|
64
112
|
// Prettier should generally be the last config in the array
|
|
65
113
|
if (xoConfigItem.prettier) {
|
|
@@ -68,7 +116,7 @@ export function xoToEslintConfig(flatXoConfig, { prettierOptions = {} } = {}) {
|
|
|
68
116
|
}
|
|
69
117
|
else {
|
|
70
118
|
// Validate that Prettier options match other `xoConfig` options.
|
|
71
|
-
|
|
119
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
|
72
120
|
if ((xoConfigItem.semicolon && prettierOptions.semi === false) || (!xoConfigItem.semicolon && prettierOptions.semi === true)) {
|
|
73
121
|
throw new Error(`The Prettier config \`semi\` is ${prettierOptions.semi} while Xo \`semicolon\` is ${xoConfigItem.semicolon}, also check your .editorconfig for inconsistencies.`);
|
|
74
122
|
}
|
|
@@ -79,9 +127,10 @@ export function xoToEslintConfig(flatXoConfig, { prettierOptions = {} } = {}) {
|
|
|
79
127
|
throw new Error(`The Prettier config \`tabWidth\` is ${prettierOptions.tabWidth} while Xo \`space\` is ${xoConfigItem.space}, also check your .editorconfig for inconsistencies.`);
|
|
80
128
|
}
|
|
81
129
|
// Add Prettier plugin
|
|
130
|
+
// TODO: Remove `fixupPluginRules` wrapping when eslint-plugin-prettier supports ESLint 10 natively.
|
|
82
131
|
eslintConfigItem.plugins = {
|
|
83
132
|
...eslintConfigItem.plugins,
|
|
84
|
-
prettier: pluginPrettier,
|
|
133
|
+
prettier: fixupPluginRules(pluginPrettier),
|
|
85
134
|
};
|
|
86
135
|
const prettierConfig = {
|
|
87
136
|
singleQuote: true,
|
|
@@ -96,6 +145,7 @@ export function xoToEslintConfig(flatXoConfig, { prettierOptions = {} } = {}) {
|
|
|
96
145
|
// Configure Prettier rules
|
|
97
146
|
const rulesWithPrettier = {
|
|
98
147
|
...eslintConfigItem.rules,
|
|
148
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
99
149
|
...pluginPrettier.configs?.['recommended']?.rules,
|
|
100
150
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
101
151
|
'prettier/prettier': ['error', prettierConfig],
|
|
@@ -106,11 +156,16 @@ export function xoToEslintConfig(flatXoConfig, { prettierOptions = {} } = {}) {
|
|
|
106
156
|
}
|
|
107
157
|
else if (xoConfigItem.prettier === false) {
|
|
108
158
|
// Turn Prettier off for a subset of files
|
|
159
|
+
eslintConfigItem.rules ??= {};
|
|
109
160
|
eslintConfigItem.rules['prettier/prettier'] = 'off';
|
|
110
161
|
}
|
|
162
|
+
if (Object.keys(eslintConfigItem).length === 0) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
111
165
|
baseConfig.push(eslintConfigItem);
|
|
112
166
|
}
|
|
113
|
-
|
|
167
|
+
// User plugins should always win, even if XO injects plugins later in the config list.
|
|
168
|
+
return hoistPlugins(baseConfig, userPluginOverrides);
|
|
114
169
|
}
|
|
115
170
|
export default xoToEslintConfig;
|
|
116
171
|
//# sourceMappingURL=xo-to-eslint.js.map
|