Skip to content

Commit

Permalink
Revert "Simplify lintFiles (xojs#583)"
Browse files Browse the repository at this point in the history
This reverts commit e2e715d.
+add in "is-path-inside" dep
+remove unused/moot dep "path-exists"
  • Loading branch information
devinrhode2 committed Oct 15, 2021
1 parent 22177d1 commit dcb76f3
Show file tree
Hide file tree
Showing 4 changed files with 252 additions and 34 deletions.
57 changes: 27 additions & 30 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@ import process from 'node:process';
import path from 'node:path';
import {ESLint} from 'eslint';
import {globby, isGitIgnoredSync} from 'globby';
import {omit, isEqual} from 'lodash-es';
import {isEqual} from 'lodash-es';
import micromatch from 'micromatch';
import arrify from 'arrify';
import pReduce from 'p-reduce';
import pMap from 'p-map';
import {cosmiconfig, defaultLoaders} from 'cosmiconfig';
import defineLazyProperty from 'define-lazy-prop';
import pFilter from 'p-filter';
import slash from 'slash';
import {CONFIG_FILES, MODULE_NAME, DEFAULT_IGNORES} from './lib/constants.js';
import {
normalizeOptions,
getIgnores,
mergeWithFileConfig,
mergeWithFileConfigs,
buildConfig,
mergeOptions,
} from './lib/options-manager.js';
Expand Down Expand Up @@ -82,19 +87,10 @@ const processReport = (report, {isQuiet = false} = {}) => {
return result;
};

const runEslint = async (filePath, options, processorOptions) => {
const engine = new ESLint(omit(options, ['filePath', 'warnIgnored']));
const filename = path.relative(options.cwd, filePath);
const runEslint = async (paths, options, processorOptions) => {
const engine = new ESLint(options);

if (
micromatch.isMatch(filename, options.baseConfig.ignorePatterns)
|| isGitIgnoredSync({cwd: options.cwd, ignore: options.baseConfig.ignorePatterns})(filePath)
|| await engine.isPathIgnored(filePath)
) {
return;
}

const report = await engine.lintFiles([filePath]);
const report = await engine.lintFiles(await pFilter(paths, async path => !(await engine.isPathIgnored(path))));
return processReport(report, processorOptions);
};

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

const lintFiles = async (patterns, inputOptions = {}) => {
inputOptions = normalizeOptions(inputOptions);
inputOptions.cwd = path.resolve(inputOptions.cwd || process.cwd());

const files = await globFiles(patterns, mergeOptions(inputOptions));

const reports = await pMap(
files,
async filePath => {
const {options: foundOptions, prettierOptions} = mergeWithFileConfig({
...inputOptions,
filePath,
});
const options = buildConfig(foundOptions, prettierOptions);
return runEslint(filePath, options, {isQuiet: inputOptions.quiet});
},
);

return mergeReports(reports.filter(Boolean));
const configExplorer = cosmiconfig(MODULE_NAME, {searchPlaces: CONFIG_FILES, loaders: {noExt: defaultLoaders['.json']}, stopDir: inputOptions.cwd});

const configFiles = (await Promise.all(
(await globby(
CONFIG_FILES.map(configFile => `**/${configFile}`),
{ignore: DEFAULT_IGNORES, gitignore: true, absolute: true, cwd: inputOptions.cwd},
)).map(configFile => configExplorer.load(configFile)),
)).filter(Boolean);

const paths = configFiles.length > 0
? await pReduce(
configFiles,
async (paths, {filepath, config}) =>
[...paths, ...(await globFiles(patterns, {...mergeOptions(inputOptions, config), cwd: path.dirname(filepath)}))],
[])
: await globFiles(patterns, mergeOptions(inputOptions));

return mergeReports(await pMap(await mergeWithFileConfigs([...new Set(paths)], inputOptions, configFiles), async ({files, options, prettierOptions}) => runEslint(files, buildConfig(options, prettierOptions), {isQuiet: options.quiet})));
};

const getFormatter = async name => {
Expand Down
72 changes: 69 additions & 3 deletions lib/options-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ import os from 'node:os';
import path from 'node:path';
import fsExtra from 'fs-extra';
import arrify from 'arrify';
import {mergeWith, flow, pick} from 'lodash-es';
import {mergeWith, groupBy, flow, pick} from 'lodash-es';
import {findUpSync} from 'find-up';
import findCacheDir from 'find-cache-dir';
import prettier from 'prettier';
import semver from 'semver';
import {cosmiconfigSync, defaultLoaders} from 'cosmiconfig';
import {cosmiconfig, cosmiconfigSync, defaultLoaders} from 'cosmiconfig';
import pReduce from 'p-reduce';
import micromatch from 'micromatch';
import JSON5 from 'json5';
import toAbsoluteGlob from 'to-absolute-glob';
import stringify from 'json-stable-stringify-without-jsonify';
import murmur from 'imurmurhash';
import isPathInside from 'is-path-inside';
import {Legacy} from '@eslint/eslintrc';
import createEsmUtils from 'esm-utils';
import {
Expand All @@ -31,7 +33,7 @@ import {

const {__dirname, json, require} = createEsmUtils(import.meta);
const pkg = json.loadSync('../package.json');
const {outputJsonSync} = fsExtra;
const {outputJson, outputJsonSync} = fsExtra;
const {normalizePackageName} = Legacy.naming;
const resolveModule = Legacy.ModuleResolver.resolve;

Expand Down Expand Up @@ -136,6 +138,69 @@ const mergeWithFileConfig = options => {
return {options, prettierOptions};
};

/**
Find config for each files found by `lintFiles`.
The config files are searched starting from each files.
*/
const mergeWithFileConfigs = async (files, options, configFiles) => {
configFiles = configFiles.sort((a, b) => b.filepath.split(path.sep).length - a.filepath.split(path.sep).length);
const tsConfigs = {};

const groups = [...(await pReduce(files, async (configs, file) => {
const pkgConfigExplorer = cosmiconfig('engines', {searchPlaces: ['package.json'], stopDir: options.cwd});

const {config: xoOptions, filepath: xoConfigPath} = findApplicableConfig(file, configFiles) || {};
const {config: enginesOptions, filepath: enginesConfigPath} = await pkgConfigExplorer.search(file) || {};

let fileOptions = mergeOptions(options, xoOptions, enginesOptions);
fileOptions.cwd = xoConfigPath && path.dirname(xoConfigPath) !== fileOptions.cwd ? path.resolve(fileOptions.cwd, path.dirname(xoConfigPath)) : fileOptions.cwd;

const {hash, options: optionsWithOverrides} = applyOverrides(file, fileOptions);
fileOptions = optionsWithOverrides;

const prettierOptions = fileOptions.prettier ? await prettier.resolveConfig(file, {editorconfig: true}) || {} : {};

let tsConfigPath;
if (isTypescript(file)) {
let tsConfig;
const tsConfigExplorer = cosmiconfig([], {searchPlaces: ['tsconfig.json'], loaders: {'.json': (_, content) => JSON5.parse(content)}});
({config: tsConfig, filepath: tsConfigPath} = await tsConfigExplorer.search(file) || {});

fileOptions.tsConfigPath = tsConfigPath;
tsConfigs[tsConfigPath || ''] = tsConfig;
fileOptions.ts = true;
}

const cacheKey = stringify({xoConfigPath, enginesConfigPath, prettierOptions, hash, tsConfigPath: fileOptions.tsConfigPath, ts: fileOptions.ts});
const cachedGroup = configs.get(cacheKey);

configs.set(cacheKey, {
files: [file, ...(cachedGroup ? cachedGroup.files : [])],
options: cachedGroup ? cachedGroup.options : fileOptions,
prettierOptions,
});

return configs;
}, new Map())).values()];

await Promise.all(Object.entries(groupBy(groups.filter(({options}) => Boolean(options.ts)), group => group.options.tsConfigPath || '')).map(
([tsConfigPath, groups]) => {
const files = groups.flatMap(group => group.files);
const cachePath = getTsConfigCachePath(files, tsConfigPath, options.cwd);

for (const group of groups) {
group.options.tsConfigPath = cachePath;
}

return outputJson(cachePath, makeTSConfig(tsConfigs[tsConfigPath], tsConfigPath, files));
},
));

return groups;
};

const findApplicableConfig = (file, configFiles) => configFiles.find(({filepath}) => isPathInside(file, path.dirname(filepath)));

/**
Generate a unique and consistent path for the temporary `tsconfig.json`.
Hashing based on https://github.com/eslint/eslint/blob/cf38d0d939b62f3670cdd59f0143fd896fccd771/lib/cli-engine/lint-result-cache.js#L30
Expand Down Expand Up @@ -539,6 +604,7 @@ export {
mergeWithPrettierConfig,
normalizeOptions,
getIgnores,
mergeWithFileConfigs,
mergeWithFileConfig,
buildConfig,
applyOverrides,
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,16 @@
"get-stdin": "^9.0.0",
"globby": "^12.0.2",
"imurmurhash": "^0.1.4",
"is-path-inside": "^4.0.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"json5": "^2.2.0",
"lodash-es": "^4.17.21",
"meow": "^10.1.1",
"micromatch": "^4.0.4",
"open-editor": "^3.0.0",
"p-filter": "^2.1.0",
"p-map": "^5.1.0",
"path-exists": "^4.0.0",
"p-reduce": "^3.0.0",
"prettier": "^2.4.1",
"semver": "^7.3.5",
"slash": "^4.0.0",
Expand Down
153 changes: 153 additions & 0 deletions test/options-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,159 @@ test('mergeWithFileConfig: tsx files', async t => {
});
});

test('mergeWithFileConfigs: nested configs with prettier', async t => {
const cwd = path.resolve('fixtures', 'nested-configs');
const paths = [
'no-semicolon.js',
'child/semicolon.js',
'child-override/two-spaces.js',
'child-override/child-prettier-override/semicolon.js',
].map(file => path.resolve(cwd, file));
const result = await manager.mergeWithFileConfigs(paths, {cwd}, [
{
filepath: path.resolve(cwd, 'child-override', 'child-prettier-override', 'package.json'),
config: {overrides: [{files: 'semicolon.js', prettier: true}]},
},
{filepath: path.resolve(cwd, 'package.json'), config: {semicolon: true}},
{
filepath: path.resolve(cwd, 'child-override', 'package.json'),
config: {overrides: [{files: 'two-spaces.js', space: 4}]},
},
{filepath: path.resolve(cwd, 'child', 'package.json'), config: {semicolon: false}},
]);

t.deepEqual(result, [
{
files: [path.resolve(cwd, 'no-semicolon.js')],
options: {
semicolon: true,
cwd,
extensions: DEFAULT_EXTENSION,
ignores: DEFAULT_IGNORES,
},
prettierOptions: {},
},
{
files: [path.resolve(cwd, 'child/semicolon.js')],
options: {
semicolon: false,
cwd: path.resolve(cwd, 'child'),
extensions: DEFAULT_EXTENSION,
ignores: DEFAULT_IGNORES,
},
prettierOptions: {},
},
{
files: [path.resolve(cwd, 'child-override/two-spaces.js')],
options: {
space: 4,
rules: {},
settings: {},
globals: [],
envs: [],
plugins: [],
extends: [],
cwd: path.resolve(cwd, 'child-override'),
extensions: DEFAULT_EXTENSION,
ignores: DEFAULT_IGNORES,
},
prettierOptions: {},
},
{
files: [path.resolve(cwd, 'child-override/child-prettier-override/semicolon.js')],
options: {
prettier: true,
rules: {},
settings: {},
globals: [],
envs: [],
plugins: [],
extends: [],
cwd: path.resolve(cwd, 'child-override', 'child-prettier-override'),
extensions: DEFAULT_EXTENSION,
ignores: DEFAULT_IGNORES,
},
prettierOptions: {endOfLine: 'lf', semi: false, useTabs: true},
},
]);
});

test('mergeWithFileConfigs: typescript files', async t => {
const cwd = path.resolve('fixtures', 'typescript');
const paths = ['two-spaces.tsx', 'child/extra-semicolon.ts', 'child/sub-child/four-spaces.ts'].map(file => path.resolve(cwd, file));
const configFiles = [
{filepath: path.resolve(cwd, 'child/sub-child/package.json'), config: {space: 2}},
{filepath: path.resolve(cwd, 'package.json'), config: {space: 4}},
{filepath: path.resolve(cwd, 'child/package.json'), config: {semicolon: false}},
];
const result = await manager.mergeWithFileConfigs(paths, {cwd}, configFiles);

t.deepEqual(omit(result[0], 'options.tsConfigPath'), {
files: [path.resolve(cwd, 'two-spaces.tsx')],
options: {
space: 4,
cwd,
extensions: DEFAULT_EXTENSION,
ignores: DEFAULT_IGNORES,
ts: true,
},
prettierOptions: {},
});
t.deepEqual(await readJson(result[0].options.tsConfigPath), {
files: [path.resolve(cwd, 'two-spaces.tsx')],
compilerOptions: {
newLine: 'lf',
noFallthroughCasesInSwitch: true,
noImplicitReturns: true,
noUnusedLocals: true,
noUnusedParameters: true,
strict: true,
target: 'es2018',
},
});

t.deepEqual(omit(result[1], 'options.tsConfigPath'), {
files: [path.resolve(cwd, 'child/extra-semicolon.ts')],
options: {
semicolon: false,
cwd: path.resolve(cwd, 'child'),
extensions: DEFAULT_EXTENSION,
ignores: DEFAULT_IGNORES,
ts: true,
},
prettierOptions: {},
});

t.deepEqual(omit(result[2], 'options.tsConfigPath'), {
files: [path.resolve(cwd, 'child/sub-child/four-spaces.ts')],
options: {
space: 2,
cwd: path.resolve(cwd, 'child/sub-child'),
extensions: DEFAULT_EXTENSION,
ignores: DEFAULT_IGNORES,
ts: true,
},
prettierOptions: {},
});

// 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
t.is(result[1].options.tsConfigPath, result[2].options.tsConfigPath);
t.deepEqual(await readJson(result[1].options.tsConfigPath), {
extends: path.resolve(cwd, 'child/tsconfig.json'),
files: [path.resolve(cwd, 'child/extra-semicolon.ts'), path.resolve(cwd, 'child/sub-child/four-spaces.ts')],
include: [
slash(path.resolve(cwd, 'child/**/*.ts')),
slash(path.resolve(cwd, 'child/**/*.tsx')),
],
});

const secondResult = await manager.mergeWithFileConfigs(paths, {cwd}, configFiles);

// Verify that on each run the options.tsConfigPath is consistent to preserve ESLint cache
t.is(result[0].options.tsConfigPath, secondResult[0].options.tsConfigPath);
t.is(result[1].options.tsConfigPath, secondResult[1].options.tsConfigPath);
});

test('applyOverrides', t => {
t.deepEqual(
manager.applyOverrides(
Expand Down

0 comments on commit dcb76f3

Please sign in to comment.