From a1041c1928230af147a1094ef5496affc04a82cc Mon Sep 17 00:00:00 2001 From: Pierre Vanduynslager Date: Thu, 27 Feb 2020 22:51:04 -0500 Subject: [PATCH] Get `extensions` and `ignores` from config files --- index.js | 50 +++++++++----- lib/options-manager.js | 30 ++++---- package.json | 1 + test/fixtures/config-files/xo-config/file.js | 2 + .../config-files/xo-config_js/file.js | 2 + .../config-files/xo-config_json/file.js | 2 + .../config-files/xo_config_js/file.js | 2 + test/lint-files.js | 19 ++++++ test/lint-text.js | 12 ++++ test/options-manager.js | 68 ++++++------------- 10 files changed, 105 insertions(+), 83 deletions(-) create mode 100644 test/fixtures/config-files/xo-config/file.js create mode 100644 test/fixtures/config-files/xo-config_js/file.js create mode 100644 test/fixtures/config-files/xo-config_json/file.js create mode 100644 test/fixtures/config-files/xo_config_js/file.js diff --git a/index.js b/index.js index 55d06ced..0839ae28 100644 --- a/index.js +++ b/index.js @@ -3,15 +3,19 @@ const path = require('path'); const eslint = require('eslint'); const globby = require('globby'); const isEqual = require('lodash/isEqual'); +const uniq = require('lodash/uniq'); const micromatch = require('micromatch'); const arrify = require('arrify'); -const {DEFAULT_EXTENSION} = require('./lib/constants'); +const pReduce = require('p-reduce'); +const {cosmiconfig, defaultLoaders} = require('cosmiconfig'); +const {CONFIG_FILES, MODULE_NAME, DEFAULT_IGNORES} = require('./lib/constants'); const { normalizeOptions, getIgnores, mergeWithFileConfig, mergeWithFileConfigs, - buildConfig + buildConfig, + mergeOptions } = require('./lib/options-manager'); const mergeReports = reports => { @@ -47,6 +51,12 @@ const runEslint = (paths, options) => { return processReport(report, options); }; +const globFiles = async (patterns, {ignores, extensions, cwd}) => ( + await globby( + patterns.length === 0 ? [`**/*.{${extensions.join(',')}}`] : arrify(patterns), + {ignore: ignores, gitignore: true, cwd} + )).filter(file => extensions.includes(path.extname(file).slice(1))).map(file => path.resolve(cwd, file)); + const lintText = (string, options) => { const {options: foundOptions, prettierOptions} = mergeWithFileConfig(normalizeOptions(options)); options = buildConfig(foundOptions, prettierOptions); @@ -83,22 +93,26 @@ const lintText = (string, options) => { return processReport(report, options); }; -const lintFiles = async (patterns, options) => { - options = normalizeOptions(options); - - const isEmptyPatterns = patterns.length === 0; - const defaultPattern = `**/*.{${DEFAULT_EXTENSION.concat(options.extensions || []).join(',')}}`; - - const paths = await globby( - isEmptyPatterns ? [defaultPattern] : arrify(patterns), - { - ignore: getIgnores(options), - gitignore: true, - cwd: options.cwd || process.cwd() - } - ); - - return mergeReports((await mergeWithFileConfigs(paths, options)).map( +const lintFiles = async (patterns, options = {}) => { + options.cwd = path.resolve(options.cwd || process.cwd()); + const configExplorer = cosmiconfig(MODULE_NAME, {searchPlaces: CONFIG_FILES, loaders: {noExt: defaultLoaders['.json']}, stopDir: options.cwd}); + + const configFiles = (await Promise.all( + (await globby( + CONFIG_FILES.map(configFile => `**/${configFile}`), + {ignore: DEFAULT_IGNORES, gitignore: true, cwd: options.cwd} + )).map(async configFile => configExplorer.load(path.resolve(options.cwd, configFile))) + )).filter(Boolean); + + const paths = configFiles.length > 0 ? + await pReduce( + configFiles, + async (paths, {filepath, config}) => + [...paths, ...(await globFiles(patterns, {...mergeOptions(options, config), cwd: path.dirname(filepath)}))], + []) : + await globFiles(patterns, mergeOptions(options)); + + return mergeReports((await mergeWithFileConfigs(uniq(paths), options, configFiles)).map( ({files, options, prettierOptions}) => runEslint(files, buildConfig(options, prettierOptions))) ); }; diff --git a/lib/options-manager.js b/lib/options-manager.js index 49e00d6f..686b052f 100644 --- a/lib/options-manager.js +++ b/lib/options-manager.js @@ -19,6 +19,7 @@ const JSON5 = require('json5'); const toAbsoluteGlob = require('to-absolute-glob'); const stringify = require('json-stable-stringify-without-jsonify'); const murmur = require('imurmurhash'); +const isPathInside = require('is-path-inside'); const { DEFAULT_IGNORES, DEFAULT_EXTENSION, @@ -89,7 +90,7 @@ const mergeWithFileConfig = options => { const {config: xoOptions, filepath: xoConfigPath} = configExplorer.search(searchPath) || {}; const {config: enginesOptions} = pkgConfigExplorer.search(searchPath) || {}; - options = mergeOptions(xoOptions, enginesOptions, options); + options = mergeOptions(options, xoOptions, enginesOptions); options.cwd = xoConfigPath && path.dirname(xoConfigPath) !== options.cwd ? path.resolve(options.cwd, path.dirname(xoConfigPath)) : options.cwd; if (options.filename) { @@ -114,26 +115,19 @@ const mergeWithFileConfig = options => { Find config for each files found by `lintFiles`. The config files are searched starting from each files. */ -const mergeWithFileConfigs = async (files, options) => { - options.cwd = path.resolve(options.cwd || process.cwd()); - +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.map(file => path.resolve(options.cwd, file)), async (configs, file) => { - const configExplorer = cosmiconfig(MODULE_NAME, {searchPlaces: CONFIG_FILES, loaders: {noExt: defaultLoaders['.json']}, stopDir: options.cwd}); + const groups = [...(await pReduce(files, async (configs, file) => { const pkgConfigExplorer = cosmiconfig('engines', {searchPlaces: ['package.json'], stopDir: options.cwd}); - const {config: xoOptions, filepath: xoConfigPath} = await configExplorer.search(file) || {}; + const {config: xoOptions, filepath: xoConfigPath} = findApplicableConfig(file, configFiles) || {}; const {config: enginesOptions, filepath: enginesConfigPath} = await pkgConfigExplorer.search(file) || {}; - let fileOptions = mergeOptions(xoOptions, enginesOptions, options); + let fileOptions = mergeOptions(options, xoOptions, enginesOptions); fileOptions.cwd = xoConfigPath && path.dirname(xoConfigPath) !== fileOptions.cwd ? path.resolve(fileOptions.cwd, path.dirname(xoConfigPath)) : fileOptions.cwd; - if (!fileOptions.extensions.includes(path.extname(file).replace('.', '')) || isFileIgnored(file, fileOptions)) { - // File extension/path is ignored, skip it - return configs; - } - const {hash, options: optionsWithOverrides} = applyOverrides(file, fileOptions); fileOptions = optionsWithOverrides; @@ -143,7 +137,6 @@ const mergeWithFileConfigs = async (files, options) => { let tsConfigPath; if (isTypescript(file)) { let tsConfig; - // Override cosmiconfig `loaders` as we look only for the path of tsconfig.json, but not its content const tsConfigExplorer = cosmiconfig([], {searchPlaces: ['tsconfig.json'], loaders: {'.json': (_, content) => JSON5.parse(content)}}); ({config: tsConfig, filepath: tsConfigPath} = await tsConfigExplorer.search(file) || {}); @@ -178,6 +171,8 @@ const mergeWithFileConfigs = async (files, options) => { 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 @@ -237,12 +232,10 @@ const normalizeOptions = options => { const normalizeSpaces = options => typeof options.space === 'number' ? options.space : 2; -const isFileIgnored = (file, options) => micromatch.isMatch(path.relative(options.cwd, file), options.ignores); - /** Merge option passed via CLI/API via options founf in config files. */ -const mergeOptions = (xoOptions, enginesOptions, options) => { +const mergeOptions = (options, xoOptions = {}, enginesOptions = {}) => { const mergedOptions = normalizeOptions({ ...xoOptions, nodeVersion: enginesOptions && enginesOptions.node && semver.validRange(enginesOptions.node), @@ -491,5 +484,6 @@ module.exports = { mergeWithFileConfigs, mergeWithFileConfig, buildConfig, - applyOverrides + applyOverrides, + mergeOptions }; diff --git a/package.json b/package.json index fd0413ee..0fec57d1 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "globby": "^9.0.0", "has-flag": "^4.0.0", "imurmurhash": "^0.1.4", + "is-path-inside": "^3.0.2", "json-stable-stringify-without-jsonify": "^1.0.1", "json5": "^2.1.1", "lodash": "^4.17.15", diff --git a/test/fixtures/config-files/xo-config/file.js b/test/fixtures/config-files/xo-config/file.js new file mode 100644 index 00000000..ad7d1b96 --- /dev/null +++ b/test/fixtures/config-files/xo-config/file.js @@ -0,0 +1,2 @@ +var obj = { a: 1 }; +console.log(obj.a); diff --git a/test/fixtures/config-files/xo-config_js/file.js b/test/fixtures/config-files/xo-config_js/file.js new file mode 100644 index 00000000..ad7d1b96 --- /dev/null +++ b/test/fixtures/config-files/xo-config_js/file.js @@ -0,0 +1,2 @@ +var obj = { a: 1 }; +console.log(obj.a); diff --git a/test/fixtures/config-files/xo-config_json/file.js b/test/fixtures/config-files/xo-config_json/file.js new file mode 100644 index 00000000..ad7d1b96 --- /dev/null +++ b/test/fixtures/config-files/xo-config_json/file.js @@ -0,0 +1,2 @@ +var obj = { a: 1 }; +console.log(obj.a); diff --git a/test/fixtures/config-files/xo_config_js/file.js b/test/fixtures/config-files/xo_config_js/file.js new file mode 100644 index 00000000..ad7d1b96 --- /dev/null +++ b/test/fixtures/config-files/xo_config_js/file.js @@ -0,0 +1,2 @@ +var obj = { a: 1 }; +console.log(obj.a); diff --git a/test/lint-files.js b/test/lint-files.js index 8b7dfac3..e7cdb10b 100644 --- a/test/lint-files.js +++ b/test/lint-files.js @@ -195,3 +195,22 @@ test('typescript files', async t => { ) ); }); + +async function configType(t, {dir}) { + const {results} = await fn.lintFiles('**/*', {cwd: path.resolve('fixtures', 'config-files', dir)}); + + t.true( + hasRule( + results, + path.resolve('fixtures', 'config-files', dir, 'file.js'), + 'no-var' + ) + ); +} + +configType.title = (_, {type}) => `load config from ${type}`.trim(); + +test(configType, {type: 'xo.config.js', dir: 'xo-config_js'}); +test(configType, {type: '.xo-config.js', dir: 'xo-config_js'}); +test(configType, {type: '.xo-config.json', dir: 'xo-config_json'}); +test(configType, {type: '.xo-config', dir: 'xo-config'}); diff --git a/test/lint-text.js b/test/lint-text.js index c0458285..1014ae21 100644 --- a/test/lint-text.js +++ b/test/lint-text.js @@ -282,3 +282,15 @@ test('typescript files', t => { ]);`, {filename: 'fixtures/typescript/child/sub-child/four-spaces.ts'})); t.true(hasRule(results, '@typescript-eslint/indent')); }); + +function configType(t, {dir}) { + const {results} = fn.lintText('var obj = { a: 1 };\n', {cwd: path.resolve('fixtures', 'config-files', dir), filename: 'file.js'}); + t.true(hasRule(results, 'no-var')); +} + +configType.title = (_, {type}) => `load config from ${type}`.trim(); + +test(configType, {type: 'xo.config.js', dir: 'xo-config_js'}); +test(configType, {type: '.xo-config.js', dir: 'xo-config_js'}); +test(configType, {type: '.xo-config.json', dir: 'xo-config_json'}); +test(configType, {type: '.xo-config', dir: 'xo-config'}); diff --git a/test/options-manager.js b/test/options-manager.js index 61e760fd..e357587a 100644 --- a/test/options-manager.js +++ b/test/options-manager.js @@ -521,20 +521,6 @@ test('mergeWithFileConfig: tsx files', async t => { }); }); -function mergeWithFileConfigFileType(t, {dir}) { - const cwd = path.resolve('fixtures', 'config-files', dir); - const {options} = manager.mergeWithFileConfig({cwd}); - const expected = {esnext: true, extensions: DEFAULT_EXTENSION, ignores: DEFAULT_IGNORES, cwd, nodeVersion: undefined}; - t.deepEqual(options, expected); -} - -mergeWithFileConfigFileType.title = (_, {type}) => `mergeWithFileConfig: load from ${type}`.trim(); - -test(mergeWithFileConfigFileType, {type: 'xo.config.js', dir: 'xo-config_js'}); -test(mergeWithFileConfigFileType, {type: '.xo-config.js', dir: 'xo-config_js'}); -test(mergeWithFileConfigFileType, {type: '.xo-config.json', dir: 'xo-config_json'}); -test(mergeWithFileConfigFileType, {type: '.xo-config', dir: 'xo-config'}); - test('mergeWithFileConfigs: nested configs with prettier', async t => { const cwd = path.resolve('fixtures', 'nested-configs'); const paths = [ @@ -542,8 +528,19 @@ test('mergeWithFileConfigs: nested configs with prettier', async t => { 'child/semicolon.js', 'child-override/two-spaces.js', 'child-override/child-prettier-override/semicolon.js' - ]; - const result = await manager.mergeWithFileConfigs(paths, {cwd}); + ].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, [ { @@ -607,8 +604,13 @@ test('mergeWithFileConfigs: nested configs with prettier', async t => { 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']; - const result = await manager.mergeWithFileConfigs(paths, {cwd}); + 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')], @@ -672,41 +674,13 @@ test('mergeWithFileConfigs: typescript files', async t => { ] }); - const secondResult = await manager.mergeWithFileConfigs(paths, {cwd}); + 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); }); -async function mergeWithFileConfigsFileType(t, {dir}) { - const cwd = path.resolve('fixtures', 'config-files', dir); - const paths = ['a.js', 'b.js']; - - const result = await manager.mergeWithFileConfigs(paths, {cwd}); - - t.deepEqual(result, [ - { - files: paths.reverse().map(p => path.resolve(cwd, p)), - options: { - esnext: true, - nodeVersion: undefined, - cwd, - extensions: DEFAULT_EXTENSION, - ignores: DEFAULT_IGNORES - }, - prettierOptions: {} - } - ]); -} - -mergeWithFileConfigsFileType.title = (_, {type}) => `mergeWithFileConfigs: load from ${type}`.trim(); - -test(mergeWithFileConfigsFileType, {type: 'xo.config.js', dir: 'xo-config_js'}); -test(mergeWithFileConfigsFileType, {type: '.xo-config.js', dir: 'xo-config_js'}); -test(mergeWithFileConfigsFileType, {type: '.xo-config.json', dir: 'xo-config_json'}); -test(mergeWithFileConfigsFileType, {type: '.xo-config', dir: 'xo-config'}); - test('applyOverrides', t => { t.deepEqual( manager.applyOverrides(