Skip to content

Commit

Permalink
Close #58 PR: Support overridable configs. Fixes #22
Browse files Browse the repository at this point in the history
  • Loading branch information
jamestalmage authored and sindresorhus committed Dec 29, 2015
1 parent 4e859d2 commit 3cf0ccf
Show file tree
Hide file tree
Showing 11 changed files with 533 additions and 112 deletions.
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));
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 @@ -25,6 +25,7 @@
},
"files": [
"index.js",
"options-manager.js",
"cli.js"
],
"keywords": [
Expand Down Expand Up @@ -69,9 +70,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 @@ -83,6 +87,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

0 comments on commit 3cf0ccf

Please sign in to comment.