UNPKG

12.6 kBJavaScriptView Raw
1/**
2 * Requires identifiers to be camelCased or UPPERCASE_WITH_UNDERSCORES
3 *
4 * Types: `Boolean` or `String` or `Object`
5 *
6 * Values:
7 *
8 * - `true`
9 * - `"ignoreProperties"` allows an exception for object property names. Deprecated, Please use the `Object` value
10 * - `Object`:
11 * - `ignoreProperties`: boolean that allows an exception for object property names
12 * - `strict`: boolean that forces the first character to not be capitalized
13 * - `allowedPrefixes`: array of String, RegExp, or ESTree RegExpLiteral values permitted as prefixes
14 * - `allowedSuffixes`: array of String, RegExp, or ESTree RegExpLiteral values permitted as suffixes
15 * - `allExcept`: array of String, RegExp, or ESTree RegExpLiteral values permitted as exceptions
16 *
17 * JSHint: [`camelcase`](http://jshint.com/docs/options/#camelcase)
18 *
19 * #### Example
20 *
21 * ```js
22 * "requireCamelCaseOrUpperCaseIdentifiers": true
23 *
24 * "requireCamelCaseOrUpperCaseIdentifiers": {"ignoreProperties": true, "strict": true}
25 *
26 * "requireCamelCaseOrUpperCaseIdentifiers": {"allowedPrefixes": ["opt_", /pfx\d+_/]}
27 *
28 * "requireCamelCaseOrUpperCaseIdentifiers": {"allowedSuffixes": ["_dCel", {regex: {pattern: "_[kMG]?Hz"}}]}
29 *
30 * "requireCamelCaseOrUpperCaseIdentifiers": {"allExcept": ["var_args", {regex: {pattern: "^ignore", flags: "i"}}]}
31 * ```
32 *
33 * ##### Valid for mode `true`
34 *
35 * ```js
36 * var camelCase = 0;
37 * var CamelCase = 1;
38 * var _camelCase = 2;
39 * var camelCase_ = 3;
40 * var UPPER_CASE = 4;
41 * ```
42 *
43 * ##### Invalid for mode `true`
44 *
45 * ```js
46 * var lower_case = 1;
47 * var Mixed_case = 2;
48 * var mixed_Case = 3;
49 * ```
50 *
51 * ##### Valid for mode `ignoreProperties`
52 *
53 * ```js
54 * var camelCase = 0;
55 * var CamelCase = 1;
56 * var _camelCase = 2;
57 * var camelCase_ = 3;
58 * var UPPER_CASE = 4;
59 * var obj.snake_case = 5;
60 * var camelCase = { snake_case: 6 };
61 * ```
62 *
63 * ##### Invalid for mode `ignoreProperties`
64 *
65 * ```js
66 * var lower_case = 1;
67 * var Mixed_case = 2;
68 * var mixed_Case = 3;
69 * var snake_case = { snake_case: 6 };
70 * ```
71 *
72 * ##### Valid for mode `strict`
73 *
74 * ```js
75 * var camelCase = 0;
76 * var _camelCase = 2;
77 * var camelCase_ = 3;
78 * var UPPER_CASE = 4;
79 * var obj.snake_case = 5;
80 * var camelCase = { snake_case: 6 };
81 * ```
82 *
83 * ##### Invalid for mode `strict`
84 *
85 * ```js
86 * var Mixed_case = 2;
87 * var Snake_case = { snake_case: 6 };
88 * var snake_case = { SnakeCase: 6 };
89 * ```
90 *
91 * ##### Valid for `{ allowedPrefix: ["opt_", /pfx\d+_/] }`
92 * ```js
93 * var camelCase = 0;
94 * var CamelCase = 1;
95 * var _camelCase = 2;
96 * var camelCase_ = 3;
97 * var UPPER_CASE = 4;
98 * var opt_camelCase = 5;
99 * var pfx32_camelCase = 6;
100 * ```
101 *
102 * ##### Invalid for `{ allowedPrefix: ["opt_", /pfx\d+/] }`
103 * ```js
104 * var lower_case = 1;
105 * var Mixed_case = 2;
106 * var mixed_Case = 3;
107 * var req_camelCase = 4;
108 * var pfx_CamelCase = 5;
109 * ```
110 *
111 * ##### Valid for `{ allowedSuffixes: ["_dCel", {regex:{pattern:"_[kMG]?Hz"}}] }`
112 * ```js
113 * var camelCase = 0;
114 * var CamelCase = 1;
115 * var _camelCase = 2;
116 * var camelCase_ = 3;
117 * var UPPER_CASE = 4;
118 * var camelCase_dCel = 5;
119 * var _camelCase_MHz = 6;
120 * ```
121 *
122 * ##### Invalid for `{ allowedSuffixes: ["_dCel", {regex:{pattern:"_[kMG]?Hz"}}] }`
123 * ```js
124 * var lower_case = 1;
125 * var Mixed_case = 2;
126 * var mixed_Case = 3;
127 * var camelCase_cCel = 4;
128 * var CamelCase_THz = 5;
129 * ```
130 *
131 * ##### Valid for `{ allExcept: ["var_args", {regex:{pattern:"^ignore",flags:"i"}}] }`
132 * ```js
133 * var camelCase = 0;
134 * var CamelCase = 1;
135 * var _camelCase = 2;
136 * var camelCase_ = 3;
137 * var UPPER_CASE = 4;
138 * var var_args = 5;
139 * var ignoreThis_Please = 6;
140 * var iGnOrEeThis_Too = 7;
141 * ```
142 *
143 * ##### Invalid for `{ allExcept: ["var_args", {regex:{pattern:"^ignore",flags:"i"}}] }`
144 * ```js
145 * var lower_case = 1;
146 * var Mixed_case = 2;
147 * var mixed_Case = 3;
148 * var var_arg = 4;
149 * var signore_per_favore = 5;
150 * ```
151 */
152
153var assert = require('assert');
154
155// Convert an array of String or RegExp or ESTree RegExpLiteral values
156// into an array of String or RegExp values. Returns falsy if the
157// input does not match expectations.
158function processArrayOfStringOrRegExp(iv) {
159 if (!Array.isArray(iv)) {
160 return;
161 }
162 var rv = [];
163 var i = 0;
164 while (rv && (i < iv.length)) {
165 var elt = iv[i];
166 if (typeof elt === 'string') {
167 // string values OK
168 rv.push(elt);
169 } else if (elt instanceof RegExp) {
170 // existing RegExp OK
171 rv.push(elt);
172 } else if (elt && (typeof elt === 'object')) {
173 try {
174 // ESTree RegExpLiteral ok if it produces RegExp
175 rv.push(new RegExp(elt.regex.pattern, elt.regex.flags || ''));
176 } catch (e) {
177 // Not a valid RegExpLiteral
178 rv = null;
179 }
180 } else {
181 // Unknown value
182 rv = null;
183 }
184 ++i;
185 }
186 return rv;
187}
188
189// Return undefined or the start of the unprefixed value.
190function startAfterStringPrefix(value, prefix) {
191 var start = prefix.length;
192 if (start >= value.length) {
193 return;
194 }
195 if (value.substr(0, prefix.length) !== prefix) {
196 return;
197 }
198 return start;
199}
200
201// Return undefined or the start of the unprefixed value.
202function startAfterRegExpPrefix(value, prefix) {
203 var match = prefix.exec(value);
204 if (!match) {
205 return;
206 }
207 if (match.index !== 0) {
208 return;
209 }
210 return match[0].length;
211}
212
213// Return undefined or the end of the unsuffixed value.
214function endBeforeStringSuffix(value, suffix) {
215 var ends = value.length - suffix.length;
216 if (ends <= 0) {
217 return;
218 }
219 if (value.substr(ends) !== suffix) {
220 return;
221 }
222 return ends;
223}
224
225// Return undefined or the end of the unsuffixed value.
226function endBeforeRegExpSuffix(value, suffix) {
227 var match = suffix.exec(value);
228 if (!match) {
229 return;
230 }
231 var ends = match.index;
232 if ((ends + match[0].length) !== value.length) {
233 return;
234 }
235 return ends;
236}
237
238// Return truthy iff the value matches the exception.
239function matchException(value, exception) {
240 if (typeof exception === 'string') {
241 return (exception === value);
242 }
243 return exception.test(value);
244}
245
246module.exports = function() {};
247
248module.exports.prototype = {
249
250 configure: function(options) {
251 if (typeof options !== 'object') {
252 assert(
253 options === true || options === 'ignoreProperties',
254 this.getOptionName() + ' option requires a true value or `ignoreProperties`'
255 );
256 var _options = {
257 ignoreProperties: options === 'ignoreProperties' ? true : false,
258 strict: false
259 };
260 return this.configure(_options);
261 }
262
263 assert(
264 !options.hasOwnProperty('ignoreProperties') || typeof options.ignoreProperties === 'boolean',
265 this.getOptionName() + ' option should have boolean value for ignoreProperties'
266 );
267 this._ignoreProperties = options.ignoreProperties;
268
269 assert(
270 !options.hasOwnProperty('strict') || typeof options.strict === 'boolean',
271 this.getOptionName() + ' option should have boolean value for strict'
272 );
273 this._strict = options.strict;
274
275 var asre = processArrayOfStringOrRegExp(options.allowedPrefixes);
276 assert(
277 !options.hasOwnProperty('allowedPrefixes') || asre,
278 this.getOptionName() + ' option should have array of string or RegExp for allowedPrefixes'
279 );
280 if (asre) {
281 this._allowedPrefixes = asre;
282 }
283
284 asre = processArrayOfStringOrRegExp(options.allowedSuffixes);
285 assert(
286 !options.hasOwnProperty('allowedSuffixes') || asre,
287 this.getOptionName() + ' option should have array of string or RegExp for allowedSuffixes'
288 );
289 if (asre) {
290 this._allowedSuffixes = asre;
291 }
292
293 asre = processArrayOfStringOrRegExp(options.allExcept);
294 assert(
295 !options.hasOwnProperty('allExcept') || asre,
296 this.getOptionName() + ' option should have array of string or RegExp for allExcept'
297 );
298 if (asre) {
299 this._allExcept = asre;
300 }
301
302 },
303
304 getOptionName: function() {
305 return 'requireCamelCaseOrUpperCaseIdentifiers';
306 },
307
308 check: function(file, errors) {
309 file.iterateTokensByType('Identifier', function(token) {
310 var value = token.value;
311
312 // Leading and trailing underscores signify visibility/scope and do not affect
313 // validation of the rule. Remove them to simplify the checks.
314 var isPrivate = (value[0] === '_');
315 value = value.replace(/^_+|_+$/g, '');
316
317 // Detect exceptions before stripping prefixes/suffixes.
318 if (this._allExcept) {
319 for (i = 0, len = this._allExcept.length; i < len; ++i) {
320 if (matchException(value, this._allExcept[i])) {
321 return;
322 }
323 }
324 }
325
326 // Strip at most one prefix permitted text from the identifier. This transformation
327 // cannot change an acceptable identifier into an unacceptable identifier so we can
328 // continue with the normal verification of whatever it produces.
329 var i;
330 var len;
331 if (this._allowedPrefixes) {
332 for (i = 0, len = this._allowedPrefixes.length; i < len; ++i) {
333 var prefix = this._allowedPrefixes[i];
334 var start;
335 if (typeof prefix === 'string') {
336 start = startAfterStringPrefix(value, prefix);
337 } else {
338 start = startAfterRegExpPrefix(value, prefix);
339 }
340 if (start !== undefined) {
341 value = value.substr(start);
342 break;
343 }
344 }
345 }
346
347 // As with prefix but for one suffix permitted text.
348 if (this._allowedSuffixes) {
349 for (i = 0, len = this._allowedSuffixes.length; i < len; ++i) {
350 var suffix = this._allowedSuffixes[i];
351 var ends;
352 if (typeof suffix === 'string') {
353 ends = endBeforeStringSuffix(value, suffix);
354 } else {
355 ends = endBeforeRegExpSuffix(value, suffix);
356 }
357 if (ends !== undefined) {
358 value = value.substr(0, ends);
359 break;
360 }
361 }
362 }
363
364 if (value.indexOf('_') === -1 || value.toUpperCase() === value) {
365 if (!this._strict) {return;}
366 if (value.length === 0 || value[0].toUpperCase() !== value[0] || isPrivate) {
367 return;
368 }
369 }
370 if (this._ignoreProperties) {
371 var nextToken = file.getNextToken(token);
372 var prevToken = token.getPreviousCodeToken();
373
374 if (nextToken && nextToken.value === ':') {
375 return;
376 }
377
378 /* This enables an identifier to be snake cased via the object
379 * destructuring pattern. We must check to see if the identifier
380 * is being used to set values into an object to determine if
381 * this is a legal assignment.
382 * Example: ({camelCase: snake_case}) => camelCase.length
383 */
384 if (prevToken && prevToken.value === ':') {
385 var node = token.parentElement;
386 var parentElement = node.parentElement;
387 if (parentElement && parentElement.type === 'ObjectProperty') {
388 var grandpa = parentElement.parentElement;
389 if (grandpa && grandpa.type === 'ObjectPattern') {
390 return;
391 }
392 }
393 }
394
395 if (prevToken && (prevToken.value === '.' ||
396 prevToken.value === 'get' || prevToken.value === 'set')) {
397 return;
398 }
399 }
400 errors.add(
401 'All identifiers must be camelCase or UPPER_CASE',
402 token
403 );
404 }.bind(this));
405 }
406
407};