diff options
Diffstat (limited to 'tools/eslint/lib/rules/new-cap.js')
-rw-r--r-- | tools/eslint/lib/rules/new-cap.js | 197 |
1 files changed, 197 insertions, 0 deletions
diff --git a/tools/eslint/lib/rules/new-cap.js b/tools/eslint/lib/rules/new-cap.js new file mode 100644 index 0000000000..1474fc8028 --- /dev/null +++ b/tools/eslint/lib/rules/new-cap.js @@ -0,0 +1,197 @@ +/** + * @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. + */ + +"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 ? false : true; + config.capIsNew = config.capIsNew === false ? false : true; + + 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"; + } + } + + /** + * 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" || 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; +}; |