From c2c993379746920063638b53c5966797e939ec3a Mon Sep 17 00:00:00 2001 From: Jason Quense Date: Fri, 3 Jul 2015 23:14:57 -0400 Subject: [PATCH] Merge pull request babel/eslint-plugin-babel#1 from mathieumg/blockscopedvar_export Added support for experimental export types --- eslint/babel-eslint-plugin/README.md | 2 + eslint/babel-eslint-plugin/index.js | 2 + .../rules/block-scoped-var.js | 340 ++++++++++++++++++ .../tests/block-scoped-var.js | 105 ++++++ 4 files changed, 449 insertions(+) create mode 100644 eslint/babel-eslint-plugin/rules/block-scoped-var.js create mode 100644 eslint/babel-eslint-plugin/tests/block-scoped-var.js diff --git a/eslint/babel-eslint-plugin/README.md b/eslint/babel-eslint-plugin/README.md index 94db5b984d..e291617388 100644 --- a/eslint/babel-eslint-plugin/README.md +++ b/eslint/babel-eslint-plugin/README.md @@ -23,6 +23,7 @@ Finally enable all the rules you like to use (remember to disable the originals ```json { "rules": { + "babel/block-scoped-var": 1, "babel/object-shorthand": 1, "babel/generator-star": 1, "babel/generator-star-spacing": 1, @@ -34,6 +35,7 @@ Finally enable all the rules you like to use (remember to disable the originals Each rule cooresponds to a core eslint rule, and has the same options. +- `babel/block-scoped-var`: doesn't complain about `export x from "mod";` or `export * as x from "mod";` - `babel/object-shorthand`: doesn't fail when using object spread (`...obj`) - `babel/generator-star`: Handles async/await functions correctly - `babel/generator-star-spacing`: Handles async/await functions correctly diff --git a/eslint/babel-eslint-plugin/index.js b/eslint/babel-eslint-plugin/index.js index 6c76dbe4cd..8bfde48f91 100644 --- a/eslint/babel-eslint-plugin/index.js +++ b/eslint/babel-eslint-plugin/index.js @@ -2,12 +2,14 @@ module.exports = { rules: { + 'block-scoped-var': require('./rules/block-scoped-var'), 'object-shorthand': require('./rules/object-shorthand'), 'generator-star-spacing': require('./rules/generator-star-spacing'), 'generator-star': require('./rules/generator-star'), 'new-cap': require('./rules/new-cap') }, rulesConfig: { + 'block-scoped-var': 0, 'generator-star-spacing': 0, 'generator-star': 0, 'object-shorthand': 0, diff --git a/eslint/babel-eslint-plugin/rules/block-scoped-var.js b/eslint/babel-eslint-plugin/rules/block-scoped-var.js new file mode 100644 index 0000000000..71479e20d7 --- /dev/null +++ b/eslint/babel-eslint-plugin/rules/block-scoped-var.js @@ -0,0 +1,340 @@ +/** + * @fileoverview Rule to check for "block scoped" variables by binding context + * @author Matt DuVall + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var scopeStack = []; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Determines whether an identifier is in declaration position or is a non-declaration reference. + * @param {ASTNode} id The identifier. + * @param {ASTNode} parent The identifier's parent AST node. + * @returns {Boolean} true when the identifier is in declaration position. + */ + function isDeclaration(id, parent) { + switch (parent.type) { + case "FunctionDeclaration": + case "FunctionExpression": + return parent.params.indexOf(id) > -1 || id === parent.id; + + case "VariableDeclarator": + return id === parent.id; + + case "CatchClause": + return id === parent.param; + + default: + return false; + } + } + + /** + * Determines whether an identifier is in property position. + * @param {ASTNode} id The identifier. + * @param {ASTNode} parent The identifier's parent AST node. + * @returns {Boolean} true when the identifier is in property position. + */ + function isProperty(id, parent) { + switch (parent.type) { + case "MemberExpression": + return id === parent.property && !parent.computed; + + case "Property": + return id === parent.key; + + default: + return false; + } + } + + /** + * Pushes a new scope object on the scope stack. + * @returns {void} + */ + function pushScope() { + scopeStack.push([]); + } + + /** + * Removes the topmost scope object from the scope stack. + * @returns {void} + */ + function popScope() { + scopeStack.pop(); + } + + /** + * Declares the given names in the topmost scope object. + * @param {[String]} names A list of names to declare. + * @returns {void} + */ + function declare(names) { + [].push.apply(scopeStack[scopeStack.length - 1], names); + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + /** + * Declares all relevant identifiers for module imports. + * @param {ASTNode} node The AST node representing an import. + * @returns {void} + * @private + */ + function declareImports(node) { + declare([node.local.name]); + + if (node.imported && node.imported.name !== node.local.name) { + declare([node.imported.name]); + } + } + + /** + * Declares all relevant identifiers for module exports. + * @param {ASTNode} node The AST node representing an export. + * @returns {void} + * @private + */ + function declareExports(node) { + if (node.exported && node.exported.name) { + declare([node.exported.name]); + + if (node.local) { + declare([node.local.name]); + } + } + } + + /** + * Declares all relevant identifiers for classes. + * @param {ASTNode} node The AST node representing a class. + * @returns {void} + * @private + */ + function declareClass(node) { + + if (node.id) { + declare([node.id.name]); + } + + pushScope(); + } + + /** + * Declares all relevant identifiers for classes. + * @param {ASTNode} node The AST node representing a class. + * @returns {void} + * @private + */ + function declareClassMethod(node) { + pushScope(); + + declare([node.key.name]); + } + + /** + * Add declarations based on the type of node being passed. + * @param {ASTNode} node The node containing declarations. + * @returns {void} + * @private + */ + function declareByNodeType(node) { + + var declarations = []; + + switch (node.type) { + case "Identifier": + declarations.push(node.name); + break; + + case "ObjectPattern": + node.properties.forEach(function(property) { + declarations.push(property.key.name); + if (property.value) { + declarations.push(property.value.name); + } + }); + break; + + case "ArrayPattern": + node.elements.forEach(function(element) { + if (element) { + declarations.push(element.name); + } + }); + break; + + case "AssignmentPattern": + declareByNodeType(node.left); + break; + + case "RestElement": + declareByNodeType(node.argument); + break; + + // no default + } + + declare(declarations); + + } + + /** + * Adds declarations of the function parameters and pushes the scope + * @param {ASTNode} node The node containing declarations. + * @returns {void} + * @private + */ + function functionHandler(node) { + pushScope(); + + node.params.forEach(function(param) { + declareByNodeType(param); + }); + + declare(node.rest ? [node.rest.name] : []); + declare(["arguments"]); + } + + /** + * Adds declaration of the function name in its parent scope then process the function + * @param {ASTNode} node The node containing declarations. + * @returns {void} + * @private + */ + function functionDeclarationHandler(node) { + declare(node.id ? [node.id.name] : []); + functionHandler(node); + } + + /** + * Process function declarations and declares its name in its own scope + * @param {ASTNode} node The node containing declarations. + * @returns {void} + * @private + */ + function functionExpressionHandler(node) { + functionHandler(node); + declare(node.id ? [node.id.name] : []); + } + + function variableDeclarationHandler(node) { + node.declarations.forEach(function(declaration) { + declareByNodeType(declaration.id); + }); + + } + + return { + "Program": function() { + var scope = context.getScope(); + scopeStack = [scope.variables.map(function(v) { + return v.name; + })]; + + // global return creates another scope + if (context.ecmaFeatures.globalReturn) { + scope = scope.childScopes[0]; + scopeStack.push(scope.variables.map(function(v) { + return v.name; + })); + } + }, + + "ImportSpecifier": declareImports, + "ImportDefaultSpecifier": declareImports, + "ImportNamespaceSpecifier": declareImports, + + "ExportSpecifier": declareExports, + "ExportDefaultSpecifier": declareExports, + "ExportNamespaceSpecifier": declareExports, + + "BlockStatement": function(node) { + var statements = node.body; + pushScope(); + statements.forEach(function(stmt) { + if (stmt.type === "VariableDeclaration") { + variableDeclarationHandler(stmt); + } else if (stmt.type === "FunctionDeclaration") { + declare([stmt.id.name]); + } + }); + }, + + "VariableDeclaration": function (node) { + variableDeclarationHandler(node); + }, + + "BlockStatement:exit": popScope, + + "CatchClause": function(node) { + pushScope(); + declare([node.param.name]); + }, + "CatchClause:exit": popScope, + + "FunctionDeclaration": functionDeclarationHandler, + "FunctionDeclaration:exit": popScope, + + "ClassDeclaration": declareClass, + "ClassDeclaration:exit": popScope, + + "ClassExpression": declareClass, + "ClassExpression:exit": popScope, + + "MethodDefinition": declareClassMethod, + "MethodDefinition:exit": popScope, + + "FunctionExpression": functionExpressionHandler, + "FunctionExpression:exit": popScope, + + // Arrow functions cannot have names + "ArrowFunctionExpression": functionHandler, + "ArrowFunctionExpression:exit": popScope, + + "ForStatement": function() { + pushScope(); + }, + "ForStatement:exit": popScope, + + "ForInStatement": function() { + pushScope(); + }, + "ForInStatement:exit": popScope, + + "ForOfStatement": function() { + pushScope(); + }, + "ForOfStatement:exit": popScope, + + "Identifier": function(node) { + var ancestor = context.getAncestors().pop(); + if (isDeclaration(node, ancestor) || isProperty(node, ancestor) || ancestor.type === "LabeledStatement") { + return; + } + + for (var i = 0, l = scopeStack.length; i < l; i++) { + if (scopeStack[i].indexOf(node.name) > -1) { + return; + } + } + + context.report(node, "\"" + node.name + "\" used outside of binding context."); + } + }; + +}; + +module.exports.schema = []; diff --git a/eslint/babel-eslint-plugin/tests/block-scoped-var.js b/eslint/babel-eslint-plugin/tests/block-scoped-var.js new file mode 100644 index 0000000000..61297094ea --- /dev/null +++ b/eslint/babel-eslint-plugin/tests/block-scoped-var.js @@ -0,0 +1,105 @@ +/* eslint-disable */ + +/** + * @fileoverview Tests for block-scoped-var rule + * @author Matt DuVall + * @copyright 2015 Mathieu M-Gosselin. All rights reserved. + */ + +var eslint = require("eslint").linter, + ESLintTester = require("eslint-tester"), + eslintTester = new ESLintTester(eslint); + +eslintTester.addRuleTest("rules/block-scoped-var", { + valid: [ + //original test cases + { code: "function f() { } f(); var exports = { f: f };", ecmaFeatures: {modules: true} }, + { code: "var f = () => {}; f(); var exports = { f: f };", ecmaFeatures: {arrowFunctions: true, modules: true} }, + "!function f(){ f; }", + "function f() { } f(); var exports = { f: f };", + "function f() { var a, b; { a = true; } b = a; }", + "var a; function f() { var b = a; }", + "function f(a) { }", + "!function(a) { };", + "!function f(a) { };", + "function f(a) { var b = a; }", + "!function f(a) { var b = a; };", + "function f() { var g = f; }", + "function f() { } function g() { var f = g; }", + "function f() { var hasOwnProperty; { hasOwnProperty; } }", + "function f(){ a; b; var a, b; }", + "function f(){ g(); function g(){} }", + { code: "function myFunc(foo) { \"use strict\"; var { bar } = foo; bar.hello();}", ecmaFeatures: { destructuring: true } }, + { code: "function myFunc(foo) { \"use strict\"; var [ bar ] = foo; bar.hello();}", ecmaFeatures: { destructuring: true } }, + { code: "function myFunc(...foo) { return foo;}", ecmaFeatures: { restParams: true } }, + { code: "var f = () => { var g = f; }", ecmaFeatures: { arrowFunctions: true } }, + { code: "class Foo {}\nexport default Foo;", ecmaFeatures: { modules: true, classes: true } }, + { code: "new Date", globals: {Date: false} }, + { code: "new Date", globals: {} }, + { code: "var eslint = require('eslint');", globals: {require: false} }, + { code: "var fun = function({x}) {return x;};", ecmaFeatures: { destructuring: true } }, + { code: "var fun = function([,x]) {return x;};", ecmaFeatures: { destructuring: true } }, + "function f(a) { return a.b; }", + "var a = { \"foo\": 3 };", + "var a = { foo: 3 };", + "var a = { foo: 3, bar: 5 };", + "var a = { set foo(a){}, get bar(){} };", + "function f(a) { return arguments[0]; }", + "function f() { }; var a = f;", + "var a = f; function f() { };", + "function f(){ for(var i; i; i) i; }", + "function f(){ for(var a=0, b=1; a; b) a, b; }", + "function f(){ for(var a in {}) a; }", + "function f(){ switch(2) { case 1: var b = 2; b; break; default: b; break;} b; }", + "a:;", + { code: "const React = require(\"react/addons\");const cx = React.addons.classSet;", globals: { require: false }, ecmaFeatures: { globalReturn: true, modules: true, blockBindings: true }}, + { code: "var v = 1; function x() { return v; };", ecmaFeatures: { globalReturn: true }}, + { code: "import * as y from \"./other.js\"; y();", ecmaFeatures: { modules: true }}, + { code: "import y from \"./other.js\"; y();", ecmaFeatures: { modules: true }}, + { code: "import {x as y} from \"./other.js\"; y();", ecmaFeatures: { modules: true }}, + { code: "var x; export {x};", ecmaFeatures: { modules: true }}, + { code: "var x; export {x as v};", ecmaFeatures: { modules: true }}, + { code: "export {x} from \"./other.js\";", ecmaFeatures: { modules: true }}, + { code: "export {x as v} from \"./other.js\";", ecmaFeatures: { modules: true }}, + { code: "class Test { myFunction() { return true; }}", ecmaFeatures: { classes: true }}, + { code: "class Test { get flag() { return true; }}", ecmaFeatures: { classes: true }}, + { code: "var Test = class { myFunction() { return true; }}", ecmaFeatures: { classes: true }}, + { code: "var doStuff; let {x: y} = {x: 1}; doStuff(y);", ecmaFeatures: { blockBindings: true, destructuring: true }}, + { code: "function foo({x: y}) { return y; }", ecmaFeatures: { blockBindings: true, destructuring: true }}, + + // Babel-specific test-cases. + { code: "export x from \"./other.js\";", parser: "babel-eslint", ecmaFeatures: {modules: true} }, + { code: "export * as x from \"./other.js\";", parser: "babel-eslint", ecmaFeatures: {modules: true} }, + ], + invalid: [ + { code: "!function f(){}; f", errors: [{ message: "\"f\" used outside of binding context." }] }, + { code: "var f = function foo() { }; foo(); var exports = { f: foo };", errors: [{ message: "\"foo\" used outside of binding context." }, { message: "\"foo\" used outside of binding context."}] }, + { code: "var f = () => { x; }", ecmaFeatures: { arrowFunctions: true }, errors: [{ message: "\"x\" used outside of binding context.", type: "Identifier" }] }, + { code: "function f(){ x; }", errors: [{ message: "\"x\" used outside of binding context.", type: "Identifier" }] }, + { code: "function f(){ x; { var x; } }", errors: [{ message: "\"x\" used outside of binding context.", type: "Identifier" }] }, + { code: "function f(){ { var x; } x; }", errors: [{ message: "\"x\" used outside of binding context.", type: "Identifier" }] }, + { code: "function f() { var a; { var b = 0; } a = b; }", errors: [{ message: "\"b\" used outside of binding context.", type: "Identifier" }] }, + { code: "function f() { try { var a = 0; } catch (e) { var b = a; } }", errors: [{ message: "\"a\" used outside of binding context.", type: "Identifier" }] }, + { code: "var eslint = require('eslint');", globals: {}, errors: [{ message: "\"require\" used outside of binding context.", type: "Identifier" }] }, + { code: "function f(a) { return a[b]; }", errors: [{ message: "\"b\" used outside of binding context.", type: "Identifier" }] }, + { code: "function f() { return b.a; }", errors: [{ message: "\"b\" used outside of binding context.", type: "Identifier" }] }, + { code: "var a = { foo: bar };", errors: [{ message: "\"bar\" used outside of binding context.", type: "Identifier" }] }, + { code: "var a = { foo: foo };", errors: [{ message: "\"foo\" used outside of binding context.", type: "Identifier" }] }, + { code: "var a = { bar: 7, foo: bar };", errors: [{ message: "\"bar\" used outside of binding context.", type: "Identifier" }] }, + { code: "var a = arguments;", errors: [{ message: "\"arguments\" used outside of binding context.", type: "Identifier" }] }, + { code: "function x(){}; var a = arguments;", errors: [{ message: "\"arguments\" used outside of binding context.", type: "Identifier" }] }, + { code: "function z(b){}; var a = b;", errors: [{ message: "\"b\" used outside of binding context.", type: "Identifier" }] }, + { code: "function z(){var b;}; var a = b;", errors: [{ message: "\"b\" used outside of binding context.", type: "Identifier" }] }, + { code: "function f(){ try{}catch(e){} e }", errors: [{ message: "\"e\" used outside of binding context.", type: "Identifier" }] }, + { code: "a:b;", errors: [{ message: "\"b\" used outside of binding context.", type: "Identifier" }] }, + { + code: "function a() { for(var b in {}) { var c = b; } c; }", + errors: [{ message: "\"c\" used outside of binding context.", type: "Identifier" }] + }, + { + code: "function a() { for(var b of {}) { var c = b;} c; }", + ecmaFeatures: { forOf: true }, + errors: [{ message: "\"c\" used outside of binding context.", type: "Identifier" }] + } + ] +});