From 3d10431958e334a2385754dc2e21292d5f4ee791 Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Sat, 7 Aug 2021 06:51:09 +0800 Subject: [PATCH] Revert #584 memory regression +Refactor (#585) +Correctly use `ignores` config, Lint `xo` with `xo` (#584) +Simplify `lintFiles` (#583) --- index.js | 183 +++++------------ lib/options-manager.js | 125 ++++-------- lib/report.js | 84 ++++++++ package.json | 20 +- test/fixtures/nested-ignores/a.js | 1 + test/fixtures/nested-ignores/b.js | 1 + test/fixtures/nested-ignores/child/a.js | 1 + test/fixtures/nested-ignores/child/b.js | 1 + .../nested-ignores/child/package.json | 8 + test/fixtures/nested-ignores/package.json | 8 + test/ignores.js | 40 ++++ test/options-manager.js | 185 ++---------------- 12 files changed, 256 insertions(+), 401 deletions(-) create mode 100644 lib/report.js create mode 100644 test/fixtures/nested-ignores/a.js create mode 100644 test/fixtures/nested-ignores/b.js create mode 100644 test/fixtures/nested-ignores/child/a.js create mode 100644 test/fixtures/nested-ignores/child/b.js create mode 100644 test/fixtures/nested-ignores/child/package.json create mode 100644 test/fixtures/nested-ignores/package.json create mode 100644 test/ignores.js diff --git a/index.js b/index.js index 538f6b35..2675f9d1 100644 --- a/index.js +++ b/index.js @@ -1,169 +1,92 @@ -import process from 'node:process'; import path from 'node:path'; import {ESLint} from 'eslint'; import {globby, isGitIgnoredSync} from 'globby'; 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, + parseOptions, getIgnores, mergeWithFileConfig, - mergeWithFileConfigs, - buildConfig, - mergeOptions, } from './lib/options-manager.js'; - -/** Merge multiple reports into a single report */ -const mergeReports = reports => { - const report = { - results: [], - errorCount: 0, - warningCount: 0, - }; - - for (const currentReport of reports) { - report.results.push(...currentReport.results); - report.errorCount += currentReport.errorCount; - report.warningCount += currentReport.warningCount; +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); } - return report; -}; + const eslint = new ESLint(eslintOptions); -const getReportStatistics = results => { - const statistics = { - errorCount: 0, - warningCount: 0, - fixableErrorCount: 0, - fixableWarningCount: 0, - }; - - for (const result of results) { - statistics.errorCount += result.errorCount; - statistics.warningCount += result.warningCount; - statistics.fixableErrorCount += result.fixableErrorCount; - statistics.fixableWarningCount += result.fixableWarningCount; + if (filePath && await eslint.isPathIgnored(filePath)) { + return getIgnoredReport(filePath); } - return statistics; + const report = await lint(eslint); + return processReport(report, {isQuiet}); }; -const processReport = (report, {isQuiet = false} = {}) => { - if (isQuiet) { - report = ESLint.getErrorResults(report); - } - - const result = { - results: report, - ...getReportStatistics(report), - }; +const globFiles = async (patterns, options) => { + const {ignores, extensions, cwd} = (await mergeWithFileConfig(options)).options; - defineLazyProperty(result, 'usedDeprecatedRules', () => { - const seenRules = new Set(); - const rules = []; - - for (const {usedDeprecatedRules} of report) { - for (const rule of usedDeprecatedRules) { - if (seenRules.has(rule.ruleId)) { - continue; - } - - seenRules.add(rule.ruleId); - rules.push(rule); - } - } - - return rules; - }); - - return result; -}; + patterns = patterns.length === 0 + ? [`**/*.{${extensions.join(',')}}`] + : arrify(patterns).map(pattern => slash(pattern)); -const runEslint = async (paths, options, processorOptions) => { - const engine = new ESLint(options); + const files = await globby( + patterns, + {ignore: ignores, gitignore: true, absolute: true, cwd}, + ); - const report = await engine.lintFiles(await pFilter(paths, async path => !(await engine.isPathIgnored(path)))); - return processReport(report, processorOptions); + return files.filter(file => extensions.includes(path.extname(file).slice(1))); }; -const globFiles = async (patterns, {ignores, extensions, cwd}) => ( - await globby( - patterns.length === 0 ? [`**/*.{${extensions.join(',')}}`] : arrify(patterns).map(pattern => slash(pattern)), - {ignore: ignores, gitignore: true, absolute: true, cwd}, - )).filter(file => extensions.includes(path.extname(file).slice(1))); - const getConfig = async options => { - const {options: foundOptions, prettierOptions} = mergeWithFileConfig(normalizeOptions(options)); - const {filePath, warnIgnored, ...eslintOptions} = buildConfig(foundOptions, prettierOptions); + const {filePath, eslintOptions} = await parseOptions(options); const engine = new ESLint(eslintOptions); return engine.calculateConfigForFile(filePath); }; -const lintText = async (string, inputOptions = {}) => { - const {options: foundOptions, prettierOptions} = mergeWithFileConfig(normalizeOptions(inputOptions)); - const options = buildConfig(foundOptions, prettierOptions); +const lintText = async (string, options) => { + options = await parseOptions(options); + const {filePath, warnIgnored, eslintOptions} = options; + const {ignorePatterns} = eslintOptions.baseConfig; - if (options.baseConfig.ignorePatterns && !isEqual(getIgnores({}), options.baseConfig.ignorePatterns) && typeof options.filePath !== 'string') { + if (typeof filePath !== 'string' && !isEqual(getIgnores({}), ignorePatterns)) { throw new Error('The `ignores` option requires the `filePath` option to be defined.'); } - const {filePath, warnIgnored, ...eslintOptions} = options; - const engine = new ESLint(eslintOptions); + return runEslint( + eslint => eslint.lintText(string, {filePath, warnIgnored}), + options, + ); +}; - if (filePath) { - const filename = path.relative(options.cwd, filePath); - - if ( - micromatch.isMatch(filename, options.baseConfig.ignorePatterns) - || isGitIgnoredSync({cwd: options.cwd, ignore: options.baseConfig.ignorePatterns})(filePath) - || await engine.isPathIgnored(filePath) - ) { - return { - errorCount: 0, - warningCount: 0, - results: [{ - errorCount: 0, - filePath: filename, - messages: [], - warningCount: 0, - }], - }; - } - } +const lintFile = async (filePath, options) => runEslint( + eslint => eslint.lintFiles([filePath]), + await parseOptions({...options, filePath}), +); - const report = await engine.lintText(string, {filePath, warnIgnored}); +const lintFiles = async (patterns, options) => { + const files = await globFiles(patterns, options); - return processReport(report, {isQuiet: inputOptions.quiet}); -}; + const reports = await Promise.all( + files.map(filePath => lintFile(filePath, options)), + ); -const lintFiles = async (patterns, inputOptions = {}) => { - inputOptions.cwd = path.resolve(inputOptions.cwd || process.cwd()); - 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 report = mergeReports(reports.filter(({isIgnored}) => !isIgnored)); + + return report; }; const getFormatter = async name => { diff --git a/lib/options-manager.js b/lib/options-manager.js index 06f0cc4c..febd3dc9 100644 --- a/lib/options-manager.js +++ b/lib/options-manager.js @@ -3,20 +3,18 @@ import os from 'node:os'; import path from 'node:path'; import fsExtra from 'fs-extra'; import arrify from 'arrify'; -import {mergeWith, groupBy, flow, pick} from 'lodash-es'; +import {mergeWith, flow, pick} from 'lodash-es'; import pathExists from 'path-exists'; import findUp from 'find-up'; import findCacheDir from 'find-cache-dir'; import prettier from 'prettier'; import semver from 'semver'; -import {cosmiconfig, cosmiconfigSync, defaultLoaders} from 'cosmiconfig'; -import pReduce from 'p-reduce'; +import {cosmiconfig, defaultLoaders} from 'cosmiconfig'; 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 eslintrc from '@eslint/eslintrc'; import createEsmUtils from 'esm-utils'; import { @@ -33,8 +31,6 @@ import { } from './constants.js'; const {__dirname, json, require} = createEsmUtils(import.meta); -const pkg = json.loadSync('../package.json'); -const {outputJson, outputJsonSync} = fsExtra; const {normalizePackageName} = eslintrc.Legacy.naming; const resolveModule = eslintrc.Legacy.ModuleResolver.resolve; @@ -105,18 +101,18 @@ const isTypescript = file => TYPESCRIPT_EXTENSION.includes(path.extname(file).sl Find config for `lintText`. The config files are searched starting from `options.filePath` if defined or `options.cwd` otherwise. */ -const mergeWithFileConfig = options => { +const mergeWithFileConfig = async options => { options.cwd = path.resolve(options.cwd || process.cwd()); - const configExplorer = cosmiconfigSync(MODULE_NAME, {searchPlaces: CONFIG_FILES, loaders: {noExt: defaultLoaders['.json']}, stopDir: options.cwd}); - const pkgConfigExplorer = cosmiconfigSync('engines', {searchPlaces: ['package.json'], stopDir: options.cwd}); + const configExplorer = cosmiconfig(MODULE_NAME, {searchPlaces: CONFIG_FILES, loaders: {noExt: defaultLoaders['.json']}, stopDir: options.cwd}); + const pkgConfigExplorer = cosmiconfig('engines', {searchPlaces: ['package.json'], stopDir: options.cwd}); if (options.filePath) { options.filePath = path.resolve(options.cwd, options.filePath); } const searchPath = options.filePath || options.cwd; - const {config: xoOptions, filepath: xoConfigPath} = configExplorer.search(searchPath) || {}; - const {config: enginesOptions} = pkgConfigExplorer.search(searchPath) || {}; + const {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; @@ -125,91 +121,31 @@ const mergeWithFileConfig = options => { ({options} = applyOverrides(options.filePath, options)); } - const prettierOptions = options.prettier ? prettier.resolveConfig.sync(searchPath, {editorconfig: true}) || {} : {}; + const prettierOptions = options.prettier ? await prettier.resolveConfig(searchPath, {editorconfig: true}) || {} : {}; if (options.filePath && isTypescript(options.filePath)) { - const tsConfigExplorer = cosmiconfigSync([], {searchPlaces: ['tsconfig.json'], loaders: {'.json': (_, content) => JSON5.parse(content)}}); - const {config: tsConfig, filepath: tsConfigPath} = tsConfigExplorer.search(options.filePath) || {}; + const tsConfigExplorer = cosmiconfig([], {searchPlaces: ['tsconfig.json'], loaders: {'.json': (_, content) => JSON5.parse(content)}}); + const {config: tsConfig, filepath: tsConfigPath} = (await tsConfigExplorer.search(options.filePath)) || {}; - options.tsConfigPath = getTsConfigCachePath([options.filePath], options.tsConfigPath, options.cwd); + options.tsConfigPath = await getTsConfigCachePath([options.filePath], options.tsConfigPath, options.cwd); options.ts = true; - outputJsonSync(options.tsConfigPath, makeTSConfig(tsConfig, tsConfigPath, [options.filePath])); + await fsExtra.outputJson(options.tsConfigPath, makeTSConfig(tsConfig, tsConfigPath, [options.filePath])); } 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 */ -const getTsConfigCachePath = (files, tsConfigPath, cwd) => path.join( - cacheLocation(cwd), - `tsconfig.${murmur(`${pkg.version}_${nodeVersion}_${stringify({files: files.sort(), tsConfigPath})}`).result().toString(36)}.json`, -); +const getTsConfigCachePath = async (files, tsConfigPath, cwd) => { + const {version} = await json.load('../package.json'); + return path.join( + cacheLocation(cwd), + `tsconfig.${murmur(`${version}_${nodeVersion}_${stringify({files: files.sort(), tsConfigPath})}`).result().toString(36)}.json`, + ); +}; const makeTSConfig = (tsConfig, tsConfigPath, files) => { const config = {files: files.filter(file => isTypescript(file))}; @@ -604,14 +540,27 @@ const gatherImportResolvers = options => { return resolvers; }; +const parseOptions = async options => { + options = normalizeOptions(options); + const {options: foundOptions, prettierOptions} = await mergeWithFileConfig(options); + const {filePath, warnIgnored, ...eslintOptions} = buildConfig(foundOptions, prettierOptions); + return { + filePath, + warnIgnored, + isQuiet: options.quiet, + eslintOptions, + }; +}; + export { + parseOptions, + getIgnores, + mergeWithFileConfig, + + // For tests + applyOverrides, findApplicableOverrides, mergeWithPrettierConfig, normalizeOptions, - getIgnores, - mergeWithFileConfigs, - mergeWithFileConfig, buildConfig, - applyOverrides, - mergeOptions, }; diff --git a/lib/report.js b/lib/report.js new file mode 100644 index 00000000..f6daf0fa --- /dev/null +++ b/lib/report.js @@ -0,0 +1,84 @@ +import defineLazyProperty from 'define-lazy-prop'; +import {ESLint} from 'eslint'; + +/** Merge multiple reports into a single report */ +const mergeReports = reports => { + const report = { + results: [], + errorCount: 0, + warningCount: 0, + }; + + for (const currentReport of reports) { + report.results.push(...currentReport.results); + report.errorCount += currentReport.errorCount; + report.warningCount += currentReport.warningCount; + } + + return report; +}; + +const processReport = (report, {isQuiet = false} = {}) => { + if (isQuiet) { + report = ESLint.getErrorResults(report); + } + + const result = { + results: report, + ...getReportStatistics(report), + }; + + defineLazyProperty(result, 'usedDeprecatedRules', () => { + const seenRules = new Set(); + const rules = []; + + for (const {usedDeprecatedRules} of report) { + for (const rule of usedDeprecatedRules) { + if (seenRules.has(rule.ruleId)) { + continue; + } + + seenRules.add(rule.ruleId); + rules.push(rule); + } + } + + return rules; + }); + + return result; +}; + +const getReportStatistics = results => { + const statistics = { + errorCount: 0, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + }; + + for (const result of results) { + statistics.errorCount += result.errorCount; + statistics.warningCount += result.warningCount; + statistics.fixableErrorCount += result.fixableErrorCount; + statistics.fixableWarningCount += result.fixableWarningCount; + } + + return statistics; +}; + +const getIgnoredReport = filePath => ({ + errorCount: 0, + warningCount: 0, + results: [ + { + errorCount: 0, + warningCount: 0, + filePath, + messages: [], + }, + ], + isIgnored: true, +}); + +export {mergeReports, processReport, getIgnoredReport}; diff --git a/package.json b/package.json index 89bc7d6d..725da3a9 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "node": ">=12.20" }, "scripts": { - "test": "eslint --quiet . --ext .js,.cjs && nyc ava" + "test": "node cli.js && nyc ava" }, "files": [ "config", @@ -87,9 +87,6 @@ "meow": "^10.1.1", "micromatch": "^4.0.4", "open-editor": "^3.0.0", - "p-filter": "^2.1.0", - "p-map": "^5.1.0", - "p-reduce": "^3.0.0", "path-exists": "^4.0.0", "prettier": "^2.3.2", "semver": "^7.3.5", @@ -108,18 +105,13 @@ "temp-write": "^5.0.0", "webpack": "^5.48.0" }, - "eslintConfig": { - "extends": [ - "eslint-config-xo", - "./config/plugins.cjs", - "./config/overrides.cjs" + "xo": { + "ignores": [ + "test/fixtures", + "test/temp", + "coverage" ] }, - "eslintIgnore": [ - "test/fixtures", - "test/temp", - "coverage" - ], "ava": { "files": [ "!test/temp" diff --git a/test/fixtures/nested-ignores/a.js b/test/fixtures/nested-ignores/a.js new file mode 100644 index 00000000..b00bfec5 --- /dev/null +++ b/test/fixtures/nested-ignores/a.js @@ -0,0 +1 @@ +console.log('semicolon') diff --git a/test/fixtures/nested-ignores/b.js b/test/fixtures/nested-ignores/b.js new file mode 100644 index 00000000..b00bfec5 --- /dev/null +++ b/test/fixtures/nested-ignores/b.js @@ -0,0 +1 @@ +console.log('semicolon') diff --git a/test/fixtures/nested-ignores/child/a.js b/test/fixtures/nested-ignores/child/a.js new file mode 100644 index 00000000..b00bfec5 --- /dev/null +++ b/test/fixtures/nested-ignores/child/a.js @@ -0,0 +1 @@ +console.log('semicolon') diff --git a/test/fixtures/nested-ignores/child/b.js b/test/fixtures/nested-ignores/child/b.js new file mode 100644 index 00000000..b00bfec5 --- /dev/null +++ b/test/fixtures/nested-ignores/child/b.js @@ -0,0 +1 @@ +console.log('semicolon') diff --git a/test/fixtures/nested-ignores/child/package.json b/test/fixtures/nested-ignores/child/package.json new file mode 100644 index 00000000..f0a313fe --- /dev/null +++ b/test/fixtures/nested-ignores/child/package.json @@ -0,0 +1,8 @@ +{ + "xo": { + "ignores": [ + "b.js", + "**/b.js" + ] + } +} diff --git a/test/fixtures/nested-ignores/package.json b/test/fixtures/nested-ignores/package.json new file mode 100644 index 00000000..f852068c --- /dev/null +++ b/test/fixtures/nested-ignores/package.json @@ -0,0 +1,8 @@ +{ + "xo": { + "ignores": [ + "a.js", + "**/a.js" + ] + } +} diff --git a/test/ignores.js b/test/ignores.js new file mode 100644 index 00000000..d6f98bb9 --- /dev/null +++ b/test/ignores.js @@ -0,0 +1,40 @@ +import path from 'node:path'; +import test from 'ava'; +import createEsmUtils from 'esm-utils'; +import {globby} from 'globby'; +import xo from '../index.js'; + +const {__dirname} = createEsmUtils(import.meta); + +test('Should pickup "ignores" config', async t => { + const cwd = path.join(__dirname, 'fixtures/nested-ignores'); + + t.deepEqual( + await globby(['**/*.js'], {cwd}), + ['a.js', 'b.js', 'child/a.js', 'child/b.js'], + 'Should has 4 js files.', + ); + + // Should not match + // `a.js` (ignored by config in current directory) + // `child/a.js` (ignored by config in current directory) + // `child/b.js` (ignored by config in child directory) + const result = await xo.lintFiles('.', {cwd}); + const files = result.results.map(({filePath}) => filePath); + t.deepEqual(files, [path.join(cwd, 'b.js')], 'Should only report on `b.js`.'); +}); + +test('Should ignore "ignores" config in parent', async t => { + const cwd = path.join(__dirname, 'fixtures/nested-ignores/child'); + + t.deepEqual( + await globby(['**/*.js'], {cwd}), + ['a.js', 'b.js'], + 'Should has 2 js files.', + ); + + // Should only match `a.js` even it's ignored in parent + const result = await xo.lintFiles('.', {cwd}); + const files = result.results.map(({filePath}) => filePath); + t.deepEqual(files, [path.join(cwd, 'a.js')], 'Should only report on `a.js`.'); +}); diff --git a/test/options-manager.js b/test/options-manager.js index 8c6eb50e..a761756c 100644 --- a/test/options-manager.js +++ b/test/options-manager.js @@ -493,51 +493,51 @@ test('findApplicableOverrides', t => { ]); }); -test('mergeWithFileConfig: use child if closest', t => { +test('mergeWithFileConfig: use child if closest', async t => { const cwd = path.resolve('fixtures', 'nested', 'child'); - const {options} = manager.mergeWithFileConfig({cwd}); + const {options} = await manager.mergeWithFileConfig({cwd}); const expected = {...childConfig.xo, extensions: DEFAULT_EXTENSION, ignores: DEFAULT_IGNORES, cwd}; t.deepEqual(options, expected); }); -test('mergeWithFileConfig: use parent if closest', t => { +test('mergeWithFileConfig: use parent if closest', async t => { const cwd = path.resolve('fixtures', 'nested'); - const {options} = manager.mergeWithFileConfig({cwd}); + const {options} = await manager.mergeWithFileConfig({cwd}); const expected = {...parentConfig.xo, extensions: DEFAULT_EXTENSION, ignores: DEFAULT_IGNORES, cwd}; t.deepEqual(options, expected); }); -test('mergeWithFileConfig: use parent if child is ignored', t => { +test('mergeWithFileConfig: use parent if child is ignored', async t => { const cwd = path.resolve('fixtures', 'nested'); const filePath = path.resolve(cwd, 'child-ignore', 'file.js'); - const {options} = manager.mergeWithFileConfig({cwd, filePath}); + const {options} = await manager.mergeWithFileConfig({cwd, filePath}); const expected = {...parentConfig.xo, extensions: DEFAULT_EXTENSION, ignores: DEFAULT_IGNORES, cwd, filePath}; t.deepEqual(options, expected); }); -test('mergeWithFileConfig: use child if child is empty', t => { +test('mergeWithFileConfig: use child if child is empty', async t => { const cwd = path.resolve('fixtures', 'nested', 'child-empty'); - const {options} = manager.mergeWithFileConfig({cwd}); + const {options} = await manager.mergeWithFileConfig({cwd}); t.deepEqual(options, {extensions: DEFAULT_EXTENSION, ignores: DEFAULT_IGNORES, cwd}); }); -test('mergeWithFileConfig: read engines from package.json', t => { +test('mergeWithFileConfig: read engines from package.json', async t => { const cwd = path.resolve('fixtures', 'engines'); - const {options} = manager.mergeWithFileConfig({cwd}); + const {options} = await manager.mergeWithFileConfig({cwd}); const expected = {nodeVersion: enginesConfig.engines.node, extensions: DEFAULT_EXTENSION, ignores: DEFAULT_IGNORES, cwd}; t.deepEqual(options, expected); }); -test('mergeWithFileConfig: XO engine options supersede package.json\'s', t => { +test('mergeWithFileConfig: XO engine options supersede package.json\'s', async t => { const cwd = path.resolve('fixtures', 'engines'); - const {options} = manager.mergeWithFileConfig({cwd, nodeVersion: '>=8'}); + const {options} = await manager.mergeWithFileConfig({cwd, nodeVersion: '>=8'}); const expected = {nodeVersion: '>=8', extensions: DEFAULT_EXTENSION, ignores: DEFAULT_IGNORES, cwd}; t.deepEqual(options, expected); }); -test('mergeWithFileConfig: XO engine options false supersede package.json\'s', t => { +test('mergeWithFileConfig: XO engine options false supersede package.json\'s', async t => { const cwd = path.resolve('fixtures', 'engines'); - const {options} = manager.mergeWithFileConfig({cwd, nodeVersion: false}); + const {options} = await manager.mergeWithFileConfig({cwd, nodeVersion: false}); const expected = {nodeVersion: false, extensions: DEFAULT_EXTENSION, ignores: DEFAULT_IGNORES, cwd}; t.deepEqual(options, expected); }); @@ -545,7 +545,7 @@ test('mergeWithFileConfig: XO engine options false supersede package.json\'s', t test('mergeWithFileConfig: typescript files', async t => { const cwd = path.resolve('fixtures', 'typescript', 'child'); const filePath = path.resolve(cwd, 'file.ts'); - const {options} = manager.mergeWithFileConfig({cwd, filePath}); + const {options} = await manager.mergeWithFileConfig({cwd, filePath}); const expected = { filePath, extensions: DEFAULT_EXTENSION, @@ -567,7 +567,7 @@ test('mergeWithFileConfig: typescript files', async t => { test('mergeWithFileConfig: tsx files', async t => { const cwd = path.resolve('fixtures', 'typescript', 'child'); const filePath = path.resolve(cwd, 'file.tsx'); - const {options} = manager.mergeWithFileConfig({cwd, filePath}); + const {options} = await manager.mergeWithFileConfig({cwd, filePath}); const expected = { filePath, extensions: DEFAULT_EXTENSION, @@ -586,159 +586,6 @@ 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(