2 * @fileoverview Rule to flag use of constructors without capital letters
3 * @author Nicholas C. Zakas
4 * @copyright 2014 Jordan Harband. All rights reserved.
5 * @copyright 2013-2014 Nicholas C. Zakas. All rights reserved.
24 * Ensure that if the key is provided, it must be an array.
25 * @param {Object} obj Object to check with `key`.
26 * @param {string} key Object key to check on `obj`.
27 * @param {*} fallback If obj[key] is not present, this will be returned.
28 * @returns {string[]} Returns obj[key] if it's an Array, otherwise `fallback`
30 function checkArray(obj, key, fallback) {
31 if (Object.prototype.hasOwnProperty.call(obj, key) && !Array.isArray(obj[key])) {
32 throw new TypeError(key + ", if provided, must be an Array");
34 return obj[key] || fallback;
38 * A reducer function to invert an array to an Object mapping the string form of the key, to `true`.
39 * @param {Object} map Accumulator object for the reduce.
40 * @param {string} key Object key to set to `true`.
41 * @returns {Object} Returns the updated Object for further reduction.
43 function invert(map, key) {
49 * Creates an object with the cap is new exceptions as its keys and true as their values.
50 * @param {Object} config Rule configuration
51 * @returns {Object} Object with cap is new exceptions.
53 function calculateCapIsNewExceptions(config) {
54 var capIsNewExceptions = checkArray(config, "capIsNewExceptions", CAPS_ALLOWED);
56 if (capIsNewExceptions !== CAPS_ALLOWED) {
57 capIsNewExceptions = capIsNewExceptions.concat(CAPS_ALLOWED);
60 return capIsNewExceptions.reduce(invert, {});
63 //------------------------------------------------------------------------------
65 //------------------------------------------------------------------------------
67 module.exports = function(context) {
69 var config = context.options[0] || {};
70 config.newIsCap = config.newIsCap === false ? false : true;
71 config.capIsNew = config.capIsNew === false ? false : true;
73 var newIsCapExceptions = checkArray(config, "newIsCapExceptions", []).reduce(invert, {});
75 var capIsNewExceptions = calculateCapIsNewExceptions(config);
79 //--------------------------------------------------------------------------
81 //--------------------------------------------------------------------------
84 * Get exact callee name from expression
85 * @param {ASTNode} node CallExpression or NewExpression node
86 * @returns {string} name
88 function extractNameFromExpression(node) {
93 if (node.callee.type === "MemberExpression") {
94 property = node.callee.property;
96 if (property.type === "Literal" && (typeof property.value === "string")) {
97 name = property.value;
98 } else if (property.type === "Identifier" && !node.callee.computed) {
102 name = node.callee.name;
108 * Returns the capitalization state of the string -
109 * Whether the first character is uppercase, lowercase, or non-alphabetic
110 * @param {string} str String
111 * @returns {string} capitalization state: "non-alpha", "lower", or "upper"
113 function getCap(str) {
114 var firstChar = str.charAt(0);
116 var firstCharLower = firstChar.toLowerCase();
117 var firstCharUpper = firstChar.toUpperCase();
119 if (firstCharLower === firstCharUpper) {
120 // char has no uppercase variant, so it's non-alphabetic
122 } else if (firstChar === firstCharLower) {
130 * Check if capitalization is allowed for a CallExpression
131 * @param {Object} allowedMap Object mapping calleeName to a Boolean
132 * @param {ASTNode} node CallExpression node
133 * @param {string} calleeName Capitalized callee name from a CallExpression
134 * @returns {Boolean} Returns true if the callee may be capitalized
136 function isCapAllowed(allowedMap, node, calleeName) {
137 if (allowedMap[calleeName]) {
140 if (calleeName === "UTC" && node.callee.type === "MemberExpression") {
141 // allow if callee is Date.UTC
142 return node.callee.object.type === "Identifier" &&
143 node.callee.object.name === "Date";
149 * Reports the given message for the given node. The location will be the start of the property or the callee.
150 * @param {ASTNode} node CallExpression or NewExpression node.
151 * @param {string} message The message to report.
154 function report(node, message) {
155 var callee = node.callee;
157 if (callee.type === "MemberExpression") {
158 callee = callee.property;
161 context.report(node, callee.loc.start, message);
164 //--------------------------------------------------------------------------
166 //--------------------------------------------------------------------------
168 if (config.newIsCap) {
169 listeners.NewExpression = function(node) {
171 var constructorName = extractNameFromExpression(node);
172 if (constructorName) {
173 var capitalization = getCap(constructorName);
174 var isAllowed = capitalization !== "lower" || isCapAllowed(newIsCapExceptions, node, constructorName);
176 report(node, "A constructor name should not start with a lowercase letter.");
182 if (config.capIsNew) {
183 listeners.CallExpression = function(node) {
185 var calleeName = extractNameFromExpression(node);
187 var capitalization = getCap(calleeName);
188 var isAllowed = capitalization !== "upper" || isCapAllowed(capIsNewExceptions, node, calleeName);
190 report(node, "A function with a name starting with an uppercase letter should only be used as a constructor.");