diff --git a/index.js b/index.js index 2675f9d1..f86cc715 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ import path from 'node:path'; import {ESLint} from 'eslint'; import {globby, isGitIgnoredSync} from 'globby'; -import {isEqual} from 'lodash-es'; +import {isEqual, groupBy} from 'lodash-es'; import micromatch from 'micromatch'; import arrify from 'arrify'; import slash from 'slash'; @@ -12,30 +12,6 @@ import { } from './lib/options-manager.js'; import {mergeReports, processReport, getIgnoredReport} from './lib/report.js'; -const runEslint = async (lint, options) => { - const {filePath, eslintOptions, isQuiet} = options; - const {cwd, baseConfig: {ignorePatterns}} = eslintOptions; - - if ( - filePath - && ( - micromatch.isMatch(path.relative(cwd, filePath), ignorePatterns) - || isGitIgnoredSync({cwd, ignore: ignorePatterns})(filePath) - ) - ) { - return getIgnoredReport(filePath); - } - - const eslint = new ESLint(eslintOptions); - - if (filePath && await eslint.isPathIgnored(filePath)) { - return getIgnoredReport(filePath); - } - - const report = await lint(eslint); - return processReport(report, {isQuiet}); -}; - const globFiles = async (patterns, options) => { const {ignores, extensions, cwd} = (await mergeWithFileConfig(options)).options; @@ -59,32 +35,76 @@ const getConfig = async options => { const lintText = async (string, options) => { options = await parseOptions(options); - const {filePath, warnIgnored, eslintOptions} = options; - const {ignorePatterns} = eslintOptions.baseConfig; + const {filePath, warnIgnored, eslintOptions, isQuiet} = options; + const {cwd, baseConfig: {ignorePatterns}} = eslintOptions; if (typeof filePath !== 'string' && !isEqual(getIgnores({}), ignorePatterns)) { throw new Error('The `ignores` option requires the `filePath` option to be defined.'); } - return runEslint( - eslint => eslint.lintText(string, {filePath, warnIgnored}), - options, - ); -}; + if ( + filePath + && ( + micromatch.isMatch(path.relative(cwd, filePath), ignorePatterns) + || isGitIgnoredSync({cwd, ignore: ignorePatterns})(filePath) + ) + ) { + return getIgnoredReport(filePath); + } -const lintFile = async (filePath, options) => runEslint( - eslint => eslint.lintFiles([filePath]), - await parseOptions({...options, filePath}), -); + const eslint = new ESLint(eslintOptions); + + if (filePath && await eslint.isPathIgnored(filePath)) { + return getIgnoredReport(filePath); + } + + const report = await eslint.lintText(string, {filePath, warnIgnored}); + return processReport(report, {isQuiet}); +}; const lintFiles = async (patterns, options) => { const files = await globFiles(patterns, options); - const reports = await Promise.all( - files.map(filePath => lintFile(filePath, options)), + const allOptions = await Promise.all( + files.map(filePath => parseOptions({...options, filePath})), ); - const report = mergeReports(reports.filter(({isIgnored}) => !isIgnored)); + // Files with same `xoConfigPath` can lint together + // https://github.com/xojs/xo/issues/599 + const groups = groupBy(allOptions, 'xoConfigPath'); + + const reports = await Promise.all( + Object.values(groups) + .map(async filesWithOptions => { + const options = filesWithOptions[0]; + const eslint = new ESLint(options.eslintOptions); + const files = []; + + for (const options of filesWithOptions) { + const {filePath, eslintOptions} = options; + const {cwd, baseConfig: {ignorePatterns}} = eslintOptions; + if (filePath + && ( + micromatch.isMatch(path.relative(cwd, filePath), ignorePatterns) + || isGitIgnoredSync({cwd, ignore: ignorePatterns})(filePath) + )) { + continue; + } + + // eslint-disable-next-line no-await-in-loop + if ((await eslint.isPathIgnored(filePath))) { + continue; + } + + files.push(filePath); + } + + const report = await eslint.lintFiles(files); + + return processReport(report, {isQuiet: options.isQuiet}); + })); + + const report = mergeReports(reports); return report; }; diff --git a/lib/options-manager.js b/lib/options-manager.js index 664cf423..d2cde38c 100644 --- a/lib/options-manager.js +++ b/lib/options-manager.js @@ -109,14 +109,19 @@ const mergeWithFileConfig = async options => { const searchPath = options.filePath || options.cwd; - const {config: xoOptions, filepath: xoConfigPath} = (await configExplorer.search(searchPath)) || {}; + let {config: xoOptions, filepath: xoConfigPath} = (await configExplorer.search(searchPath)) || {}; const {config: enginesOptions} = (await pkgConfigExplorer.search(searchPath)) || {}; options = mergeOptions(options, xoOptions, enginesOptions); options.cwd = xoConfigPath && path.dirname(xoConfigPath) !== options.cwd ? path.resolve(options.cwd, path.dirname(xoConfigPath)) : options.cwd; if (options.filePath) { - ({options} = applyOverrides(options.filePath, options)); + const overrides = applyOverrides(options.filePath, options); + options = overrides.options; + + if (overrides.hash) { + xoConfigPath += overrides.hash; + } } const prettierOptions = options.prettier ? await prettier.resolveConfig(searchPath, {editorconfig: true}) || {} : {}; @@ -132,7 +137,7 @@ const mergeWithFileConfig = async options => { await fs.writeFile(options.tsConfigPath, JSON.stringify(config)); } - return {options, prettierOptions}; + return {options, prettierOptions, xoConfigPath}; }; /** @@ -538,13 +543,14 @@ const gatherImportResolvers = options => { const parseOptions = async options => { options = normalizeOptions(options); - const {options: foundOptions, prettierOptions} = await mergeWithFileConfig(options); + const {options: foundOptions, prettierOptions, xoConfigPath} = await mergeWithFileConfig(options); const {filePath, warnIgnored, ...eslintOptions} = buildConfig(foundOptions, prettierOptions); return { filePath, warnIgnored, isQuiet: options.quiet, eslintOptions, + xoConfigPath, }; };