diff --git a/eslint/babel-eslint-plugin/README.md b/eslint/babel-eslint-plugin/README.md index d2496f5679..94db5b984d 100644 --- a/eslint/babel-eslint-plugin/README.md +++ b/eslint/babel-eslint-plugin/README.md @@ -26,6 +26,7 @@ Finally enable all the rules you like to use (remember to disable the originals "babel/object-shorthand": 1, "babel/generator-star": 1, "babel/generator-star-spacing": 1, + "babel/new-cap": 1, } } ``` @@ -36,3 +37,4 @@ Each rule cooresponds to a core eslint rule, and has the same options. - `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 +- `babel/new-cap`: Ignores capitalized decorators (`@Decorator`) diff --git a/eslint/babel-eslint-plugin/index.js b/eslint/babel-eslint-plugin/index.js index e2cc71b923..6c76dbe4cd 100644 --- a/eslint/babel-eslint-plugin/index.js +++ b/eslint/babel-eslint-plugin/index.js @@ -4,11 +4,13 @@ module.exports = { rules: { 'object-shorthand': require('./rules/object-shorthand'), 'generator-star-spacing': require('./rules/generator-star-spacing'), - 'generator-star': require('./rules/generator-star') + 'generator-star': require('./rules/generator-star'), + 'new-cap': require('./rules/new-cap') }, rulesConfig: { 'generator-star-spacing': 0, 'generator-star': 0, - 'object-shorthand': 0 + 'object-shorthand': 0, + 'new-cap': 0 } }; diff --git a/eslint/babel-eslint-plugin/rules/new-cap.js b/eslint/babel-eslint-plugin/rules/new-cap.js new file mode 100644 index 0000000000..b1ab732d87 --- /dev/null +++ b/eslint/babel-eslint-plugin/rules/new-cap.js @@ -0,0 +1,234 @@ +/** + * @fileoverview Rule to flag use of constructors without capital letters + * @author Nicholas C. Zakas + * @copyright 2014 Jordan Harband. All rights reserved. + * @copyright 2013-2014 Nicholas C. Zakas. All rights reserved. + * @copyright 2015 Mathieu M-Gosselin. All rights reserved. + */ + +"use strict"; + +var CAPS_ALLOWED = [ + "Array", + "Boolean", + "Date", + "Error", + "Function", + "Number", + "Object", + "RegExp", + "String", + "Symbol" +]; + +/** + * Ensure that if the key is provided, it must be an array. + * @param {Object} obj Object to check with `key`. + * @param {string} key Object key to check on `obj`. + * @param {*} fallback If obj[key] is not present, this will be returned. + * @returns {string[]} Returns obj[key] if it's an Array, otherwise `fallback` + */ +function checkArray(obj, key, fallback) { + if (Object.prototype.hasOwnProperty.call(obj, key) && !Array.isArray(obj[key])) { + throw new TypeError(key + ", if provided, must be an Array"); + } + return obj[key] || fallback; +} + +/** + * A reducer function to invert an array to an Object mapping the string form of the key, to `true`. + * @param {Object} map Accumulator object for the reduce. + * @param {string} key Object key to set to `true`. + * @returns {Object} Returns the updated Object for further reduction. + */ +function invert(map, key) { + map[key] = true; + return map; +} + +/** + * Creates an object with the cap is new exceptions as its keys and true as their values. + * @param {Object} config Rule configuration + * @returns {Object} Object with cap is new exceptions. + */ +function calculateCapIsNewExceptions(config) { + var capIsNewExceptions = checkArray(config, "capIsNewExceptions", CAPS_ALLOWED); + + if (capIsNewExceptions !== CAPS_ALLOWED) { + capIsNewExceptions = capIsNewExceptions.concat(CAPS_ALLOWED); + } + + return capIsNewExceptions.reduce(invert, {}); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var config = context.options[0] || {}; + config.newIsCap = config.newIsCap !== false; + config.capIsNew = config.capIsNew !== false; + + var newIsCapExceptions = checkArray(config, "newIsCapExceptions", []).reduce(invert, {}); + + var capIsNewExceptions = calculateCapIsNewExceptions(config); + + var listeners = {}; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Get exact callee name from expression + * @param {ASTNode} node CallExpression or NewExpression node + * @returns {string} name + */ + function extractNameFromExpression(node) { + + var name = "", + property; + + if (node.callee.type === "MemberExpression") { + property = node.callee.property; + + if (property.type === "Literal" && (typeof property.value === "string")) { + name = property.value; + } else if (property.type === "Identifier" && !node.callee.computed) { + name = property.name; + } + } else { + name = node.callee.name; + } + return name; + } + + /** + * Returns the capitalization state of the string - + * Whether the first character is uppercase, lowercase, or non-alphabetic + * @param {string} str String + * @returns {string} capitalization state: "non-alpha", "lower", or "upper" + */ + function getCap(str) { + var firstChar = str.charAt(0); + + var firstCharLower = firstChar.toLowerCase(); + var firstCharUpper = firstChar.toUpperCase(); + + if (firstCharLower === firstCharUpper) { + // char has no uppercase variant, so it's non-alphabetic + return "non-alpha"; + } else if (firstChar === firstCharLower) { + return "lower"; + } else { + return "upper"; + } + } + + /** + * Returns whether a node is under a decorator or not. + * @param {ASTNode} node CallExpression node + * @returns {Boolean} Returns true if the node is under a decorator. + */ + function isDecorator(node) { + return node.parent.type === "Decorator"; + } + + /** + * Check if capitalization is allowed for a CallExpression + * @param {Object} allowedMap Object mapping calleeName to a Boolean + * @param {ASTNode} node CallExpression node + * @param {string} calleeName Capitalized callee name from a CallExpression + * @returns {Boolean} Returns true if the callee may be capitalized + */ + function isCapAllowed(allowedMap, node, calleeName) { + if (allowedMap[calleeName]) { + return true; + } + if (calleeName === "UTC" && node.callee.type === "MemberExpression") { + // allow if callee is Date.UTC + return node.callee.object.type === "Identifier" && + node.callee.object.name === "Date"; + } + return false; + } + + /** + * Reports the given message for the given node. The location will be the start of the property or the callee. + * @param {ASTNode} node CallExpression or NewExpression node. + * @param {string} message The message to report. + * @returns {void} + */ + function report(node, message) { + var callee = node.callee; + + if (callee.type === "MemberExpression") { + callee = callee.property; + } + + context.report(node, callee.loc.start, message); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + if (config.newIsCap) { + listeners.NewExpression = function(node) { + + var constructorName = extractNameFromExpression(node); + if (constructorName) { + var capitalization = getCap(constructorName); + var isAllowed = capitalization !== "lower" || isCapAllowed(newIsCapExceptions, node, constructorName); + if (!isAllowed) { + report(node, "A constructor name should not start with a lowercase letter."); + } + } + }; + } + + if (config.capIsNew) { + listeners.CallExpression = function(node) { + + var calleeName = extractNameFromExpression(node); + if (calleeName) { + var capitalization = getCap(calleeName); + var isAllowed = capitalization !== "upper" || isDecorator(node) || isCapAllowed(capIsNewExceptions, node, calleeName); + if (!isAllowed) { + report(node, "A function with a name starting with an uppercase letter should only be used as a constructor."); + } + } + }; + } + + return listeners; +}; + +module.exports.schema = [ + { + "type": "object", + "properties": { + "newIsCap": { + "type": "boolean" + }, + "capIsNew": { + "type": "boolean" + }, + "newIsCapExceptions": { + "type": "array", + "items": { + "type": "string" + } + }, + "capIsNewExceptions": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } +]; diff --git a/eslint/babel-eslint-plugin/tests/new-cap.js b/eslint/babel-eslint-plugin/tests/new-cap.js new file mode 100644 index 0000000000..d25f5d2ad3 --- /dev/null +++ b/eslint/babel-eslint-plugin/tests/new-cap.js @@ -0,0 +1,122 @@ +/* eslint-disable */ + +/** + * @fileoverview Tests for new-cap rule. + * @author Nicholas C. Zakas + */ + +var linter = require('eslint').linter + , ESLintTester = require('eslint-tester') + , eslintTester = new ESLintTester(linter); + + +eslintTester.addRuleTest("rules/new-cap", { + valid: [ + // Original test cases. + "var x = new Constructor();", + "var x = new a.b.Constructor();", + "var x = new a.b['Constructor']();", + "var x = new a.b[Constructor]();", + "var x = new a.b[constructor]();", + "var x = new function(){};", + "var x = new _;", + "var x = new $;", + "var x = new Σ;", + "var x = new _x;", + "var x = new $x;", + "var x = new this;", + "var x = Array(42)", + "var x = Boolean(42)", + "var x = Date(42)", + "var x = Date.UTC(2000, 0)", + "var x = Error('error')", + "var x = Function('return 0')", + "var x = Number(42)", + "var x = Object(null)", + "var x = RegExp(42)", + "var x = String(42)", + "var x = Symbol('symbol')", + "var x = _();", + "var x = $();", + { code: "var x = Foo(42)", args: [1, {"capIsNew": false}] }, + { code: "var x = bar.Foo(42)", args: [1, {"capIsNew": false}] }, + "var x = bar[Foo](42)", + {code: "var x = bar['Foo'](42)", args: [1, {"capIsNew": false}] }, + "var x = Foo.bar(42)", + { code: "var x = new foo(42)", args: [1, {"newIsCap": false}] }, + "var o = { 1: function () {} }; o[1]();", + "var o = { 1: function () {} }; new o[1]();", + { code: "var x = Foo(42);", args: [1, { capIsNew: true, capIsNewExceptions: ["Foo"] }] }, + { code: "var x = new foo(42);", args: [1, { newIsCap: true, newIsCapExceptions: ["foo"] }] }, + { code: "var x = Object(42);", args: [1, { capIsNewExceptions: ["Foo"] }] }, + + // Babel-specific test cases. + { code: "@MyDecorator(123) class MyClass{}", parser: "babel-eslint" }, + ], + invalid: [ + { code: "var x = new c();", errors: [{ message: "A constructor name should not start with a lowercase letter.", type: "NewExpression"}] }, + { code: "var x = new φ;", errors: [{ message: "A constructor name should not start with a lowercase letter.", type: "NewExpression"}] }, + { code: "var x = new a.b.c;", errors: [{ message: "A constructor name should not start with a lowercase letter.", type: "NewExpression"}] }, + { code: "var x = new a.b['c'];", errors: [{ message: "A constructor name should not start with a lowercase letter.", type: "NewExpression"}] }, + { code: "var b = Foo();", errors: [{ message: "A function with a name starting with an uppercase letter should only be used as a constructor.", type: "CallExpression"}] }, + { code: "var b = a.Foo();", errors: [{ message: "A function with a name starting with an uppercase letter should only be used as a constructor.", type: "CallExpression"}] }, + { code: "var b = a['Foo']();", errors: [{ message: "A function with a name starting with an uppercase letter should only be used as a constructor.", type: "CallExpression"}] }, + { code: "var b = a.Date.UTC();", errors: [{ message: "A function with a name starting with an uppercase letter should only be used as a constructor.", type: "CallExpression"}] }, + { code: "var b = UTC();", errors: [{ message: "A function with a name starting with an uppercase letter should only be used as a constructor.", type: "CallExpression"}] }, + { + code: "var a = B.C();", + errors: [ + { + message: "A function with a name starting with an uppercase letter should only be used as a constructor.", + type: "CallExpression", + line: 1, + column: 10 + } + ] + }, + { + code: "var a = B\n.C();", + errors: [ + { + message: "A function with a name starting with an uppercase letter should only be used as a constructor.", + type: "CallExpression", + line: 2, + column: 1 + } + ] + }, + { + code: "var a = new B.c();", + errors: [ + { + message: "A constructor name should not start with a lowercase letter.", + type: "NewExpression", + line: 1, + column: 14 + } + ] + }, + { + code: "var a = new B.\nc();", + errors: [ + { + message: "A constructor name should not start with a lowercase letter.", + type: "NewExpression", + line: 2, + column: 0 + } + ] + }, + { + code: "var a = new c();", + errors: [ + { + message: "A constructor name should not start with a lowercase letter.", + type: "NewExpression", + line: 1, + column: 12 + } + ] + } + ] +});