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

Commit

Permalink
requireCamelCaseOrUpperCaseIdentifiers: add exception options
Browse files Browse the repository at this point in the history
Fixes #1900
Closes gh-2071
  • Loading branch information
pabigot authored and markelog committed Jan 24, 2016
1 parent 72f9cb5 commit c1d277c
Show file tree
Hide file tree
Showing 2 changed files with 528 additions and 5 deletions.
254 changes: 250 additions & 4 deletions lib/rules/require-camelcase-or-uppercase-identifiers.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
* - `Object`:
* - `ignoreProperties`: boolean that allows an exception for object property names
* - `strict`: boolean that forces the first character to not be capitalized
* - `allowedPrefixes`: array of String, RegExp, or ESTree RegExpLiteral values permitted as prefixes
* - `allowedSuffixes`: array of String, RegExp, or ESTree RegExpLiteral values permitted as suffixes
* - `allExcept`: array of String, RegExp, or ESTree RegExpLiteral values permitted as exceptions
*
* JSHint: [`camelcase`](http://jshint.com/docs/options/#camelcase)
*
Expand All @@ -19,6 +22,12 @@
* "requireCamelCaseOrUpperCaseIdentifiers": true
*
* "requireCamelCaseOrUpperCaseIdentifiers": {"ignoreProperties": true, "strict": true}
*
* "requireCamelCaseOrUpperCaseIdentifiers": {"allowedPrefixes": ["opt_", /pfx\d+_/]}
*
* "requireCamelCaseOrUpperCaseIdentifiers": {"allowedSuffixes": ["_dCel", {regex: {pattern: "_[kMG]?Hz"}}]}
*
* "requireCamelCaseOrUpperCaseIdentifiers": {"allExcept": ["var_args", {regex: {pattern: "^ignore", flags: "i"}}]}
* ```
*
* ##### Valid for mode `true`
Expand Down Expand Up @@ -79,10 +88,161 @@
* var snake_case = { SnakeCase: 6 };
* ```
*
* ##### Valid for `{ allowedPrefix: ["opt_", /pfx\d+_/] }`
* ```js
* var camelCase = 0;
* var CamelCase = 1;
* var _camelCase = 2;
* var camelCase_ = 3;
* var UPPER_CASE = 4;
* var opt_camelCase = 5;
* var pfx32_camelCase = 6;
* ```
*
* ##### Invalid for `{ allowedPrefix: ["opt_", /pfx\d+/] }`
* ```js
* var lower_case = 1;
* var Mixed_case = 2;
* var mixed_Case = 3;
* var req_camelCase = 4;
* var pfx_CamelCase = 5;
* ```
*
* ##### Valid for `{ allowedSuffixes: ["_dCel", {regex:{pattern:"_[kMG]?Hz"}}] }`
* ```js
* var camelCase = 0;
* var CamelCase = 1;
* var _camelCase = 2;
* var camelCase_ = 3;
* var UPPER_CASE = 4;
* var camelCase_dCel = 5;
* var _camelCase_MHz = 6;
* ```
*
* ##### Invalid for `{ allowedSuffixes: ["_dCel", {regex:{pattern:"_[kMG]?Hz"}}] }`
* ```js
* var lower_case = 1;
* var Mixed_case = 2;
* var mixed_Case = 3;
* var camelCase_cCel = 4;
* var CamelCase_THz = 5;
* ```
*
* ##### Valid for `{ allExcept: ["var_args", {regex:{pattern:"^ignore",flags:"i"}}] }`
* ```js
* var camelCase = 0;
* var CamelCase = 1;
* var _camelCase = 2;
* var camelCase_ = 3;
* var UPPER_CASE = 4;
* var var_args = 5;
* var ignoreThis_Please = 6;
* var iGnOrEeThis_Too = 7;
* ```
*
* ##### Invalid for `{ allExcept: ["var_args", {regex:{pattern:"^ignore",flags:"i"}}] }`
* ```js
* var lower_case = 1;
* var Mixed_case = 2;
* var mixed_Case = 3;
* var var_arg = 4;
* var signore_per_favore = 5;
* ```
*/

var assert = require('assert');

// Convert an array of String or RegExp or ESTree RegExpLiteral values
// into an array of String or RegExp values. Returns falsy if the
// input does not match expectations.
function processArrayOfStringOrRegExp(iv) {
if (!Array.isArray(iv)) {
return;
}
var rv = [];
var i = 0;
while (rv && (i < iv.length)) {
var elt = iv[i];
if (typeof elt === 'string') {
// string values OK
rv.push(elt);
} else if (elt instanceof RegExp) {
// existing RegExp OK
rv.push(elt);
} else if (elt && (typeof elt === 'object')) {
try {
// ESTree RegExpLiteral ok if it produces RegExp
rv.push(new RegExp(elt.regex.pattern, elt.regex.flags || ''));
} catch (e) {
// Not a valid RegExpLiteral
rv = null;
}
} else {
// Unknown value
rv = null;
}
++i;
}
return rv;
}

// Return undefined or the start of the unprefixed value.
function startAfterStringPrefix(value, prefix) {
var start = prefix.length;
if (start >= value.length) {
return;
}
if (value.substr(0, prefix.length) !== prefix) {
return;
}
return start;
}

// Return undefined or the start of the unprefixed value.
function startAfterRegExpPrefix(value, prefix) {
var match = prefix.exec(value);
if (!match) {
return;
}
if (match.index !== 0) {
return;
}
return match[0].length;
}

// Return undefined or the end of the unsuffixed value.
function endBeforeStringSuffix(value, suffix) {
var ends = value.length - suffix.length;
if (ends <= 0) {
return;
}
if (value.substr(ends) !== suffix) {
return;
}
return ends;
}

// Return undefined or the end of the unsuffixed value.
function endBeforeRegExpSuffix(value, suffix) {
var match = suffix.exec(value);
if (!match) {
return;
}
var ends = match.index;
if ((ends + match[0].length) !== value.length) {
return;
}
return ends;
}

// Return truthy iff the value matches the exception.
function matchException(value, exception) {
if (typeof exception === 'string') {
return (exception === value);
}
return exception.test(value);
}

module.exports = function() {};

module.exports.prototype = {
Expand All @@ -101,11 +261,44 @@ module.exports.prototype = {
}

assert(
typeof options.ignoreProperties === 'boolean' || typeof options.strict === 'boolean',
this.getOptionName() + ' option should have boolean values for ignoreProperties and/or strict'
!options.hasOwnProperty('ignoreProperties') || typeof options.ignoreProperties === 'boolean',
this.getOptionName() + ' option should have boolean value for ignoreProperties'
);
this._ignoreProperties = options.ignoreProperties;

assert(
!options.hasOwnProperty('strict') || typeof options.strict === 'boolean',
this.getOptionName() + ' option should have boolean value for strict'
);
this._strict = options.strict;

var asre = processArrayOfStringOrRegExp(options.allowedPrefixes);
assert(
!options.hasOwnProperty('allowedPrefixes') || asre,
this.getOptionName() + ' option should have array of string or RegExp for allowedPrefixes'
);
if (asre) {
this._allowedPrefixes = asre;
}

asre = processArrayOfStringOrRegExp(options.allowedSuffixes);
assert(
!options.hasOwnProperty('allowedSuffixes') || asre,
this.getOptionName() + ' option should have array of string or RegExp for allowedSuffixes'
);
if (asre) {
this._allowedSuffixes = asre;
}

asre = processArrayOfStringOrRegExp(options.allExcept);
assert(
!options.hasOwnProperty('allExcept') || asre,
this.getOptionName() + ' option should have array of string or RegExp for allExcept'
);
if (asre) {
this._allExcept = asre;
}

},

getOptionName: function() {
Expand All @@ -115,9 +308,62 @@ module.exports.prototype = {
check: function(file, errors) {
file.iterateTokensByType('Identifier', function(token) {
var value = token.value;
if (value.replace(/^_+|_+$/g, '').indexOf('_') === -1 || value.toUpperCase() === value) {

// Leading and trailing underscores signify visibility/scope and do not affect
// validation of the rule. Remove them to simplify the checks.
var isPrivate = (value[0] === '_');
value = value.replace(/^_+|_+$/g, '');

// Detect exceptions before stripping prefixes/suffixes.
if (this._allExcept) {
for (i = 0, len = this._allExcept.length; i < len; ++i) {
if (matchException(value, this._allExcept[i])) {
return;
}
}
}

// Strip at most one prefix permitted text from the identifier. This transformation
// cannot change an acceptable identifier into an unacceptable identifier so we can
// continue with the normal verification of whatever it produces.
var i;
var len;
if (this._allowedPrefixes) {
for (i = 0, len = this._allowedPrefixes.length; i < len; ++i) {
var prefix = this._allowedPrefixes[i];
var start;
if (typeof prefix === 'string') {
start = startAfterStringPrefix(value, prefix);
} else {
start = startAfterRegExpPrefix(value, prefix);
}
if (start !== undefined) {
value = value.substr(start);
break;
}
}
}

// As with prefix but for one suffix permitted text.
if (this._allowedSuffixes) {
for (i = 0, len = this._allowedSuffixes.length; i < len; ++i) {
var suffix = this._allowedSuffixes[i];
var ends;
if (typeof suffix === 'string') {
ends = endBeforeStringSuffix(value, suffix);
} else {
ends = endBeforeRegExpSuffix(value, suffix);
}
if (ends !== undefined) {
value = value.substr(0, ends);
break;
}
}
}

if (value.indexOf('_') === -1 || value.toUpperCase() === value) {
if (!this._strict) {return;}
if (value[0].toUpperCase() !== value[0] || value[0] === '_') {
if (value.length === 0 || value[0].toUpperCase() !== value[0] || isPrivate) {
return;
}
}
Expand Down
Loading

0 comments on commit c1d277c

Please sign in to comment.