Skip to content

Commit 0d63c64

Browse files
authored
Get extensions and ignores from config files (#436)
1 parent 6d25237 commit 0d63c64

File tree

10 files changed

+105
-83
lines changed

10 files changed

+105
-83
lines changed

index.js

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,19 @@ const path = require('path');
33
const eslint = require('eslint');
44
const globby = require('globby');
55
const isEqual = require('lodash/isEqual');
6+
const uniq = require('lodash/uniq');
67
const micromatch = require('micromatch');
78
const arrify = require('arrify');
8-
const {DEFAULT_EXTENSION} = require('./lib/constants');
9+
const pReduce = require('p-reduce');
10+
const {cosmiconfig, defaultLoaders} = require('cosmiconfig');
11+
const {CONFIG_FILES, MODULE_NAME, DEFAULT_IGNORES} = require('./lib/constants');
912
const {
1013
normalizeOptions,
1114
getIgnores,
1215
mergeWithFileConfig,
1316
mergeWithFileConfigs,
14-
buildConfig
17+
buildConfig,
18+
mergeOptions
1519
} = require('./lib/options-manager');
1620

1721
const mergeReports = reports => {
@@ -47,6 +51,12 @@ const runEslint = (paths, options) => {
4751
return processReport(report, options);
4852
};
4953

54+
const globFiles = async (patterns, {ignores, extensions, cwd}) => (
55+
await globby(
56+
patterns.length === 0 ? [`**/*.{${extensions.join(',')}}`] : arrify(patterns),
57+
{ignore: ignores, gitignore: true, cwd}
58+
)).filter(file => extensions.includes(path.extname(file).slice(1))).map(file => path.resolve(cwd, file));
59+
5060
const lintText = (string, options) => {
5161
const {options: foundOptions, prettierOptions} = mergeWithFileConfig(normalizeOptions(options));
5262
options = buildConfig(foundOptions, prettierOptions);
@@ -83,22 +93,26 @@ const lintText = (string, options) => {
8393
return processReport(report, options);
8494
};
8595

86-
const lintFiles = async (patterns, options) => {
87-
options = normalizeOptions(options);
88-
89-
const isEmptyPatterns = patterns.length === 0;
90-
const defaultPattern = `**/*.{${DEFAULT_EXTENSION.concat(options.extensions || []).join(',')}}`;
91-
92-
const paths = await globby(
93-
isEmptyPatterns ? [defaultPattern] : arrify(patterns),
94-
{
95-
ignore: getIgnores(options),
96-
gitignore: true,
97-
cwd: options.cwd || process.cwd()
98-
}
99-
);
100-
101-
return mergeReports((await mergeWithFileConfigs(paths, options)).map(
96+
const lintFiles = async (patterns, options = {}) => {
97+
options.cwd = path.resolve(options.cwd || process.cwd());
98+
const configExplorer = cosmiconfig(MODULE_NAME, {searchPlaces: CONFIG_FILES, loaders: {noExt: defaultLoaders['.json']}, stopDir: options.cwd});
99+
100+
const configFiles = (await Promise.all(
101+
(await globby(
102+
CONFIG_FILES.map(configFile => `**/${configFile}`),
103+
{ignore: DEFAULT_IGNORES, gitignore: true, cwd: options.cwd}
104+
)).map(async configFile => configExplorer.load(path.resolve(options.cwd, configFile)))
105+
)).filter(Boolean);
106+
107+
const paths = configFiles.length > 0 ?
108+
await pReduce(
109+
configFiles,
110+
async (paths, {filepath, config}) =>
111+
[...paths, ...(await globFiles(patterns, {...mergeOptions(options, config), cwd: path.dirname(filepath)}))],
112+
[]) :
113+
await globFiles(patterns, mergeOptions(options));
114+
115+
return mergeReports((await mergeWithFileConfigs(uniq(paths), options, configFiles)).map(
102116
({files, options, prettierOptions}) => runEslint(files, buildConfig(options, prettierOptions)))
103117
);
104118
};

lib/options-manager.js

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const JSON5 = require('json5');
1919
const toAbsoluteGlob = require('to-absolute-glob');
2020
const stringify = require('json-stable-stringify-without-jsonify');
2121
const murmur = require('imurmurhash');
22+
const isPathInside = require('is-path-inside');
2223
const {
2324
DEFAULT_IGNORES,
2425
DEFAULT_EXTENSION,
@@ -89,7 +90,7 @@ const mergeWithFileConfig = options => {
8990
const {config: xoOptions, filepath: xoConfigPath} = configExplorer.search(searchPath) || {};
9091
const {config: enginesOptions} = pkgConfigExplorer.search(searchPath) || {};
9192

92-
options = mergeOptions(xoOptions, enginesOptions, options);
93+
options = mergeOptions(options, xoOptions, enginesOptions);
9394
options.cwd = xoConfigPath && path.dirname(xoConfigPath) !== options.cwd ? path.resolve(options.cwd, path.dirname(xoConfigPath)) : options.cwd;
9495

9596
if (options.filename) {
@@ -114,26 +115,19 @@ const mergeWithFileConfig = options => {
114115
Find config for each files found by `lintFiles`.
115116
The config files are searched starting from each files.
116117
*/
117-
const mergeWithFileConfigs = async (files, options) => {
118-
options.cwd = path.resolve(options.cwd || process.cwd());
119-
118+
const mergeWithFileConfigs = async (files, options, configFiles) => {
119+
configFiles = configFiles.sort((a, b) => b.filepath.split(path.sep).length - a.filepath.split(path.sep).length);
120120
const tsConfigs = {};
121121

122-
const groups = [...(await pReduce(files.map(file => path.resolve(options.cwd, file)), async (configs, file) => {
123-
const configExplorer = cosmiconfig(MODULE_NAME, {searchPlaces: CONFIG_FILES, loaders: {noExt: defaultLoaders['.json']}, stopDir: options.cwd});
122+
const groups = [...(await pReduce(files, async (configs, file) => {
124123
const pkgConfigExplorer = cosmiconfig('engines', {searchPlaces: ['package.json'], stopDir: options.cwd});
125124

126-
const {config: xoOptions, filepath: xoConfigPath} = await configExplorer.search(file) || {};
125+
const {config: xoOptions, filepath: xoConfigPath} = findApplicableConfig(file, configFiles) || {};
127126
const {config: enginesOptions, filepath: enginesConfigPath} = await pkgConfigExplorer.search(file) || {};
128127

129-
let fileOptions = mergeOptions(xoOptions, enginesOptions, options);
128+
let fileOptions = mergeOptions(options, xoOptions, enginesOptions);
130129
fileOptions.cwd = xoConfigPath && path.dirname(xoConfigPath) !== fileOptions.cwd ? path.resolve(fileOptions.cwd, path.dirname(xoConfigPath)) : fileOptions.cwd;
131130

132-
if (!fileOptions.extensions.includes(path.extname(file).replace('.', '')) || isFileIgnored(file, fileOptions)) {
133-
// File extension/path is ignored, skip it
134-
return configs;
135-
}
136-
137131
const {hash, options: optionsWithOverrides} = applyOverrides(file, fileOptions);
138132
fileOptions = optionsWithOverrides;
139133

@@ -143,7 +137,6 @@ const mergeWithFileConfigs = async (files, options) => {
143137
let tsConfigPath;
144138
if (isTypescript(file)) {
145139
let tsConfig;
146-
// Override cosmiconfig `loaders` as we look only for the path of tsconfig.json, but not its content
147140
const tsConfigExplorer = cosmiconfig([], {searchPlaces: ['tsconfig.json'], loaders: {'.json': (_, content) => JSON5.parse(content)}});
148141
({config: tsConfig, filepath: tsConfigPath} = await tsConfigExplorer.search(file) || {});
149142

@@ -178,6 +171,8 @@ const mergeWithFileConfigs = async (files, options) => {
178171
return groups;
179172
};
180173

174+
const findApplicableConfig = (file, configFiles) => configFiles.find(({filepath}) => isPathInside(file, path.dirname(filepath)));
175+
181176
/**
182177
Generate a unique and consistent path for the temporary `tsconfig.json`.
183178
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 => {
237232

238233
const normalizeSpaces = options => typeof options.space === 'number' ? options.space : 2;
239234

240-
const isFileIgnored = (file, options) => micromatch.isMatch(path.relative(options.cwd, file), options.ignores);
241-
242235
/**
243236
Merge option passed via CLI/API via options founf in config files.
244237
*/
245-
const mergeOptions = (xoOptions, enginesOptions, options) => {
238+
const mergeOptions = (options, xoOptions = {}, enginesOptions = {}) => {
246239
const mergedOptions = normalizeOptions({
247240
...xoOptions,
248241
nodeVersion: enginesOptions && enginesOptions.node && semver.validRange(enginesOptions.node),
@@ -491,5 +484,6 @@ module.exports = {
491484
mergeWithFileConfigs,
492485
mergeWithFileConfig,
493486
buildConfig,
494-
applyOverrides
487+
applyOverrides,
488+
mergeOptions
495489
};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"globby": "^9.0.0",
7676
"has-flag": "^4.0.0",
7777
"imurmurhash": "^0.1.4",
78+
"is-path-inside": "^3.0.2",
7879
"json-stable-stringify-without-jsonify": "^1.0.1",
7980
"json5": "^2.1.1",
8081
"lodash": "^4.17.15",
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
var obj = { a: 1 };
2+
console.log(obj.a);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
var obj = { a: 1 };
2+
console.log(obj.a);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
var obj = { a: 1 };
2+
console.log(obj.a);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
var obj = { a: 1 };
2+
console.log(obj.a);

test/lint-files.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,22 @@ test('typescript files', async t => {
195195
)
196196
);
197197
});
198+
199+
async function configType(t, {dir}) {
200+
const {results} = await fn.lintFiles('**/*', {cwd: path.resolve('fixtures', 'config-files', dir)});
201+
202+
t.true(
203+
hasRule(
204+
results,
205+
path.resolve('fixtures', 'config-files', dir, 'file.js'),
206+
'no-var'
207+
)
208+
);
209+
}
210+
211+
configType.title = (_, {type}) => `load config from ${type}`.trim();
212+
213+
test(configType, {type: 'xo.config.js', dir: 'xo-config_js'});
214+
test(configType, {type: '.xo-config.js', dir: 'xo-config_js'});
215+
test(configType, {type: '.xo-config.json', dir: 'xo-config_json'});
216+
test(configType, {type: '.xo-config', dir: 'xo-config'});

test/lint-text.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,3 +282,15 @@ test('typescript files', t => {
282282
]);`, {filename: 'fixtures/typescript/child/sub-child/four-spaces.ts'}));
283283
t.true(hasRule(results, '@typescript-eslint/indent'));
284284
});
285+
286+
function configType(t, {dir}) {
287+
const {results} = fn.lintText('var obj = { a: 1 };\n', {cwd: path.resolve('fixtures', 'config-files', dir), filename: 'file.js'});
288+
t.true(hasRule(results, 'no-var'));
289+
}
290+
291+
configType.title = (_, {type}) => `load config from ${type}`.trim();
292+
293+
test(configType, {type: 'xo.config.js', dir: 'xo-config_js'});
294+
test(configType, {type: '.xo-config.js', dir: 'xo-config_js'});
295+
test(configType, {type: '.xo-config.json', dir: 'xo-config_json'});
296+
test(configType, {type: '.xo-config', dir: 'xo-config'});

test/options-manager.js

Lines changed: 21 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -521,29 +521,26 @@ test('mergeWithFileConfig: tsx files', async t => {
521521
});
522522
});
523523

524-
function mergeWithFileConfigFileType(t, {dir}) {
525-
const cwd = path.resolve('fixtures', 'config-files', dir);
526-
const {options} = manager.mergeWithFileConfig({cwd});
527-
const expected = {esnext: true, extensions: DEFAULT_EXTENSION, ignores: DEFAULT_IGNORES, cwd, nodeVersion: undefined};
528-
t.deepEqual(options, expected);
529-
}
530-
531-
mergeWithFileConfigFileType.title = (_, {type}) => `mergeWithFileConfig: load from ${type}`.trim();
532-
533-
test(mergeWithFileConfigFileType, {type: 'xo.config.js', dir: 'xo-config_js'});
534-
test(mergeWithFileConfigFileType, {type: '.xo-config.js', dir: 'xo-config_js'});
535-
test(mergeWithFileConfigFileType, {type: '.xo-config.json', dir: 'xo-config_json'});
536-
test(mergeWithFileConfigFileType, {type: '.xo-config', dir: 'xo-config'});
537-
538524
test('mergeWithFileConfigs: nested configs with prettier', async t => {
539525
const cwd = path.resolve('fixtures', 'nested-configs');
540526
const paths = [
541527
'no-semicolon.js',
542528
'child/semicolon.js',
543529
'child-override/two-spaces.js',
544530
'child-override/child-prettier-override/semicolon.js'
545-
];
546-
const result = await manager.mergeWithFileConfigs(paths, {cwd});
531+
].map(file => path.resolve(cwd, file));
532+
const result = await manager.mergeWithFileConfigs(paths, {cwd}, [
533+
{
534+
filepath: path.resolve(cwd, 'child-override', 'child-prettier-override', 'package.json'),
535+
config: {overrides: [{files: 'semicolon.js', prettier: true}]}
536+
},
537+
{filepath: path.resolve(cwd, 'package.json'), config: {semicolon: true}},
538+
{
539+
filepath: path.resolve(cwd, 'child-override', 'package.json'),
540+
config: {overrides: [{files: 'two-spaces.js', space: 4}]}
541+
},
542+
{filepath: path.resolve(cwd, 'child', 'package.json'), config: {semicolon: false}}
543+
]);
547544

548545
t.deepEqual(result, [
549546
{
@@ -607,8 +604,13 @@ test('mergeWithFileConfigs: nested configs with prettier', async t => {
607604

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

613615
t.deepEqual(omit(result[0], 'options.tsConfigPath'), {
614616
files: [path.resolve(cwd, 'two-spaces.tsx')],
@@ -672,41 +674,13 @@ test('mergeWithFileConfigs: typescript files', async t => {
672674
]
673675
});
674676

675-
const secondResult = await manager.mergeWithFileConfigs(paths, {cwd});
677+
const secondResult = await manager.mergeWithFileConfigs(paths, {cwd}, configFiles);
676678

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

682-
async function mergeWithFileConfigsFileType(t, {dir}) {
683-
const cwd = path.resolve('fixtures', 'config-files', dir);
684-
const paths = ['a.js', 'b.js'];
685-
686-
const result = await manager.mergeWithFileConfigs(paths, {cwd});
687-
688-
t.deepEqual(result, [
689-
{
690-
files: paths.reverse().map(p => path.resolve(cwd, p)),
691-
options: {
692-
esnext: true,
693-
nodeVersion: undefined,
694-
cwd,
695-
extensions: DEFAULT_EXTENSION,
696-
ignores: DEFAULT_IGNORES
697-
},
698-
prettierOptions: {}
699-
}
700-
]);
701-
}
702-
703-
mergeWithFileConfigsFileType.title = (_, {type}) => `mergeWithFileConfigs: load from ${type}`.trim();
704-
705-
test(mergeWithFileConfigsFileType, {type: 'xo.config.js', dir: 'xo-config_js'});
706-
test(mergeWithFileConfigsFileType, {type: '.xo-config.js', dir: 'xo-config_js'});
707-
test(mergeWithFileConfigsFileType, {type: '.xo-config.json', dir: 'xo-config_json'});
708-
test(mergeWithFileConfigsFileType, {type: '.xo-config', dir: 'xo-config'});
709-
710684
test('applyOverrides', t => {
711685
t.deepEqual(
712686
manager.applyOverrides(

0 commit comments

Comments
 (0)