Skip to content

Commit e63d7a5

Browse files
authored
Fix: Ensure lint text does not strip rules between runs (#802)
1 parent 27bf5fb commit e63d7a5

File tree

6 files changed

+121
-95
lines changed

6 files changed

+121
-95
lines changed

‎cli.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import formatterPretty from 'eslint-formatter-pretty';
77
import getStdin from 'get-stdin';
88
import meow from 'meow';
99
import {pathExists} from 'path-exists';
10-
import {tsExtensions} from './lib/constants.js';
10+
import findCacheDirectory from 'find-cache-directory';
11+
import {cacheDirName, tsExtensions} from './lib/constants.js';
1112
import type {LinterOptions, XoConfigOptions} from './lib/types.js';
1213
import {Xo} from './lib/xo.js';
1314
import openReport from './lib/open-report.js';
@@ -181,7 +182,8 @@ if (cliOptions.stdin) {
181182
if (cliOptions.stdinFilename && tsExtensions.includes(path.extname(cliOptions.stdinFilename).slice(1))) {
182183
const absoluteFilePath = path.resolve(cliOptions.cwd, cliOptions.stdinFilename);
183184
if (!await pathExists(absoluteFilePath)) {
184-
cliOptions.stdinFilename = path.join(cliOptions.cwd, 'node_modules', '.cache', 'xo-linter', path.basename(absoluteFilePath));
185+
const cacheDir = findCacheDirectory({name: cacheDirName, cwd: linterOptions.cwd}) ?? path.join(cliOptions.cwd, 'node_modules', '.cache', cacheDirName);
186+
cliOptions.stdinFilename = path.join(cacheDir, path.basename(absoluteFilePath));
185187
shouldRemoveStdInFile = true;
186188
baseXoConfigOptions.ignores = [
187189
'!**/node_modules/**',
@@ -216,7 +218,7 @@ if (cliOptions.stdin) {
216218
}
217219

218220
const xo = new Xo(linterOptions, baseXoConfigOptions);
219-
await log(await xo.lintText(stdin, {filePath: cliOptions.stdinFilename, warnIgnored: true}));
221+
await log(await xo.lintText(stdin, {filePath: cliOptions.stdinFilename, warnIgnored: false}));
220222
if (shouldRemoveStdInFile) {
221223
await fs.rm(cliOptions.stdinFilename);
222224
}

‎lib/handle-ts-files.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export async function handleTsconfig({cwd, files}: {cwd: string; files: string[]
4040
// If we match on excluded, then we definitively know that there is no tsconfig match.
4141
if (Array.isArray(tsConfig.exclude)) {
4242
const exclude = Array.isArray(tsConfig.exclude) ? tsConfig.exclude : [];
43-
hasMatch = !micromatch.isMatch(filePath, exclude, micromatchOptions);
43+
hasMatch = !micromatch.contains(filePath, exclude, micromatchOptions);
4444
} else {
4545
// Not explicitly excluded and included by tsconfig defaults
4646
hasMatch = true;
@@ -52,7 +52,7 @@ export async function handleTsconfig({cwd, files}: {cwd: string; files: string[]
5252
const exclude = Array.isArray(tsConfig.exclude) ? tsConfig.exclude : [];
5353
// If we also have an exlcude we need to check all the arrays, (files, include, exclude)
5454
// this check not excluded and included in one of the file/include array
55-
hasMatch = !micromatch.isMatch(filePath, exclude, micromatchOptions) && micromatch.isMatch(filePath, [...include, ...files], micromatchOptions);
55+
hasMatch = !micromatch.contains(filePath, exclude, micromatchOptions) && micromatch.isMatch(filePath, [...include, ...files], micromatchOptions);
5656
}
5757

5858
if (!hasMatch) {

‎lib/xo.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,8 @@ export class Xo {
166166

167167
this.xoConfig = [
168168
this.baseXoConfig,
169-
...flatOptions,
169+
// Ensure resolved options do not mutate between runs
170+
...structuredClone(flatOptions),
170171
];
171172

172173
// Split off the TS rules in a special case, so that you won't get errors

‎scripts/setup-tests.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ await fs.writeFile(
3737
strictNullChecks: true,
3838
lib: ['DOM', 'DOM.Iterable', 'ES2022'],
3939
},
40-
files: [path.join(cwd, 'test.ts')],
4140
exclude: ['node_modules'],
4241
}),
4342
);

‎test/helpers/copy-test-project.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,6 @@ export const copyTestProject = async () => {
1919

2020
await fs.cp(testCwd, newCwd, {recursive: true});
2121

22-
// Create a tsconfig.json file
23-
await fs.writeFile(
24-
path.join(newCwd, 'tsconfig.json'),
25-
JSON.stringify({
26-
compilerOptions: {
27-
module: 'node16',
28-
target: 'ES2022',
29-
strictNullChecks: true,
30-
lib: ['DOM', 'DOM.Iterable', 'ES2022'],
31-
},
32-
files: [path.join(newCwd, 'test.ts')],
33-
exclude: ['node_modules'],
34-
}),
35-
);
36-
3722
return newCwd;
3823
};
3924

‎test/xo/lint-text.test.ts

Lines changed: 112 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ test('no config > js > semi', async t => {
2727
});
2828

2929
test('no config > ts > semi', async t => {
30-
const filePath = path.join(t.context.cwd, 'test.ts');
31-
const {results} = await new Xo({cwd: t.context.cwd}).lintText(
32-
dedent`console.log('hello')\n`,
33-
{filePath},
34-
);
30+
const {cwd} = t.context;
31+
const filePath = path.join(cwd, 'test.ts');
32+
const text = dedent`console.log('hello')\n`;
33+
await fs.writeFile(filePath, text, 'utf8');
34+
const {results} = await new Xo({cwd}).lintText(text, {filePath});
3535

3636
t.is(results?.[0]?.messages?.length, 1);
3737
t.is(results?.[0]?.messages?.[0]?.ruleId, '@stylistic/semi');
@@ -59,9 +59,10 @@ test('flat config > js > semi', async t => {
5959
});
6060

6161
test('flat config > ts > semi', async t => {
62-
const filePath = path.join(t.context.cwd, 'test.ts');
62+
const {cwd} = t.context;
63+
const filePath = path.join(cwd, 'test.ts');
6364
await fs.writeFile(
64-
path.join(t.context.cwd, 'xo.config.js'),
65+
path.join(cwd, 'xo.config.js'),
6566
dedent`
6667
export default [
6768
{
@@ -71,10 +72,10 @@ test('flat config > ts > semi', async t => {
7172
`,
7273
'utf8',
7374
);
74-
const xo = new Xo({cwd: t.context.cwd});
75-
const {results} = await xo.lintText(dedent`console.log('hello');\n`, {
76-
filePath,
77-
});
75+
const text = dedent`console.log('hello');\n`;
76+
await fs.writeFile(filePath, text, 'utf8');
77+
const xo = new Xo({cwd});
78+
const {results} = await xo.lintText(text, {filePath});
7879
t.is(results?.[0]?.messages?.length, 1);
7980
t.is(results?.[0]?.messages?.[0]?.ruleId, '@stylistic/semi');
8081
});
@@ -139,10 +140,11 @@ test('flat config > js > space', async t => {
139140
});
140141

141142
test('flat config > ts > space', async t => {
142-
const filePath = path.join(t.context.cwd, 'test.ts');
143+
const {cwd} = t.context;
144+
const filePath = path.join(cwd, 'test.ts');
143145

144146
await fs.writeFile(
145-
path.join(t.context.cwd, 'xo.config.js'),
147+
path.join(cwd, 'xo.config.js'),
146148
dedent`
147149
export default [
148150
{
@@ -153,17 +155,15 @@ test('flat config > ts > space', async t => {
153155
'utf8',
154156
);
155157

156-
const xo = new Xo({cwd: t.context.cwd});
157-
const {results} = await xo.lintText(
158-
dedent`
159-
export function foo() {
160-
console.log('hello');
161-
}\n
162-
`,
163-
{
164-
filePath,
165-
},
166-
);
158+
const text = dedent`
159+
export function foo() {
160+
console.log('hello');
161+
}\n
162+
`;
163+
await fs.writeFile(filePath, text, 'utf8');
164+
165+
const xo = new Xo({cwd});
166+
const {results} = await xo.lintText(text, {filePath});
167167
t.is(results?.[0]?.messages.length, 1);
168168
t.is(results?.[0]?.messages?.[0]?.messageId, 'wrongIndentation');
169169
t.is(results?.[0]?.messages?.[0]?.ruleId, '@stylistic/indent');
@@ -188,18 +188,17 @@ test('plugin > js > no-use-extend-native', async t => {
188188
);
189189
});
190190

191-
test('pliugin > ts > no-use-extend-native', async t => {
191+
test('plugin > ts > no-use-extend-native', async t => {
192192
const {cwd} = t.context;
193-
const tsFilePath = path.join(t.context.cwd, 'test.ts');
194-
const {results} = await new Xo({cwd}).lintText(
195-
dedent`
196-
import {util} from 'node:util';
193+
const filePath = path.join(cwd, 'test.ts');
194+
const text = dedent`
195+
import {util} from 'node:util';
197196
198-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
199-
util.isBoolean('50bda47b09923e045759db8e8dd01a0bacd97370'.shortHash() === '50bdcs47');\n
200-
`,
201-
{filePath: tsFilePath},
202-
);
197+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
198+
util.isBoolean('50bda47b09923e045759db8e8dd01a0bacd97370'.shortHash() === '50bdcs47');\n
199+
`;
200+
await fs.writeFile(filePath, text, 'utf8');
201+
const {results} = await new Xo({cwd}).lintText(text, {filePath});
203202
t.true(results[0]?.messages?.length === 1);
204203
t.truthy(results[0]?.messages?.[0]);
205204
t.is(
@@ -229,15 +228,14 @@ test('plugin > js > eslint-plugin-import import-x/order', async t => {
229228
test('plugin > ts > eslint-plugin-import import-x/order', async t => {
230229
const {cwd} = t.context;
231230
const filePath = path.join(cwd, 'test.ts');
232-
const {results} = await new Xo({cwd}).lintText(
233-
dedent`
234-
import foo from 'foo';
235-
import util from 'node:util';
231+
const text = dedent`
232+
import foo from 'foo';
233+
import util from 'node:util';
236234
237-
util.inspect(foo);\n
238-
`,
239-
{filePath},
240-
);
235+
util.inspect(foo);\n
236+
`;
237+
await fs.writeFile(filePath, text, 'utf8');
238+
const {results} = await new Xo({cwd}).lintText(text, {filePath});
241239
t.true(results[0]?.messages?.length === 1);
242240
t.truthy(results[0]?.messages?.[0]);
243241
t.is(results[0]?.messages?.[0]?.ruleId, 'import-x/order');
@@ -262,14 +260,13 @@ test('plugin > js > eslint-plugin-import import-x/extensions', async t => {
262260
test('plugin > ts > eslint-plugin-import import-x/extensions', async t => {
263261
const {cwd} = t.context;
264262
const filePath = path.join(cwd, 'test.ts');
265-
const {results} = await new Xo({cwd}).lintText(
266-
dedent`
267-
import foo from './foo';
263+
const text = dedent`
264+
import foo from './foo';
268265
269-
console.log(foo);\n
270-
`,
271-
{filePath},
272-
);
266+
console.log(foo);\n
267+
`;
268+
await fs.writeFile(filePath, text, 'utf8');
269+
const {results} = await new Xo({cwd}).lintText(text, {filePath});
273270
t.true(results[0]?.messages?.length === 1);
274271
t.truthy(results[0]?.messages?.[0]);
275272
t.is(results[0]?.messages?.[0]?.ruleId, 'import-x/extensions');
@@ -278,14 +275,13 @@ test('plugin > ts > eslint-plugin-import import-x/extensions', async t => {
278275
test('plugin > ts > eslint-plugin-import import-x/no-absolute-path', async t => {
279276
const {cwd} = t.context;
280277
const filePath = path.join(cwd, 'test.ts');
281-
const {results} = await new Xo({cwd}).lintText(
282-
dedent`
283-
import foo from '/foo';
278+
const text = dedent`
279+
import foo from '/foo';
284280
285-
console.log(foo);\n
286-
`,
287-
{filePath},
288-
);
281+
console.log(foo);\n
282+
`;
283+
await fs.writeFile(filePath, text, 'utf8');
284+
const {results} = await new Xo({cwd}).lintText(text, {filePath});
289285
t.true(results[0]?.messages?.some(({ruleId}) => ruleId === 'import-x/no-absolute-path'));
290286
});
291287

@@ -318,13 +314,12 @@ test('plugin > js > eslint-plugin-n n/prefer-global/process', async t => {
318314

319315
test('plugin > ts > eslint-plugin-n n/prefer-global/process', async t => {
320316
const {cwd} = t.context;
321-
const tsFilePath = path.join(cwd, 'test.ts');
322-
const {results} = await new Xo({cwd}).lintText(
323-
dedent`
324-
process.cwd();\n
325-
`,
326-
{filePath: tsFilePath},
327-
);
317+
const filePath = path.join(cwd, 'test.ts');
318+
const text = dedent`
319+
process.cwd();\n
320+
`;
321+
await fs.writeFile(filePath, text, 'utf8');
322+
const {results} = await new Xo({cwd}).lintText(text, {filePath});
328323
t.true(results[0]?.messages?.length === 1);
329324
t.truthy(results[0]?.messages?.[0]);
330325
t.is(results[0]?.messages?.[0]?.ruleId, 'n/prefer-global/process');
@@ -351,17 +346,61 @@ test('plugin > js > eslint-plugin-eslint-comments enable-duplicate-disable', asy
351346
test('plugin > ts > eslint-plugin-eslint-comments no-duplicate-disable', async t => {
352347
const {cwd} = t.context;
353348
const tsFilePath = path.join(cwd, 'test.ts');
354-
const {results} = await new Xo({
355-
cwd,
356-
}).lintText(
357-
dedent`
358-
/* eslint-disable no-undef */
359-
export const foo = 10; // eslint-disable-line no-undef
360-
\n
361-
`,
362-
{filePath: tsFilePath},
363-
);
349+
const text = dedent`
350+
/* eslint-disable no-undef */
351+
export const foo = 10; // eslint-disable-line no-undef
352+
\n
353+
`;
354+
await fs.writeFile(tsFilePath, text, 'utf8');
355+
const {results} = await new Xo({cwd}).lintText(text, {filePath: tsFilePath});
364356
t.true(results[0]?.errorCount === 1);
365357
t.true(results[0]?.messages.some(({ruleId}) =>
366358
ruleId === '@eslint-community/eslint-comments/no-duplicate-disable'));
367359
});
360+
361+
test('lint-text can be ran multiple times in a row with top level typescript rules', async t => {
362+
const {cwd} = t.context;
363+
364+
const filePath = path.join(cwd, 'test.ts');
365+
// Text should violate the @typescript-eslint/naming-convention rule
366+
const text = dedent`
367+
const fooBar = 10;
368+
const FooBar = 10;
369+
const FOO_BAR = 10;
370+
const foo_bar = 10;\n
371+
`;
372+
373+
// We must write tsfiles to disk for the typescript rules to apply
374+
await fs.writeFile(filePath, text, 'utf8');
375+
376+
const {results: resultsNoConfig} = await Xo.lintText(text, {cwd, filePath});
377+
// Ensure that with no config, the text is linted and errors are found
378+
t.true(resultsNoConfig[0]?.errorCount === 3);
379+
380+
await fs.writeFile(
381+
path.join(cwd, 'xo.config.ts'),
382+
dedent`
383+
export default [
384+
{
385+
rules: {
386+
'@typescript-eslint/naming-convention': 'off',
387+
'@typescript-eslint/no-unused-vars': 'off'
388+
},
389+
}
390+
];\n
391+
`,
392+
'utf8',
393+
);
394+
395+
// Now with a config that turns off the naming-convention rule, the text should not have any errors
396+
// and should not have any messages when ran multiple times
397+
const {results} = await Xo.lintText(text, {cwd, filePath});
398+
t.is(results[0]?.errorCount, 0);
399+
t.true(results[0]?.messages?.length === 0);
400+
const {results: results2} = await Xo.lintText(text, {cwd, filePath});
401+
t.is(results2[0]?.errorCount, 0);
402+
t.true(results2[0]?.messages?.length === 0);
403+
const {results: results3} = await Xo.lintText(text, {cwd, filePath});
404+
t.is(results3[0]?.errorCount, 0);
405+
t.true(results3[0]?.messages?.length === 0);
406+
});

0 commit comments

Comments
 (0)