Skip to content
This repository was archived by the owner on Mar 23, 2024. It is now read-only.

Commit c1d277c

Browse files
pabigotmarkelog
authored andcommitted
requireCamelCaseOrUpperCaseIdentifiers: add exception options
Fixes #1900 Closes gh-2071
1 parent 72f9cb5 commit c1d277c

File tree

2 files changed

+528
-5
lines changed

2 files changed

+528
-5
lines changed

lib/rules/require-camelcase-or-uppercase-identifiers.js

Lines changed: 250 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
* - `Object`:
1111
* - `ignoreProperties`: boolean that allows an exception for object property names
1212
* - `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
1316
*
1417
* JSHint: [`camelcase`](http://jshint.com/docs/options/#camelcase)
1518
*
@@ -19,6 +22,12 @@
1922
* "requireCamelCaseOrUpperCaseIdentifiers": true
2023
*
2124
* "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"}}]}
2231
* ```
2332
*
2433
* ##### Valid for mode `true`
@@ -79,10 +88,161 @@
7988
* var snake_case = { SnakeCase: 6 };
8089
* ```
8190
*
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+
* ```
82151
*/
83152

84153
var assert = require('assert');
85154

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.
158+
function 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.
190+
function 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.
202+
function 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.
214+
function 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.
226+
function 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.
239+
function matchException(value, exception) {
240+
if (typeof exception === 'string') {
241+
return (exception === value);
242+
}
243+
return exception.test(value);
244+
}
245+
86246
module.exports = function() {};
87247

88248
module.exports.prototype = {
@@ -101,11 +261,44 @@ module.exports.prototype = {
101261
}
102262

103263
assert(
104-
typeof options.ignoreProperties === 'boolean' || typeof options.strict === 'boolean',
105-
this.getOptionName() + ' option should have boolean values for ignoreProperties and/or strict'
264+
!options.hasOwnProperty('ignoreProperties') || typeof options.ignoreProperties === 'boolean',
265+
this.getOptionName() + ' option should have boolean value for ignoreProperties'
106266
);
107267
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+
);
108273
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+
109302
},
110303

111304
getOptionName: function() {
@@ -115,9 +308,62 @@ module.exports.prototype = {
115308
check: function(file, errors) {
116309
file.iterateTokensByType('Identifier', function(token) {
117310
var value = token.value;
118-
if (value.replace(/^_+|_+$/g, '').indexOf('_') === -1 || value.toUpperCase() === 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) {
119365
if (!this._strict) {return;}
120-
if (value[0].toUpperCase() !== value[0] || value[0] === '_') {
366+
if (value.length === 0 || value[0].toUpperCase() !== value[0] || isPrivate) {
121367
return;
122368
}
123369
}

0 commit comments

Comments
 (0)