1474fc80289cbb7412ffa9b251a9ef4e31b8bbc7
[platform/framework/web/crosswalk-tizen.git] /
1 /**
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.
6  */
7
8 "use strict";
9
10 var CAPS_ALLOWED = [
11     "Array",
12     "Boolean",
13     "Date",
14     "Error",
15     "Function",
16     "Number",
17     "Object",
18     "RegExp",
19     "String",
20     "Symbol"
21 ];
22
23 /**
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`
29  */
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");
33     }
34     return obj[key] || fallback;
35 }
36
37 /**
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.
42  */
43 function invert(map, key) {
44     map[key] = true;
45     return map;
46 }
47
48 /**
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.
52  */
53 function calculateCapIsNewExceptions(config) {
54     var capIsNewExceptions = checkArray(config, "capIsNewExceptions", CAPS_ALLOWED);
55
56     if (capIsNewExceptions !== CAPS_ALLOWED) {
57         capIsNewExceptions = capIsNewExceptions.concat(CAPS_ALLOWED);
58     }
59
60     return capIsNewExceptions.reduce(invert, {});
61 }
62
63 //------------------------------------------------------------------------------
64 // Rule Definition
65 //------------------------------------------------------------------------------
66
67 module.exports = function(context) {
68
69     var config = context.options[0] || {};
70     config.newIsCap = config.newIsCap === false ? false : true;
71     config.capIsNew = config.capIsNew === false ? false : true;
72
73     var newIsCapExceptions = checkArray(config, "newIsCapExceptions", []).reduce(invert, {});
74
75     var capIsNewExceptions = calculateCapIsNewExceptions(config);
76
77     var listeners = {};
78
79     //--------------------------------------------------------------------------
80     // Helpers
81     //--------------------------------------------------------------------------
82
83     /**
84      * Get exact callee name from expression
85      * @param {ASTNode} node CallExpression or NewExpression node
86      * @returns {string} name
87      */
88     function extractNameFromExpression(node) {
89
90         var name = "",
91             property;
92
93         if (node.callee.type === "MemberExpression") {
94             property = node.callee.property;
95
96             if (property.type === "Literal" && (typeof property.value === "string")) {
97                 name = property.value;
98             } else if (property.type === "Identifier" && !node.callee.computed) {
99                 name = property.name;
100             }
101         } else {
102             name = node.callee.name;
103         }
104         return name;
105     }
106
107     /**
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"
112      */
113     function getCap(str) {
114         var firstChar = str.charAt(0);
115
116         var firstCharLower = firstChar.toLowerCase();
117         var firstCharUpper = firstChar.toUpperCase();
118
119         if (firstCharLower === firstCharUpper) {
120             // char has no uppercase variant, so it's non-alphabetic
121             return "non-alpha";
122         } else if (firstChar === firstCharLower) {
123             return "lower";
124         } else {
125             return "upper";
126         }
127     }
128
129     /**
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
135      */
136     function isCapAllowed(allowedMap, node, calleeName) {
137         if (allowedMap[calleeName]) {
138             return true;
139         }
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";
144         }
145         return false;
146     }
147
148     /**
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.
152      * @returns {void}
153      */
154     function report(node, message) {
155         var callee = node.callee;
156
157         if (callee.type === "MemberExpression") {
158             callee = callee.property;
159         }
160
161         context.report(node, callee.loc.start, message);
162     }
163
164     //--------------------------------------------------------------------------
165     // Public
166     //--------------------------------------------------------------------------
167
168     if (config.newIsCap) {
169         listeners.NewExpression = function(node) {
170
171             var constructorName = extractNameFromExpression(node);
172             if (constructorName) {
173                 var capitalization = getCap(constructorName);
174                 var isAllowed = capitalization !== "lower" || isCapAllowed(newIsCapExceptions, node, constructorName);
175                 if (!isAllowed) {
176                     report(node, "A constructor name should not start with a lowercase letter.");
177                 }
178             }
179         };
180     }
181
182     if (config.capIsNew) {
183         listeners.CallExpression = function(node) {
184
185             var calleeName = extractNameFromExpression(node);
186             if (calleeName) {
187                 var capitalization = getCap(calleeName);
188                 var isAllowed = capitalization !== "upper" || isCapAllowed(capIsNewExceptions, node, calleeName);
189                 if (!isAllowed) {
190                     report(node, "A function with a name starting with an uppercase letter should only be used as a constructor.");
191                 }
192             }
193         };
194     }
195
196     return listeners;
197 };