Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support overridable configs #58

Closed
wants to merge 11 commits into from
3 changes: 2 additions & 1 deletion cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ var debug = require('debug')('xo');

// Prefer the local installation of XO.
var resolveCwd = require('resolve-cwd');
var hasFlag = require('has-flag');
var localCLI = resolveCwd('xo/cli');

if (localCLI && localCLI !== __filename) {
if (!hasFlag('no-local') && localCLI && localCLI !== __filename) {
debug('Using local install of XO.');
require(localCLI);
return;
Expand Down
148 changes: 39 additions & 109 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,121 +2,19 @@
var path = require('path');
var eslint = require('eslint');
var globby = require('globby');
var objectAssign = require('object-assign');
var arrify = require('arrify');
var pkgConf = require('pkg-conf');
var deepAssign = require('deep-assign');
var resolveFrom = require('resolve-from');
var homeOrTmp = require('home-or-tmp');

var DEFAULT_IGNORE = [
'**/node_modules/**',
'**/bower_components/**',
'coverage/**',
'{tmp,temp}/**',
'**/*.min.js',
'**/bundle.js',
'fixture{-*,}.{js,jsx}',
'{test/,}fixture{s,}/**',
'vendor/**',
'dist/**'
];

var DEFAULT_CONFIG = {
useEslintrc: false,
cache: true,
cacheLocation: path.join(homeOrTmp, '.xo-cache/'),
baseConfig: {
extends: 'xo'
}
};

var DEFAULT_PLUGINS = [
'no-empty-blocks',
'no-use-extend-native'
];

function handleOpts(opts) {
opts = objectAssign({
cwd: process.cwd()
}, opts);

opts = objectAssign({}, pkgConf.sync('xo', opts.cwd), opts);

// alias to help humans
opts.envs = opts.envs || opts.env;
opts.globals = opts.globals || opts.global;
opts.ignores = opts.ignores || opts.ignore;
opts.plugins = opts.plugins || opts.plugin;
opts.rules = opts.rules || opts.rule;
opts.extends = opts.extends || opts.extend;

opts.extends = arrify(opts.extends);
opts.ignores = DEFAULT_IGNORE.concat(opts.ignores || []);

opts._config = deepAssign({}, DEFAULT_CONFIG, {
envs: arrify(opts.envs),
globals: arrify(opts.globals),
plugins: DEFAULT_PLUGINS.concat(opts.plugins || []),
rules: opts.rules,
fix: opts.fix
});

if (!opts._config.rules) {
opts._config.rules = {};
}

if (opts.space) {
var spaces = typeof opts.space === 'number' ? opts.space : 2;
opts._config.rules.indent = [2, spaces, {SwitchCase: 1}];
}

if (opts.semicolon === false) {
opts._config.rules.semi = [2, 'never'];
opts._config.rules['semi-spacing'] = [2, {before: false, after: true}];
}

if (opts.esnext) {
opts._config.baseConfig.extends = 'xo/esnext';
} else {
// always use the Babel parser so it won't throw
// on esnext features in normal mode
opts._config.parser = 'babel-eslint';
opts._config.plugins = ['babel'].concat(opts._config.plugins);
opts._config.rules['generator-star-spacing'] = 0;
opts._config.rules['arrow-parens'] = 0;
opts._config.rules['object-curly-spacing'] = 0;
opts._config.rules['babel/object-curly-spacing'] = [2, 'never'];
}

if (opts.extends.length > 0) {
// user's configs must be resolved to their absolute paths
var configs = opts.extends.map(function (name) {
if (name.indexOf('eslint-config-') === -1) {
name = 'eslint-config-' + name;
}

return resolveFrom(opts.cwd, name);
});

configs.unshift(opts._config.baseConfig.extends);

opts._config.baseConfig.extends = configs;
}

return opts;
}
var optionsManager = require('./options-manager');

exports.lintText = function (str, opts) {
opts = handleOpts(opts);
opts = optionsManager.preprocess(opts);
opts = optionsManager.buildConfig(opts);

var engine = new eslint.CLIEngine(opts._config);
var engine = new eslint.CLIEngine(opts);

return engine.executeOnText(str, opts.filename);
};

exports.lintFiles = function (patterns, opts) {
opts = handleOpts(opts);
opts = optionsManager.preprocess(opts);

if (patterns.length === 0) {
patterns = '**/*.{js,jsx}';
Expand All @@ -129,12 +27,44 @@ exports.lintFiles = function (patterns, opts) {
return ext === '.js' || ext === '.jsx';
});

var engine = new eslint.CLIEngine(opts._config);
if (!(opts.overrides && opts.overrides.length)) {
return runEslint(paths, opts);
}

var overrides = opts.overrides;
delete opts.overrides;

return engine.executeOnFiles(paths);
var grouped = optionsManager.groupConfigs(paths, opts, overrides);

return mergeReports(grouped.map(function (data) {
return runEslint(data.paths, data.opts);
}));
});
};

function mergeReports(reports) {
// merge multiple reports into a single report
var results = [];
var errorCount = 0;
var warningCount = 0;
reports.forEach(function (report) {
results = results.concat(report.results);
errorCount += report.errorCount;
warningCount += report.warningCount;
});
return {
errorCount: errorCount,
warningCount: warningCount,
results: results
};
}

function runEslint(paths, opts) {
var config = optionsManager.buildConfig(opts);
var engine = new eslint.CLIEngine(config);
return engine.executeOnFiles(paths, config);
}

exports.getFormatter = eslint.CLIEngine.getFormatter;
exports.getErrorResults = eslint.CLIEngine.getErrorResults;
exports.outputFixes = eslint.CLIEngine.outputFixes;
178 changes: 178 additions & 0 deletions options-manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
'use strict';
var path = require('path');
var arrify = require('arrify');
var pkgConf = require('pkg-conf');
var deepAssign = require('deep-assign');
var resolveFrom = require('resolve-from');
var objectAssign = require('object-assign');
var homeOrTmp = require('home-or-tmp');
var multimatch = require('multimatch');

var DEFAULT_IGNORE = [
'**/node_modules/**',
'**/bower_components/**',
'coverage/**',
'{tmp,temp}/**',
'**/*.min.js',
'**/bundle.js',
'fixture{-*,}.{js,jsx}',
'{test/,}fixture{s,}/**',
'vendor/**',
'dist/**'
];

var DEFAULT_CONFIG = {
useEslintrc: false,
cache: true,
cacheLocation: path.join(homeOrTmp, '.xo-cache/'),
baseConfig: {
extends: 'xo'
}
};

var DEFAULT_PLUGINS = [
'no-empty-blocks',
'no-use-extend-native'
];

function normalizeOpts(opts) {
opts = objectAssign({}, opts);
// alias to help humans
['env', 'global', 'ignore', 'plugin', 'rule', 'extend'].forEach(function (singular) {
var plural = singular + 's';
var value = opts[plural] || opts[singular];

delete opts[singular];

if (value === undefined) {
return;
}

if (singular !== 'rule') {
value = arrify(value);
}

opts[plural] = value;
});

return opts;
}

function mergeWithPkgConf(opts) {
opts = objectAssign({
cwd: process.cwd()
}, opts);

return objectAssign({}, pkgConf.sync('xo', opts.cwd), opts);
}

function buildConfig(opts) {
var config = deepAssign({}, DEFAULT_CONFIG, {
envs: opts.envs,
globals: opts.globals,
plugins: DEFAULT_PLUGINS.concat(opts.plugins || []),
rules: opts.rules,
fix: opts.fix
});

if (!config.rules) {
config.rules = {};
}

if (opts.space) {
var spaces = typeof opts.space === 'number' ? opts.space : 2;
config.rules.indent = [2, spaces, {SwitchCase: 1}];
}

if (opts.semicolon === false) {
config.rules.semi = [2, 'never'];
config.rules['semi-spacing'] = [2, {before: false, after: true}];
}

if (opts.esnext) {
config.baseConfig.extends = 'xo/esnext';
} else {
// always use the Babel parser so it won't throw
// on esnext features in normal mode
config.parser = 'babel-eslint';
config.plugins = ['babel'].concat(config.plugins);
config.rules['generator-star-spacing'] = 0;
config.rules['arrow-parens'] = 0;
config.rules['object-curly-spacing'] = 0;
config.rules['babel/object-curly-spacing'] = [2, 'never'];
}

if (opts.extends && opts.extends.length > 0) {
// user's configs must be resolved to their absolute paths
var configs = opts.extends.map(function (name) {
if (name.indexOf('eslint-config-') === -1) {
name = 'eslint-config-' + name;
}

return resolveFrom(opts.cwd, name);
});

configs.unshift(config.baseConfig.extends);

config.baseConfig.extends = configs;
}

return config;
}

// Builds a list of overrides for a particular path, and a hash value.
// The hash value is a binary representation of which elements in the `overrides` array apply to the path.
//
// If overrides.length === 4, and only the first and third elements apply, then our hash is: 1010 (in binary)
function findApplicableOverrides(path, overrides) {
var hash = 0;
var applicable = [];
overrides.forEach(function (override) {
hash <<= 1;
if (multimatch(path, override.files).length > 0) {
applicable.push(override);
hash |= 1;
}
});
return {
hash: hash,
applicable: applicable
};
}

// Creates grouped sets of merged options together with the paths they apply to.
function groupConfigs(paths, baseOptions, overrides) {
var map = {};
var arr = [];

paths.forEach(function (x) {
var data = findApplicableOverrides(x, overrides);
if (!map[data.hash]) {
var mergedOpts = deepAssign.apply(null, [{}, baseOptions].concat(data.applicable));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where the merging of every applicable "overrides" happens. Is the deepAssign logic sufficient here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

delete mergedOpts.files;
arr.push(map[data.hash] = {
opts: mergedOpts,
paths: []
});
}
map[data.hash].paths.push(x);
});

return arr;
}

function preprocess(opts) {
opts = mergeWithPkgConf(opts);
opts = normalizeOpts(opts);
opts.ignores = DEFAULT_IGNORE.concat(opts.ignores || []);
return opts;
}

exports.DEFAULT_IGNORE = DEFAULT_IGNORE;
exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
exports.mergeWithPkgConf = mergeWithPkgConf;
exports.normalizeOpts = normalizeOpts;
exports.buildConfig = buildConfig;
exports.findApplicableOverrides = findApplicableOverrides;
exports.groupConfigs = groupConfigs;
exports.preprocess = preprocess;
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
},
"files": [
"index.js",
"options-manager.js",
"cli.js"
],
"keywords": [
Expand Down Expand Up @@ -62,9 +63,12 @@
"eslint-plugin-no-use-extend-native": "^0.3.2",
"get-stdin": "^5.0.0",
"globby": "^4.0.0",
"has-flag": "^1.0.0",
"home-or-tmp": "^2.0.0",
"meow": "^3.4.2",
"multimatch": "^2.1.0",
"object-assign": "^4.0.1",
"pinkie-promise": "^2.0.0",
"pkg-conf": "^1.0.1",
"resolve-cwd": "^1.0.0",
"resolve-from": "^2.0.0",
Expand All @@ -76,6 +80,7 @@
"eslint-config-xo-react": "^0.3.0",
"eslint-plugin-react": "^3.5.1",
"execa": "^0.1.1",
"proxyquire": "^1.7.3",
"temp-write": "^2.0.1",
"xo": "sindresorhus/xo#v0.11.2"
},
Expand Down
Loading