Merge pull request babel/babel-eslint#264 from hzoo/eslint2

ESLint 2
This commit is contained in:
Henry Zhu 2016-03-01 22:34:53 -05:00
parent dce22ce5f6
commit 511862c4ee
11 changed files with 603 additions and 85 deletions

View File

@ -0,0 +1,57 @@
// comment fixes
module.exports = function (ast, comments, tokens) {
if (comments.length) {
var firstComment = comments[0];
var lastComment = comments[comments.length - 1];
// fixup program start
if (!tokens.length) {
// if no tokens, the program starts at the end of the last comment
ast.start = lastComment.end;
ast.loc.start.line = lastComment.loc.end.line;
ast.loc.start.column = lastComment.loc.end.column;
if (ast.leadingComments === null && ast.innerComments.length) {
ast.leadingComments = ast.innerComments;
}
} else if (firstComment.start < tokens[0].start) {
// if there are comments before the first token, the program starts at the first token
var token = tokens[0];
// ast.start = token.start;
// ast.loc.start.line = token.loc.start.line;
// ast.loc.start.column = token.loc.start.column;
// estraverse do not put leading comments on first node when the comment
// appear before the first token
if (ast.body.length) {
var node = ast.body[0];
node.leadingComments = [];
var firstTokenStart = token.start;
var len = comments.length;
for (var i = 0; i < len && comments[i].start < firstTokenStart; i++) {
node.leadingComments.push(comments[i]);
}
}
}
// fixup program end
if (tokens.length) {
var lastToken = tokens[tokens.length - 1];
if (lastComment.end > lastToken.end) {
// If there is a comment after the last token, the program ends at the
// last token and not the comment
// ast.end = lastToken.end;
ast.range[1] = lastToken.end;
ast.loc.end.line = lastToken.loc.end.line;
ast.loc.end.column = lastToken.loc.end.column;
}
}
} else {
if (!tokens.length) {
ast.loc.start.line = 1;
ast.loc.end.line = 1;
}
}
if (ast.body && ast.body.length > 0) {
ast.loc.start.line = ast.body[0].loc.start.line;
ast.range[0] = ast.body[0].start;
}
};

View File

@ -0,0 +1,93 @@
module.exports = function (tokens, tt) {
var startingToken = 0;
var currentToken = 0;
var numBraces = 0; // track use of {}
var numBackQuotes = 0; // track number of nested templates
function isBackQuote(token) {
return tokens[token].type === tt.backQuote;
}
function isTemplateStarter(token) {
return isBackQuote(token) ||
// only can be a template starter when in a template already
tokens[token].type === tt.braceR && numBackQuotes > 0;
}
function isTemplateEnder(token) {
return isBackQuote(token) ||
tokens[token].type === tt.dollarBraceL;
}
// append the values between start and end
function createTemplateValue(start, end) {
var value = "";
while (start <= end) {
if (tokens[start].value) {
value += tokens[start].value;
} else if (tokens[start].type !== tt.template) {
value += tokens[start].type.label;
}
start++;
}
return value;
}
// create Template token
function replaceWithTemplateType(start, end) {
var templateToken = {
type: "Template",
value: createTemplateValue(start, end),
start: tokens[start].start,
end: tokens[end].end,
loc: {
start: tokens[start].loc.start,
end: tokens[end].loc.end
}
};
// put new token in place of old tokens
tokens.splice(start, end - start + 1, templateToken);
}
function trackNumBraces(token) {
if (tokens[token].type === tt.braceL) {
numBraces++;
} else if (tokens[token].type === tt.braceR) {
numBraces--;
}
}
while (startingToken < tokens.length) {
// template start: check if ` or }
if (isTemplateStarter(startingToken) && numBraces === 0) {
if (isBackQuote(startingToken)) {
numBackQuotes++;
}
currentToken = startingToken + 1;
// check if token after template start is "template"
if (currentToken >= tokens.length - 1 || tokens[currentToken].type !== tt.template) {
break;
}
// template end: find ` or ${
while (!isTemplateEnder(currentToken)) {
if (currentToken >= tokens.length - 1) {
break;
}
currentToken++;
}
if (isBackQuote(currentToken)) {
numBackQuotes--;
}
// template start and end found: create new token
replaceWithTemplateType(startingToken, currentToken);
} else if (numBackQuotes > 0) {
trackNumBraces(startingToken);
}
startingToken++;
}
}

View File

@ -0,0 +1,20 @@
exports.attachComments = require("./attachComments");
exports.toTokens = require("./toTokens");
exports.toAST = require("./toAST");
exports.convertComments = function (comments) {
for (var i = 0; i < comments.length; i++) {
var comment = comments[i];
if (comment.type === "CommentBlock") {
comment.type = "Block";
} else if (comment.type === "CommentLine") {
comment.type = "Line";
}
// sometimes comments don't get ranges computed,
// even with options.ranges === true
if (!comment.range) {
comment.range = [comment.start, comment.end];
}
}
}

View File

@ -0,0 +1,272 @@
var source;
module.exports = function (ast, traverse, code) {
source = code;
ast.range = [ast.start, ast.end];
traverse(ast, astTransformVisitor);
};
function changeToLiteral(node) {
node.type = "Literal";
if (!node.raw) {
if (node.extra && node.extra.raw) {
node.raw = node.extra.raw;
} else {
node.raw = source.slice(node.start, node.end);
}
}
}
var astTransformVisitor = {
noScope: true,
enter: function (path) {
var node = path.node;
node.range = [node.start, node.end];
// private var to track original node type
node._babelType = node.type;
if (node.innerComments) {
node.trailingComments = node.innerComments;
delete node.innerComments;
}
if (node.trailingComments) {
for (var i = 0; i < node.trailingComments.length; i++) {
var comment = node.trailingComments[i];
if (comment.type === "CommentLine") {
comment.type = "Line";
} else if (comment.type === "CommentBlock") {
comment.type = "Block";
}
comment.range = [comment.start, comment.end];
}
}
if (node.leadingComments) {
for (var i = 0; i < node.leadingComments.length; i++) {
var comment = node.leadingComments[i];
if (comment.type === "CommentLine") {
comment.type = "Line";
} else if (comment.type === "CommentBlock") {
comment.type = "Block";
}
comment.range = [comment.start, comment.end];
}
}
// make '_paths' non-enumerable (babel-eslint #200)
Object.defineProperty(node, "_paths", { value: node._paths, writable: true });
},
exit: function (path) {
var node = path.node;
[
fixDirectives,
].forEach(function (fixer) {
fixer(path);
});
if (path.isJSXText()) {
node.type = "Literal";
node.raw = node.value;
}
if (path.isNumericLiteral() ||
path.isStringLiteral()) {
changeToLiteral(node);
}
if (path.isBooleanLiteral()) {
node.type = "Literal";
node.raw = String(node.value);
}
if (path.isNullLiteral()) {
node.type = "Literal";
node.raw = "null";
node.value = null;
}
if (path.isRegExpLiteral()) {
node.type = "Literal";
node.raw = node.extra.raw;
node.value = {};
node.regex = {
pattern: node.pattern,
flags: node.flags
};
delete node.extra;
delete node.pattern;
delete node.flags;
}
if (path.isObjectProperty()) {
node.type = "Property";
node.kind = "init";
}
if (path.isClassMethod() || path.isObjectMethod()) {
var code = source.slice(node.key.end, node.body.start);
var offset = code.indexOf("(");
node.value = {
type: "FunctionExpression",
id: node.id,
params: node.params,
body: node.body,
async: node.async,
generator: node.generator,
expression: node.expression,
defaults: [], // basic support - TODO: remove (old esprima)
loc: {
start: {
line: node.key.loc.start.line,
column: node.key.loc.end.column + offset // a[() {]
},
end: node.body.loc.end
}
};
// [asdf]() {
node.value.range = [node.key.end + offset, node.body.end];
node.value.start = node.value.range && node.value.range[0] || node.value.loc.start.column;
node.value.end = node.value.range && node.value.range[1] || node.value.loc.end.column;
if (node.returnType) {
node.value.returnType = node.returnType;
}
if (node.typeParameters) {
node.value.typeParameters = node.typeParameters;
}
if (path.isClassMethod()) {
node.type = "MethodDefinition";
}
if (path.isObjectMethod()) {
node.type = "Property";
if (node.kind === "method") {
node.kind = "init";
}
}
delete node.body;
delete node.id;
delete node.async;
delete node.generator;
delete node.expression;
delete node.params;
delete node.returnType;
delete node.typeParameters;
}
if (path.isRestProperty() || path.isSpreadProperty()) {
node.type = "Experimental" + node.type;
}
// flow: prevent "no-undef"
// for "Component" in: "let x: React.Component"
if (path.isQualifiedTypeIdentifier()) {
delete node.id;
}
// for "b" in: "var a: { b: Foo }"
if (path.isObjectTypeProperty()) {
delete node.key;
}
// for "indexer" in: "var a: {[indexer: string]: number}"
if (path.isObjectTypeIndexer()) {
delete node.id;
}
// for "param" in: "var a: { func(param: Foo): Bar };"
if (path.isFunctionTypeParam()) {
delete node.name;
}
// modules
if (path.isImportDeclaration()) {
delete node.isType;
}
if (path.isExportDeclaration()) {
var declar = path.get("declaration");
if (declar.isClassExpression()) {
node.declaration.type = "ClassDeclaration";
} else if (declar.isFunctionExpression()) {
node.declaration.type = "FunctionDeclaration";
}
}
// remove class property keys (or patch in escope)
if (path.isClassProperty()) {
delete node.key;
}
// async function as generator
if (path.isFunction()) {
if (node.async) node.generator = true;
}
// TODO: remove (old esprima)
if (path.isFunction()) {
if (!node.defaults) {
node.defaults = [];
}
}
// await transform to yield
if (path.isAwaitExpression()) {
node.type = "YieldExpression";
node.delegate = node.all;
delete node.all;
}
// template string range fixes
if (path.isTemplateLiteral()) {
node.quasis.forEach(function (q) {
q.range[0] -= 1;
if (q.tail) {
q.range[1] += 1;
} else {
q.range[1] += 2;
}
q.loc.start.column -= 1;
if (q.tail) {
q.loc.end.column += 1;
} else {
q.loc.end.column += 2;
}
});
}
}
};
function fixDirectives (path) {
if (!(path.isProgram() || path.isFunction())) return;
var node = path.node;
var directivesContainer = node;
var body = node.body;
if (node.type !== "Program") {
directivesContainer = body;
body = body.body;
}
if (!directivesContainer.directives) return;
directivesContainer.directives.reverse().forEach(function (directive) {
directive.type = "ExpressionStatement";
directive.expression = directive.value;
delete directive.value;
directive.expression.type = "Literal";
changeToLiteral(directive.expression);
body.unshift(directive);
});
delete directivesContainer.directives;
}
// fixDirectives

View File

@ -0,0 +1,60 @@
module.exports = function (token, tt, source) {
var type = token.type;
token.range = [token.start, token.end];
if (type === tt.name) {
token.type = "Identifier";
} else if (type === tt.semi || type === tt.comma ||
type === tt.parenL || type === tt.parenR ||
type === tt.braceL || type === tt.braceR ||
type === tt.slash || type === tt.dot ||
type === tt.bracketL || type === tt.bracketR ||
type === tt.ellipsis || type === tt.arrow ||
type === tt.star || type === tt.incDec ||
type === tt.colon || type === tt.question ||
type === tt.template || type === tt.backQuote ||
type === tt.dollarBraceL || type === tt.at ||
type === tt.logicalOR || type === tt.logicalAND ||
type === tt.bitwiseOR || type === tt.bitwiseXOR ||
type === tt.bitwiseAND || type === tt.equality ||
type === tt.relational || type === tt.bitShift ||
type === tt.plusMin || type === tt.modulo ||
type === tt.exponent || type === tt.prefix ||
type === tt.doubleColon ||
type.isAssign) {
token.type = "Punctuator";
if (!token.value) token.value = type.label;
} else if (type === tt.jsxTagStart) {
token.type = "Punctuator";
token.value = "<";
} else if (type === tt.jsxTagEnd) {
token.type = "Punctuator";
token.value = ">";
} else if (type === tt.jsxName) {
token.type = "JSXIdentifier";
} else if (type === tt.jsxText) {
token.type = "JSXText";
} else if (type.keyword === "null") {
token.type = "Null";
} else if (type.keyword === "false" || type.keyword === "true") {
token.type = "Boolean";
} else if (type.keyword) {
token.type = "Keyword";
} else if (type === tt.num) {
token.type = "Numeric";
token.value = source.slice(token.start, token.end);
} else if (type === tt.string) {
token.type = "String";
token.value = source.slice(token.start, token.end);
} else if (type === tt.regexp) {
token.type = "RegularExpression";
var value = token.value;
token.regex = {
pattern: value.pattern,
flags: value.flags
};
token.value = "/" + value.pattern + "/" + value.flags;
}
return token;
};

View File

@ -0,0 +1,16 @@
var convertTemplateType = require("./convertTemplateType");
var toToken = require("./toToken");
module.exports = function (tokens, tt, code) {
// transform tokens to type "Template"
convertTemplateType(tokens, tt);
var transformedTokens = tokens.filter(function (token) {
return token.type !== "CommentLine" && token.type !== "CommentBlock";
});
for (var i = 0, l = transformedTokens.length; i < l; i++) {
transformedTokens[i] = toToken(transformedTokens[i], tt, code);
}
return transformedTokens;
};

View File

@ -1,4 +1,4 @@
var acornToEsprima = require("acorn-to-esprima");
var babylonToEspree = require("./babylon-to-espree");
var assign = require("lodash.assign");
var pick = require("lodash.pick");
var Module = require("module");
@ -380,7 +380,9 @@ function monkeypatch() {
};
}
exports.parse = function (code) {
exports.parse = function (code, options) {
options = options || {};
try {
monkeypatch();
} catch (err) {
@ -388,12 +390,12 @@ exports.parse = function (code) {
process.exit(1);
}
return exports.parseNoPatch(code);
return exports.parseNoPatch(code, options);
}
exports.parseNoPatch = function (code) {
exports.parseNoPatch = function (code, options) {
var opts = {
sourceType: "module",
sourceType: options.sourceType || "module",
strictMode: true,
allowImportExportEverywhere: false, // consistent with espree
allowReturnOutsideFunction: true,
@ -422,7 +424,7 @@ exports.parseNoPatch = function (code) {
} catch (err) {
if (err instanceof SyntaxError) {
err.lineNumber = err.loc.line;
err.column = err.loc.column;
err.column = err.loc.column + 1;
// remove trailing "(LINE:COLUMN)" acorn message and add in esprima syntax error message start
err.message = "Line " + err.lineNumber + ": " + err.message.replace(/ \((\d+):(\d+)\)$/, "");
@ -437,27 +439,27 @@ exports.parseNoPatch = function (code) {
ast.tokens.pop();
// convert tokens
ast.tokens = acornToEsprima.toTokens(ast.tokens, tt, code);
ast.tokens = babylonToEspree.toTokens(ast.tokens, tt, code);
// add comments
acornToEsprima.convertComments(ast.comments);
babylonToEspree.convertComments(ast.comments);
// transform esprima and acorn divergent nodes
acornToEsprima.toAST(ast, traverse, code);
babylonToEspree.toAST(ast, traverse, code);
// ast.program.tokens = ast.tokens;
// ast.program.comments = ast.comments;
// ast = ast.program;
// remove File
ast.type = 'Program';
ast.type = "Program";
ast.sourceType = ast.program.sourceType;
ast.directives = ast.program.directives;
ast.body = ast.program.body;
delete ast.program;
delete ast._paths;
acornToEsprima.attachComments(ast, ast.comments, ast.tokens);
babylonToEspree.attachComments(ast, ast.comments, ast.tokens);
return ast;
}

View File

@ -1,17 +1,17 @@
{
"name": "babel-eslint",
"version": "5.0.0",
"description": "",
"description": "Custom parser for ESLint",
"main": "index.js",
"files": [
"index.js"
"index.js",
"babylon-to-espree"
],
"repository": {
"type": "git",
"url": "https://github.com/babel/babel-eslint.git"
},
"dependencies": {
"acorn-to-esprima": "^2.0.4",
"babel-traverse": "^6.0.20",
"babel-types": "^6.0.19",
"babylon": "^6.0.18",
@ -22,7 +22,8 @@
"bootstrap": "git submodule update --init && cd eslint && npm install",
"eslint": "cd eslint && mocha -c tests/lib/rules/*.js -r ../eslint-tester.js",
"test": "mocha",
"lint": "./node_modules/eslint/bin/eslint.js ./test index.js acorn-to-esprima.js",
"lint": "./node_modules/eslint/bin/eslint.js index.js babylon-to-espree test",
"fix": "./node_modules/eslint/bin/eslint.js index.js babylon-to-espree test --fix",
"preversion": "npm test"
},
"author": "Sebastian McKenzie <sebmck@gmail.com>",
@ -32,8 +33,8 @@
},
"homepage": "https://github.com/babel/babel-eslint",
"devDependencies": {
"eslint": "^1.10.2",
"espree": "^2.2.5",
"eslint": "^2.0.0",
"espree": "^3.0.0",
"mocha": "^2.3.3"
}
}

View File

@ -32,49 +32,49 @@ function assertImplementsAST(target, source, path) {
}
}
function lookup(obj, keypath, backwardsDepth) {
if (!keypath) { return obj; }
return keypath.split(".").slice(0, -1 * backwardsDepth)
.reduce(function (base, segment) { return base && base[segment], obj });
}
function parseAndAssertSame(code) {
var esAST = espree.parse(code, {
ecmaFeatures: {
arrowFunctions: true,
binaryLiterals: true,
blockBindings: true,
classes: true,
defaultParams: true,
destructuring: true,
forOf: true,
generators: true,
modules: true,
objectLiteralComputedProperties: true,
objectLiteralDuplicateProperties: true,
objectLiteralShorthandMethods: true,
objectLiteralShorthandProperties: true,
octalLiterals: true,
regexUFlag: true,
regexYFlag: true,
restParams: true,
spread: true,
superInFunctions: true,
templateStrings: true,
unicodeCodePointEscapes: true,
globalReturn: true,
// enable JSX parsing
jsx: true,
experimentalObjectRestSpread: true,
// enable return in global scope
globalReturn: true,
// enable implied strict mode (if ecmaVersion >= 5)
impliedStrict: true,
// allow experimental object rest/spread
experimentalObjectRestSpread: true
},
tokens: true,
loc: true,
range: true,
comment: true,
attachComment: true
attachComment: true,
ecmaVersion: 6,
sourceType: "module"
});
var babylonAST = babelEslint.parse(code);
try {
assertImplementsAST(esAST, babylonAST);
} catch(err) {
var traversal = err.message.slice(3, err.message.indexOf(":"));
if (esAST.tokens) {
delete esAST.tokens;
}
if (babylonAST.tokens) {
delete babylonAST.tokens;
}
err.message +=
"\nespree:\n" +
util.inspect(esAST, {depth: err.depth, colors: true}) +
util.inspect(lookup(esAST, traversal, 2), {depth: err.depth, colors: true}) +
"\nbabel-eslint:\n" +
util.inspect(babylonAST, {depth: err.depth, colors: true});
util.inspect(lookup(babylonAST, traversal, 2), {depth: err.depth, colors: true});
throw err;
}
// assert.equal(esAST, babylonAST);
@ -394,7 +394,7 @@ describe("acorn-to-esprima", function () {
it("do not allow import export everywhere", function() {
assert.throws(function () {
parseAndAssertSame("function F() { import a from \"a\"; }");
}, /Illegal import declaration/)
}, /SyntaxError: 'import' and 'export' may only appear at the top level/)
});
it("return outside function", function () {
@ -406,7 +406,7 @@ describe("acorn-to-esprima", function () {
});
it("StringLiteral", function () {
parseAndAssertSame('');
parseAndAssertSame("");
parseAndAssertSame("");
parseAndAssertSame("a");
});
@ -443,5 +443,17 @@ describe("acorn-to-esprima", function () {
"};"
].join("\n"));
});
it("RestOperator", function () {
parseAndAssertSame("var { a, ...b } = c");
parseAndAssertSame("var [ a, ...b ] = c");
parseAndAssertSame("var a = function (...b) {}");
});
it("SpreadOperator", function () {
parseAndAssertSame("var a = { b, ...c }");
parseAndAssertSame("var a = [ a, ...b ]");
parseAndAssertSame("var a = sum(...b)");
});
});
});

View File

@ -13,6 +13,9 @@ var errorLevel = 2;
var baseEslintOpts = {
parser: require.resolve(".."),
parserOptions: {
sourceType: "script"
}
};
/**

View File

@ -2,43 +2,25 @@
"use strict";
var eslint = require("eslint");
function verifyAndAssertMessages(code, rules, expectedMessages, features) {
var defaultEcmaFeatures = {
arrowFunctions: true,
binaryLiterals: true,
blockBindings: true,
classes: true,
defaultParams: true,
destructuring: true,
forOf: true,
generators: true,
modules: true,
objectLiteralComputedProperties: true,
objectLiteralDuplicateProperties: true,
objectLiteralShorthandMethods: true,
objectLiteralShorthandProperties: true,
octalLiterals: true,
regexUFlag: true,
regexYFlag: true,
restParams: true,
spread: true,
superInFunctions: true,
templateStrings: true,
unicodeCodePointEscapes: true,
globalReturn: true,
jsx: true,
experimentalObjectRestSpread: true
};
function verifyAndAssertMessages(code, rules, expectedMessages, sourceType) {
var messages = eslint.linter.verify(
code,
{
parser: require.resolve(".."),
rules: rules,
env: {
node: true
node: true,
es6: true
},
ecmaFeatures: features || defaultEcmaFeatures
parserOptions: {
ecmaVersion: 6,
ecmaFeatures: {
jsx: true,
experimentalObjectRestSpread: true,
globalReturn: true
},
sourceType: sourceType || "module"
}
}
);
@ -143,7 +125,7 @@ describe("verify", function () {
"\"use strict\"; () => 1",
{ "strict": [1, "global"] },
[],
{ modules: false }
"script"
);
});
@ -415,8 +397,8 @@ describe("verify", function () {
"var b: T = 1; b;"
].join("\n"),
{ "no-unused-vars": 1, "no-undef": 1 },
[ "1:21 \"T\" is defined but never used no-unused-vars",
'2:8 "T" is not defined. no-undef' ]
[ "1:21 'T' is defined but never used no-unused-vars",
"2:8 'T' is not defined. no-undef" ]
);
});
@ -426,7 +408,7 @@ describe("verify", function () {
"export class Foo extends Bar<T> {}",
].join("\n"),
{ "no-unused-vars": 1, "no-undef": 1 },
[ '2:30 "T" is not defined. no-undef' ]
[ "2:30 'T' is not defined. no-undef" ]
);
});
@ -1151,7 +1133,7 @@ describe("verify", function () {
verifyAndAssertMessages(
"var unused;",
{ "no-unused-vars": 1 },
[ "1:5 \"unused\" is defined but never used no-unused-vars" ]
[ "1:5 'unused' is defined but never used no-unused-vars" ]
);
});
@ -1175,7 +1157,7 @@ describe("verify", function () {
verifyAndAssertMessages(
"const {Bacona} = require('baconjs')",
{ "no-undef": 1, "no-unused-vars": 1 },
[ "1:8 \"Bacona\" is defined but never used no-unused-vars" ]
[ "1:8 'Bacona' is defined but never used no-unused-vars" ]
);
});
@ -1301,7 +1283,7 @@ describe("verify", function () {
"var x = 1;"
].join("\n"),
{ "no-use-before-define": 1 },
[ "1:13 \"x\" was used before it was defined no-use-before-define" ]
[ "1:13 'x' was used before it was defined no-use-before-define" ]
)
});
@ -1316,10 +1298,10 @@ describe("verify", function () {
it("getter/setter #218", function () {
verifyAndAssertMessages([
"class Person {",
"set a (v) { }",
" set a (v) { }",
"}"
].join("\n"),
{ "space-before-function-paren": 1, "space-before-keywords": 1, "indent": 1 },
{ "space-before-function-paren": 1, "keyword-spacing": [1, {"before": true}], "indent": 1 },
[]
)
});