Skip to content

Commit fc8950d

Browse files
committed
Revert "Simplify lintFiles (xojs#583)"
This reverts commit e2e715d.
1 parent 22177d1 commit fc8950d

File tree

4 files changed

+251
-33
lines changed

4 files changed

+251
-33
lines changed

index.js

Lines changed: 27 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@ import process from 'node:process';
22
import path from 'node:path';
33
import {ESLint} from 'eslint';
44
import {globby, isGitIgnoredSync} from 'globby';
5-
import {omit, isEqual} from 'lodash-es';
5+
import {isEqual} from 'lodash-es';
66
import micromatch from 'micromatch';
77
import arrify from 'arrify';
8+
import pReduce from 'p-reduce';
89
import pMap from 'p-map';
10+
import {cosmiconfig, defaultLoaders} from 'cosmiconfig';
911
import defineLazyProperty from 'define-lazy-prop';
12+
import pFilter from 'p-filter';
1013
import slash from 'slash';
14+
import {CONFIG_FILES, MODULE_NAME, DEFAULT_IGNORES} from './lib/constants.js';
1115
import {
1216
normalizeOptions,
1317
getIgnores,
1418
mergeWithFileConfig,
19+
mergeWithFileConfigs,
1520
buildConfig,
1621
mergeOptions,
1722
} from './lib/options-manager.js';
@@ -82,19 +87,10 @@ const processReport = (report, {isQuiet = false} = {}) => {
8287
return result;
8388
};
8489

85-
const runEslint = async (filePath, options, processorOptions) => {
86-
const engine = new ESLint(omit(options, ['filePath', 'warnIgnored']));
87-
const filename = path.relative(options.cwd, filePath);
90+
const runEslint = async (paths, options, processorOptions) => {
91+
const engine = new ESLint(options);
8892

89-
if (
90-
micromatch.isMatch(filename, options.baseConfig.ignorePatterns)
91-
|| isGitIgnoredSync({cwd: options.cwd, ignore: options.baseConfig.ignorePatterns})(filePath)
92-
|| await engine.isPathIgnored(filePath)
93-
) {
94-
return;
95-
}
96-
97-
const report = await engine.lintFiles([filePath]);
93+
const report = await engine.lintFiles(await pFilter(paths, async path => !(await engine.isPathIgnored(path))));
9894
return processReport(report, processorOptions);
9995
};
10096

@@ -149,24 +145,25 @@ const lintText = async (string, inputOptions = {}) => {
149145
};
150146

151147
const lintFiles = async (patterns, inputOptions = {}) => {
152-
inputOptions = normalizeOptions(inputOptions);
153148
inputOptions.cwd = path.resolve(inputOptions.cwd || process.cwd());
154-
155-
const files = await globFiles(patterns, mergeOptions(inputOptions));
156-
157-
const reports = await pMap(
158-
files,
159-
async filePath => {
160-
const {options: foundOptions, prettierOptions} = mergeWithFileConfig({
161-
...inputOptions,
162-
filePath,
163-
});
164-
const options = buildConfig(foundOptions, prettierOptions);
165-
return runEslint(filePath, options, {isQuiet: inputOptions.quiet});
166-
},
167-
);
168-
169-
return mergeReports(reports.filter(Boolean));
149+
const configExplorer = cosmiconfig(MODULE_NAME, {searchPlaces: CONFIG_FILES, loaders: {noExt: defaultLoaders['.json']}, stopDir: inputOptions.cwd});
150+
151+
const configFiles = (await Promise.all(
152+
(await globby(
153+
CONFIG_FILES.map(configFile => `**/${configFile}`),
154+
{ignore: DEFAULT_IGNORES, gitignore: true, absolute: true, cwd: inputOptions.cwd},
155+
)).map(configFile => configExplorer.load(configFile)),
156+
)).filter(Boolean);
157+
158+
const paths = configFiles.length > 0
159+
? await pReduce(
160+
configFiles,
161+
async (paths, {filepath, config}) =>
162+
[...paths, ...(await globFiles(patterns, {...mergeOptions(inputOptions, config), cwd: path.dirname(filepath)}))],
163+
[])
164+
: await globFiles(patterns, mergeOptions(inputOptions));
165+
166+
return mergeReports(await pMap(await mergeWithFileConfigs([...new Set(paths)], inputOptions, configFiles), async ({files, options, prettierOptions}) => runEslint(files, buildConfig(options, prettierOptions), {isQuiet: options.quiet})));
170167
};
171168

172169
const getFormatter = async name => {

lib/options-manager.js

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,19 @@ import os from 'node:os';
44
import path from 'node:path';
55
import fsExtra from 'fs-extra';
66
import arrify from 'arrify';
7-
import {mergeWith, flow, pick} from 'lodash-es';
7+
import {mergeWith, groupBy, flow, pick} from 'lodash-es';
88
import {findUpSync} from 'find-up';
99
import findCacheDir from 'find-cache-dir';
1010
import prettier from 'prettier';
1111
import semver from 'semver';
12-
import {cosmiconfigSync, defaultLoaders} from 'cosmiconfig';
12+
import {cosmiconfig, cosmiconfigSync, defaultLoaders} from 'cosmiconfig';
13+
import pReduce from 'p-reduce';
1314
import micromatch from 'micromatch';
1415
import JSON5 from 'json5';
1516
import toAbsoluteGlob from 'to-absolute-glob';
1617
import stringify from 'json-stable-stringify-without-jsonify';
1718
import murmur from 'imurmurhash';
19+
import isPathInside from 'is-path-inside';
1820
import {Legacy} from '@eslint/eslintrc';
1921
import createEsmUtils from 'esm-utils';
2022
import {
@@ -31,7 +33,7 @@ import {
3133

3234
const {__dirname, json, require} = createEsmUtils(import.meta);
3335
const pkg = json.loadSync('../package.json');
34-
const {outputJsonSync} = fsExtra;
36+
const {outputJson, outputJsonSync} = fsExtra;
3537
const {normalizePackageName} = Legacy.naming;
3638
const resolveModule = Legacy.ModuleResolver.resolve;
3739

@@ -136,6 +138,69 @@ const mergeWithFileConfig = options => {
136138
return {options, prettierOptions};
137139
};
138140

141+
/**
142+
Find config for each files found by `lintFiles`.
143+
The config files are searched starting from each files.
144+
*/
145+
const mergeWithFileConfigs = async (files, options, configFiles) => {
146+
configFiles = configFiles.sort((a, b) => b.filepath.split(path.sep).length - a.filepath.split(path.sep).length);
147+
const tsConfigs = {};
148+
149+
const groups = [...(await pReduce(files, async (configs, file) => {
150+
const pkgConfigExplorer = cosmiconfig('engines', {searchPlaces: ['package.json'], stopDir: options.cwd});
151+
152+
const {config: xoOptions, filepath: xoConfigPath} = findApplicableConfig(file, configFiles) || {};
153+
const {config: enginesOptions, filepath: enginesConfigPath} = await pkgConfigExplorer.search(file) || {};
154+
155+
let fileOptions = mergeOptions(options, xoOptions, enginesOptions);
156+
fileOptions.cwd = xoConfigPath && path.dirname(xoConfigPath) !== fileOptions.cwd ? path.resolve(fileOptions.cwd, path.dirname(xoConfigPath)) : fileOptions.cwd;
157+
158+
const {hash, options: optionsWithOverrides} = applyOverrides(file, fileOptions);
159+
fileOptions = optionsWithOverrides;
160+
161+
const prettierOptions = fileOptions.prettier ? await prettier.resolveConfig(file, {editorconfig: true}) || {} : {};
162+
163+
let tsConfigPath;
164+
if (isTypescript(file)) {
165+
let tsConfig;
166+
const tsConfigExplorer = cosmiconfig([], {searchPlaces: ['tsconfig.json'], loaders: {'.json': (_, content) => JSON5.parse(content)}});
167+
({config: tsConfig, filepath: tsConfigPath} = await tsConfigExplorer.search(file) || {});
168+
169+
fileOptions.tsConfigPath = tsConfigPath;
170+
tsConfigs[tsConfigPath || ''] = tsConfig;
171+
fileOptions.ts = true;
172+
}
173+
174+
const cacheKey = stringify({xoConfigPath, enginesConfigPath, prettierOptions, hash, tsConfigPath: fileOptions.tsConfigPath, ts: fileOptions.ts});
175+
const cachedGroup = configs.get(cacheKey);
176+
177+
configs.set(cacheKey, {
178+
files: [file, ...(cachedGroup ? cachedGroup.files : [])],
179+
options: cachedGroup ? cachedGroup.options : fileOptions,
180+
prettierOptions,
181+
});
182+
183+
return configs;
184+
}, new Map())).values()];
185+
186+
await Promise.all(Object.entries(groupBy(groups.filter(({options}) => Boolean(options.ts)), group => group.options.tsConfigPath || '')).map(
187+
([tsConfigPath, groups]) => {
188+
const files = groups.flatMap(group => group.files);
189+
const cachePath = getTsConfigCachePath(files, tsConfigPath, options.cwd);
190+
191+
for (const group of groups) {
192+
group.options.tsConfigPath = cachePath;
193+
}
194+
195+
return outputJson(cachePath, makeTSConfig(tsConfigs[tsConfigPath], tsConfigPath, files));
196+
},
197+
));
198+
199+
return groups;
200+
};
201+
202+
const findApplicableConfig = (file, configFiles) => configFiles.find(({filepath}) => isPathInside(file, path.dirname(filepath)));
203+
139204
/**
140205
Generate a unique and consistent path for the temporary `tsconfig.json`.
141206
Hashing based on https://github.com/eslint/eslint/blob/cf38d0d939b62f3670cdd59f0143fd896fccd771/lib/cli-engine/lint-result-cache.js#L30
@@ -539,6 +604,7 @@ export {
539604
mergeWithPrettierConfig,
540605
normalizeOptions,
541606
getIgnores,
607+
mergeWithFileConfigs,
542608
mergeWithFileConfig,
543609
buildConfig,
544610
applyOverrides,

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@
8585
"meow": "^10.1.1",
8686
"micromatch": "^4.0.4",
8787
"open-editor": "^3.0.0",
88+
"p-filter": "^2.1.0",
8889
"p-map": "^5.1.0",
90+
"p-reduce": "^3.0.0",
8991
"path-exists": "^4.0.0",
9092
"prettier": "^2.4.1",
9193
"semver": "^7.3.5",

test/options-manager.js

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,159 @@ test('mergeWithFileConfig: tsx files', async t => {
587587
});
588588
});
589589

590+
test('mergeWithFileConfigs: nested configs with prettier', async t => {
591+
const cwd = path.resolve('fixtures', 'nested-configs');
592+
const paths = [
593+
'no-semicolon.js',
594+
'child/semicolon.js',
595+
'child-override/two-spaces.js',
596+
'child-override/child-prettier-override/semicolon.js',
597+
].map(file => path.resolve(cwd, file));
598+
const result = await manager.mergeWithFileConfigs(paths, {cwd}, [
599+
{
600+
filepath: path.resolve(cwd, 'child-override', 'child-prettier-override', 'package.json'),
601+
config: {overrides: [{files: 'semicolon.js', prettier: true}]},
602+
},
603+
{filepath: path.resolve(cwd, 'package.json'), config: {semicolon: true}},
604+
{
605+
filepath: path.resolve(cwd, 'child-override', 'package.json'),
606+
config: {overrides: [{files: 'two-spaces.js', space: 4}]},
607+
},
608+
{filepath: path.resolve(cwd, 'child', 'package.json'), config: {semicolon: false}},
609+
]);
610+
611+
t.deepEqual(result, [
612+
{
613+
files: [path.resolve(cwd, 'no-semicolon.js')],
614+
options: {
615+
semicolon: true,
616+
cwd,
617+
extensions: DEFAULT_EXTENSION,
618+
ignores: DEFAULT_IGNORES,
619+
},
620+
prettierOptions: {},
621+
},
622+
{
623+
files: [path.resolve(cwd, 'child/semicolon.js')],
624+
options: {
625+
semicolon: false,
626+
cwd: path.resolve(cwd, 'child'),
627+
extensions: DEFAULT_EXTENSION,
628+
ignores: DEFAULT_IGNORES,
629+
},
630+
prettierOptions: {},
631+
},
632+
{
633+
files: [path.resolve(cwd, 'child-override/two-spaces.js')],
634+
options: {
635+
space: 4,
636+
rules: {},
637+
settings: {},
638+
globals: [],
639+
envs: [],
640+
plugins: [],
641+
extends: [],
642+
cwd: path.resolve(cwd, 'child-override'),
643+
extensions: DEFAULT_EXTENSION,
644+
ignores: DEFAULT_IGNORES,
645+
},
646+
prettierOptions: {},
647+
},
648+
{
649+
files: [path.resolve(cwd, 'child-override/child-prettier-override/semicolon.js')],
650+
options: {
651+
prettier: true,
652+
rules: {},
653+
settings: {},
654+
globals: [],
655+
envs: [],
656+
plugins: [],
657+
extends: [],
658+
cwd: path.resolve(cwd, 'child-override', 'child-prettier-override'),
659+
extensions: DEFAULT_EXTENSION,
660+
ignores: DEFAULT_IGNORES,
661+
},
662+
prettierOptions: {endOfLine: 'lf', semi: false, useTabs: true},
663+
},
664+
]);
665+
});
666+
667+
test('mergeWithFileConfigs: typescript files', async t => {
668+
const cwd = path.resolve('fixtures', 'typescript');
669+
const paths = ['two-spaces.tsx', 'child/extra-semicolon.ts', 'child/sub-child/four-spaces.ts'].map(file => path.resolve(cwd, file));
670+
const configFiles = [
671+
{filepath: path.resolve(cwd, 'child/sub-child/package.json'), config: {space: 2}},
672+
{filepath: path.resolve(cwd, 'package.json'), config: {space: 4}},
673+
{filepath: path.resolve(cwd, 'child/package.json'), config: {semicolon: false}},
674+
];
675+
const result = await manager.mergeWithFileConfigs(paths, {cwd}, configFiles);
676+
677+
t.deepEqual(omit(result[0], 'options.tsConfigPath'), {
678+
files: [path.resolve(cwd, 'two-spaces.tsx')],
679+
options: {
680+
space: 4,
681+
cwd,
682+
extensions: DEFAULT_EXTENSION,
683+
ignores: DEFAULT_IGNORES,
684+
ts: true,
685+
},
686+
prettierOptions: {},
687+
});
688+
t.deepEqual(await readJson(result[0].options.tsConfigPath), {
689+
files: [path.resolve(cwd, 'two-spaces.tsx')],
690+
compilerOptions: {
691+
newLine: 'lf',
692+
noFallthroughCasesInSwitch: true,
693+
noImplicitReturns: true,
694+
noUnusedLocals: true,
695+
noUnusedParameters: true,
696+
strict: true,
697+
target: 'es2018',
698+
},
699+
});
700+
701+
t.deepEqual(omit(result[1], 'options.tsConfigPath'), {
702+
files: [path.resolve(cwd, 'child/extra-semicolon.ts')],
703+
options: {
704+
semicolon: false,
705+
cwd: path.resolve(cwd, 'child'),
706+
extensions: DEFAULT_EXTENSION,
707+
ignores: DEFAULT_IGNORES,
708+
ts: true,
709+
},
710+
prettierOptions: {},
711+
});
712+
713+
t.deepEqual(omit(result[2], 'options.tsConfigPath'), {
714+
files: [path.resolve(cwd, 'child/sub-child/four-spaces.ts')],
715+
options: {
716+
space: 2,
717+
cwd: path.resolve(cwd, 'child/sub-child'),
718+
extensions: DEFAULT_EXTENSION,
719+
ignores: DEFAULT_IGNORES,
720+
ts: true,
721+
},
722+
prettierOptions: {},
723+
});
724+
725+
// Verify that we use the same temporary tsconfig.json for both files group sharing the same original tsconfig.json even if they have different xo config
726+
t.is(result[1].options.tsConfigPath, result[2].options.tsConfigPath);
727+
t.deepEqual(await readJson(result[1].options.tsConfigPath), {
728+
extends: path.resolve(cwd, 'child/tsconfig.json'),
729+
files: [path.resolve(cwd, 'child/extra-semicolon.ts'), path.resolve(cwd, 'child/sub-child/four-spaces.ts')],
730+
include: [
731+
slash(path.resolve(cwd, 'child/**/*.ts')),
732+
slash(path.resolve(cwd, 'child/**/*.tsx')),
733+
],
734+
});
735+
736+
const secondResult = await manager.mergeWithFileConfigs(paths, {cwd}, configFiles);
737+
738+
// Verify that on each run the options.tsConfigPath is consistent to preserve ESLint cache
739+
t.is(result[0].options.tsConfigPath, secondResult[0].options.tsConfigPath);
740+
t.is(result[1].options.tsConfigPath, secondResult[1].options.tsConfigPath);
741+
});
742+
590743
test('applyOverrides', t => {
591744
t.deepEqual(
592745
manager.applyOverrides(

0 commit comments

Comments
 (0)