Skip to content

Commit

Permalink
Add config upgrader
Browse files Browse the repository at this point in the history
Automatically upgrade pre-ESLint 3 config to ESLint 3-compatible format.
  • Loading branch information
pointlessone committed Feb 22, 2017
1 parent 0c02a2b commit 1f9ccf3
Show file tree
Hide file tree
Showing 5 changed files with 517 additions and 1 deletion.
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"env": {
"node": true,
"mocha": true
"mocha": true,
"es6": true
},
"rules": {
"block-spacing": 2,
Expand Down
5 changes: 5 additions & 0 deletions bin/eslint.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ var ESLINT_WARNING_SEVERITY = 1;
var checks = require("../lib/checks");
var validateConfig = require("../lib/validate_config");
var computeFingerprint = require("../lib/compute_fingerprint");
const ConfigUpgrader = require("../lib/config_upgrader");

// a wrapper for emitting perf timing
function runWithTiming(name, fn) {
Expand Down Expand Up @@ -235,6 +236,10 @@ function analyzeFiles() {
if (validateConfig(options.configFile)) {
console.error("ESLint is running with the " + cli.getConfigForFile(null).parser + " parser.");

for (const line of ConfigUpgrader.upgradeInstructions(analysisFiles, process.cwd())) {
console.error(line);
}

analyzeFiles();
} else {
console.error("No rules are configured. Make sure you have added a config file with rules enabled.");
Expand Down
272 changes: 272 additions & 0 deletions lib/config_upgrader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
"use strict";

const Config = require("eslint/lib/config")
, merge = require("eslint/lib/config/config-ops").merge
, path = require("path")
, stringify = require("json-stable-stringify")
, CONFIG_FILES = require("eslint/lib/config/config-file").CONFIG_FILES
, FileFinder = require("eslint/lib/file-finder");

var SEVERITIES = {
0: "off",
1: "warn",
2: "error"
};

var ES6Features = [
"arrowFunctions", "binaryLiterals", "blockBindings", "classes",
"defaultParams", "destructuring", "forOf", "generators", "modules",
"objectLiteralComputedProperties", "objectLiteralDuplicateProperties",
"objectLiteralShorthandMethods", "objectLiteralShorthandProperties",
"octalLiterals", "regexUFlag", "regexYFlag", "restParams", "spread",
"superInFunctions", "templateStrings", "unicodeCodePointEscapes"
];

function upgradeEcmaFeatures(config) {
let report = [];
if (Reflect.has(config, "ecmaFeatures")) {
let parserOptions = {};
if (!Reflect.has(config, "parserOptions")) {
config["parserOptions"] = parserOptions;
};
const features = config.ecmaFeatures;
let es = 5;

if (features["modules"] === true) {
parserOptions["sourceType"] = "module";
report.push(`* Set sourceType to "module" in parserOptions section`);
}

for (const feature of ES6Features) {
if (Reflect.has(features, feature)) {
if (features[feature] === true) {
es = 6;
}
delete features[feature];
report.push(`* Remove ${feature} from ecmaFeatures section`);
}
}

parserOptions.ecmaVersion = Math.max(es, parserOptions.ecmaVersion || 0);
if (parserOptions.ecmaVersion !== 5) {
report.push(`* Set ecmaVersion to ${parserOptions.ecmaVersion} in parserOptions section`);
}

if (Object.keys(features).length) {
parserOptions["ecmaFeatures"] = features;
delete config["ecmaFeatures"];
report.push("* Move ecmaFeatures section under parserOptions section");
}

delete config["ecmaFeatures"];
}
return report;
}

const removedRules = {
"generator-star":
function(severity, pos) {
var pos = pos || "";
var config = {
before: pos === "middle" || pos === "end",
after: pos === "begin" || pos === "middle"
};

return {"generator-star-spacing": [severity, config]};
},
"global-strict":
function(severity, option) {
if (option === "always") {
var config = {"global": true};
return {"strict": [severity, config]};
} else {
return {"strict": severity};
};
},
"no-arrow-condition":
function(severity, option) {
return {
"no-confusing-arrow": severity,
"no-constant-condition": [severity, {"checkLoops": false}]
};
},
"no-comma-dangle":
function(severity) {
return {"comma-dangle": [severity, "always-multiline"]};
},
"no-empty-class":
function(severity) {
return {"no-empty-character-class": severity};
},
"no-empty-label":
function(severity) {
return {"no-labels": [severity, {"allowLoop": true}]};
},
"no-extra-strict":
function(severity) {
return {"strict": [severity, {"global": true}]};
},
"no-reserved-keys":
function(severity) {
return {"quote-props": [severity, "as-needed", {"keywords": true}]};
},
"no-space-before-semi":
function(severity) {
return {"semi-spacing": [severity, {"before": false}]};
},
"no-wrap-func":
function(severity) {
return {"no-extra-parens": [severity, "functions"]};
},
"space-after-function-name":
function(severity, option) {
return {"space-before-function-paren": [severity, option]};
},
"space-after-keywords":
function(severity, option) {
return {
"keyword-spacing": [
severity,
{
"after": option === "always"
}
]};
},
"space-before-function-parentheses":
function(severity, options) {
return {"space-before-function-paren": [severity, options]};
},
"space-before-keywords":
function(severity, option) {
var config = {
"before": option === "always"
};

return {"keyword-spacing": [severity, config]};
},
"space-in-brackets":
function(severity, option) {
return {
"object-curly-spacing": [severity, option],
"array-bracket-spacing": [severity, option]
};
},
"space-return-throw-case":
function(severity) {
return {"keyword-spacing": [severity, {"after": true}]};
},
"space-unary-word-ops":
function(severity) {
return {"space-unary-ops": [severity, {"words": true}]};
},
"spaced-line-comment":
function(severity, options) {
return {"spaced-comment": [severity].concat(options)};
}
};

function upgradeRules(rules) {
let report = [];
for (const oldName in removedRules) {
if (Reflect.has(rules, oldName)) {
let config = rules[oldName];
if (config.constructor !== Array) {
config = [config];
}
let severity = config.shift();
severity = SEVERITIES[severity] || severity;
if (config.length === 1) {
config = config[0];
}
let newRules = removedRules[oldName](severity, config);
delete rules[oldName];
for (const rule in newRules) {
rules[rule] = newRules[rule];
}

report.push(
`* Remove ${oldName} rule and add the following:\n` +
stringify(newRules, { space: 4 }).replace(/^[{}]$/gm, "") +
"\n"
);
}
}
return report;
}

function relativePath(filePath, root) {
return filePath.replace(new RegExp(`^${root}/`), '');
}

class ConfigUpgrader {
constructor() {
this._report = [];
};

upgrade(originalConfig) {
let config = merge({}, originalConfig);

this._report = [];

let report = upgradeEcmaFeatures(config);
this._report = this._report.concat(report);
if (Reflect.has(config, "rules")) {
report = upgradeRules(config.rules);
this._report = this._report.concat(report);
}

return config;
}

get report() {
return [].concat(this._report);
}

static configs(analysisFiles) {
const dirs = analysisFiles.map(function(fileName){
return path.dirname(fileName);
});
const directories = Array.from(new Set(dirs));

const localConfigFinder = new FileFinder(CONFIG_FILES, process.cwd());

const configs = new Set();
for (const dir of directories) {
const configFiles = localConfigFinder.findAllInDirectoryAndParents(dir);
for (const file of configFiles) {
configs.add(file);
}
}

return Array.from(configs);
}

static upgradeInstructions(analysisFiles, root) {
const reports = this.configs(analysisFiles).map(function(configFile) {
let report = [];

const upgrader = new ConfigUpgrader();
const config = new Config({configFile: configFile});
upgrader.upgrade(config.useSpecificConfig);

if (path.extname(configFile) === '') {
report.push("* Add .yml or .json to the config file name. Extension-less config file names are deprecated.");
}

const bareConfigFilePath = relativePath(configFile, root);

if (report.length > 0 || upgrader.report.length > 0) {
report = [
`${bareConfigFilePath} appears to be incompatible with ESLint 3.`,
"To upgrade it do the following:\n"
].concat(report).concat(upgrader.report);
}

return report;
});

return reports.reduce(function(a, b) { return a.concat([""]).concat(b); });
}
}

module.exports = ConfigUpgrader;
10 changes: 10 additions & 0 deletions lib/eslint-patch.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use strict';
var meld = require('meld');
var docs = require('./docs');
var Config = require("eslint/lib/config");
var ConfigUpgrader = require('./config_upgrader');

var supportedPlugins = ['react', 'babel'];

Expand All @@ -27,6 +29,14 @@ module.exports = function patcher(eslint) {
// }
// });

const originalGetConfig = Config.prototype.getConfig;
Config.prototype.getConfig = function(filePath) {
const originalConfig = originalGetConfig.apply(this, [filePath]);
const configUpgrader = new ConfigUpgrader();

return configUpgrader.upgrade(originalConfig);
};

eslint.docs = docs;

return eslint;
Expand Down
Loading

0 comments on commit 1f9ccf3

Please sign in to comment.