Skip to content

Commit e2e715d

Browse files
authored
Simplify lintFiles (#583)
1 parent 2670d3d commit e2e715d

File tree

4 files changed

+33
-251
lines changed

4 files changed

+33
-251
lines changed

index.js

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,16 @@ import process from 'node:process';
22
import path from 'node:path';
33
import {ESLint} from 'eslint';
44
import {globby, isGitIgnoredSync} from 'globby';
5-
import {isEqual} from 'lodash-es';
5+
import {omit, isEqual} from 'lodash-es';
66
import micromatch from 'micromatch';
77
import arrify from 'arrify';
8-
import pReduce from 'p-reduce';
98
import pMap from 'p-map';
10-
import {cosmiconfig, defaultLoaders} from 'cosmiconfig';
119
import defineLazyProperty from 'define-lazy-prop';
12-
import pFilter from 'p-filter';
1310
import slash from 'slash';
14-
import {CONFIG_FILES, MODULE_NAME, DEFAULT_IGNORES} from './lib/constants.js';
1511
import {
1612
normalizeOptions,
1713
getIgnores,
1814
mergeWithFileConfig,
19-
mergeWithFileConfigs,
2015
buildConfig,
2116
mergeOptions,
2217
} from './lib/options-manager.js';
@@ -87,10 +82,19 @@ const processReport = (report, {isQuiet = false} = {}) => {
8782
return result;
8883
};
8984

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

93-
const report = await engine.lintFiles(await pFilter(paths, async path => !(await engine.isPathIgnored(path))));
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]);
9498
return processReport(report, processorOptions);
9599
};
96100

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

147151
const lintFiles = async (patterns, inputOptions = {}) => {
152+
inputOptions = normalizeOptions(inputOptions);
148153
inputOptions.cwd = path.resolve(inputOptions.cwd || process.cwd());
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})));
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));
167170
};
168171

169172
const getFormatter = async name => {

lib/options-manager.js

Lines changed: 3 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,18 @@ import os from 'node:os';
33
import path from 'node:path';
44
import fsExtra from 'fs-extra';
55
import arrify from 'arrify';
6-
import {mergeWith, groupBy, flow, pick} from 'lodash-es';
6+
import {mergeWith, flow, pick} from 'lodash-es';
77
import pathExists from 'path-exists';
88
import findUp from 'find-up';
99
import findCacheDir from 'find-cache-dir';
1010
import prettier from 'prettier';
1111
import semver from 'semver';
12-
import {cosmiconfig, cosmiconfigSync, defaultLoaders} from 'cosmiconfig';
13-
import pReduce from 'p-reduce';
12+
import {cosmiconfigSync, defaultLoaders} from 'cosmiconfig';
1413
import micromatch from 'micromatch';
1514
import JSON5 from 'json5';
1615
import toAbsoluteGlob from 'to-absolute-glob';
1716
import stringify from 'json-stable-stringify-without-jsonify';
1817
import murmur from 'imurmurhash';
19-
import isPathInside from 'is-path-inside';
2018
import eslintrc from '@eslint/eslintrc';
2119
import createEsmUtils from 'esm-utils';
2220
import {
@@ -34,7 +32,7 @@ import {
3432

3533
const {__dirname, json, require} = createEsmUtils(import.meta);
3634
const pkg = json.loadSync('../package.json');
37-
const {outputJson, outputJsonSync} = fsExtra;
35+
const {outputJsonSync} = fsExtra;
3836
const {normalizePackageName} = eslintrc.Legacy.naming;
3937
const resolveModule = eslintrc.Legacy.ModuleResolver.resolve;
4038

@@ -139,69 +137,6 @@ const mergeWithFileConfig = options => {
139137
return {options, prettierOptions};
140138
};
141139

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

package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,7 @@
8787
"meow": "^10.1.1",
8888
"micromatch": "^4.0.4",
8989
"open-editor": "^3.0.0",
90-
"p-filter": "^2.1.0",
9190
"p-map": "^5.1.0",
92-
"p-reduce": "^3.0.0",
9391
"path-exists": "^4.0.0",
9492
"prettier": "^2.3.2",
9593
"semver": "^7.3.5",

test/options-manager.js

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

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

0 commit comments

Comments
 (0)