Skip to content

Commit

Permalink
Update: add propertiesPattern for id-match rule
Browse files Browse the repository at this point in the history
  • Loading branch information
tinymins committed May 28, 2019
1 parent d662b17 commit 50095f4
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 23 deletions.
42 changes: 42 additions & 0 deletions docs/rules/id-match.md
Expand Up @@ -57,10 +57,13 @@ var obj = {
This rule has an object option:

* `"properties": true` requires object properties to match the specified regular expression
* `"propertiesPattern": String` can seprately set properties id-match pattern, its value will be copied from main pattern if not provided
* `"onlyDeclarations": true` requires only `var`, `function`, and `class` declarations to match the specified regular expression
* `"onlyDeclarations": false` requires all variable names to match the specified regular expression
* `"ignoreDestructuring": false` (default) enforces `id-match` for destructured identifiers
* `"ignoreDestructuring": true` does not check destructured identifiers
* `"errorMessage": String` can customize some more humanized error report message for main pattern
* `"propertiesErrorMessage": String` can customize some more humanized error report message for properties pattern

### properties

Expand All @@ -74,6 +77,27 @@ var obj = {
};
```

### propertiesPattern

Examples of **incorrect** code for this rule with the `"^[a-zA-Z]+$", { "properties": true, "propertiesPattern": "^[a-z][a-zA-Z]*$" }` options:

```js
/*eslint id-match: ["error", "^[a-zA-Z]+$", { "properties": true, "propertiesPattern": "^[a-z][a-zA-Z]*$" }]*/

var ClassA = { Name: "class-a" };
```

Examples of **correct** code for this rule with the `"^[a-zA-Z]+$", { "properties": true, "propertiesPattern": "^[a-z][a-zA-Z]*$" }` options:

```js
/*eslint id-match: ["error", "^[a-zA-Z]+$", { "properties": true, "propertiesPattern": "^[a-z][a-zA-Z]*$" }]*/

var ClassA = { name: "class-a" };
var x = { [ClassA.name]: false };

var x = { [Math.max(1, 2)]: false };
```

### onlyDeclarations

Examples of **correct** code for this rule with the `"^[a-z]+([A-Z][a-z]+)*$", { "onlyDeclarations": true }` options:
Expand Down Expand Up @@ -126,6 +150,24 @@ var { category_id = 1 } = query;
var { category_id: category_id } = query;
```

### errorMessage: String

This option is provided to output humanized error reports, example of `errorMessage` option:

```js
/*eslint id-match: [2, "^[a-z]+[a-zA-Z0-9]*$", { "errorMessage": "Identifier '{{name}}' in not in lower camelcase." }]*/

var UpperCamelcase = 1;
```

Error report will be: `Identifier 'UpperCamelcase' in not in lower camelcase.`.

### propertiesErrorMessage: String

This option is similar with errorMessage, the only difference is this message is for properties error report.

If not set, the value of this option will be the same as `errorMessage` option.

## When Not To Use It

If you don't want to enforce any particular naming convention for all identifiers, or your naming convention is too complex to be enforced by configuring this rule, then you should not enable this rule.
71 changes: 48 additions & 23 deletions lib/rules/id-match.js
Expand Up @@ -31,13 +31,24 @@ module.exports = {
type: "boolean",
default: false
},
propertiesPattern: {
type: "string"
},
onlyDeclarations: {
type: "boolean",
default: false
},
ignoreDestructuring: {
type: "boolean",
default: false
},
errorMessage: {
type: "string",
default: ""
},
propertiesErrorMessage: {
type: "string",
default: ""
}
}
}
Expand All @@ -57,8 +68,12 @@ module.exports = {

const options = context.options[1] || {},
properties = !!options.properties,
propertiesPattern = options.propertiesPattern || pattern,
propertiesRegexp = new RegExp(propertiesPattern, "u"),
onlyDeclarations = !!options.onlyDeclarations,
ignoreDestructuring = !!options.ignoreDestructuring;
ignoreDestructuring = !!options.ignoreDestructuring,
errorMessage = options.errorMessage,
propertiesErrorMessage = options.propertiesErrorMessage || errorMessage;

//--------------------------------------------------------------------------
// Helpers
Expand All @@ -73,11 +88,12 @@ module.exports = {
/**
* Checks if a string matches the provided pattern
* @param {string} name The string to check.
* @param {boolean} isProperty Whether the identifier name of the identifier node is a property
* @returns {boolean} if the string is a match
* @private
*/
function isInvalid(name) {
return !regexp.test(name);
function isInvalid(name, isProperty) {
return isProperty ? !propertiesRegexp.test(name) : !regexp.test(name);
}

/**
Expand Down Expand Up @@ -105,29 +121,38 @@ module.exports = {
* parent node and the identifier name.
* @param {ASTNode} effectiveParent The effective parent node of the node to be reported
* @param {string} name The identifier name of the identifier node
* @param {boolean} isProperty Whether the identifier name of the identifier node is a property
* @returns {boolean} whether an error should be reported or not
*/
function shouldReport(effectiveParent, name) {
function shouldReport(effectiveParent, name, isProperty) {
return (!onlyDeclarations || DECLARATION_TYPES.has(effectiveParent.type)) &&
!ALLOWED_PARENT_TYPES.has(effectiveParent.type) && isInvalid(name);
!ALLOWED_PARENT_TYPES.has(effectiveParent.type) && isInvalid(name, isProperty);
}

/**
* Reports an AST node as a rule violation.
* @param {ASTNode} node The node to report.
* @param {boolean} isProperty Whether the identifier name of the reported identifier node is a property
* @returns {void}
* @private
*/
function report(node) {
function report(node, isProperty) {
if (!reported.has(node)) {
context.report({
const error = {
node,
messageId: "notMatch",
data: {
name: node.name,
pattern
pattern: isProperty ? propertiesPattern : pattern
}
});
};
const message = isProperty ? propertiesErrorMessage : errorMessage;

if (message) {
error.message = message;
} else {
error.messageId = "notMatch";
}
context.report(error);
reported.set(node, true);
}
}
Expand All @@ -148,21 +173,21 @@ module.exports = {
// Always check object names
if (parent.object.type === "Identifier" &&
parent.object.name === name) {
if (isInvalid(name)) {
if (isInvalid(name, false)) {
report(node);
}

// Report AssignmentExpressions left side's assigned variable id
} else if (effectiveParent.type === "AssignmentExpression" &&
effectiveParent.left.type === "MemberExpression" &&
effectiveParent.left.property.name === node.name) {
if (isInvalid(name)) {
if (isInvalid(name, true)) {
report(node);
}

// Report AssignmentExpressions only if they are the left side of the assignment
} else if (effectiveParent.type === "AssignmentExpression" && effectiveParent.right.type !== "MemberExpression") {
if (isInvalid(name)) {
if (isInvalid(name, false)) {
report(node);
}
}
Expand All @@ -175,9 +200,9 @@ module.exports = {
} else if (parent.type === "Property" || parent.type === "AssignmentPattern") {

if (parent.parent && parent.parent.type === "ObjectPattern") {
if (parent.shorthand && parent.value.left && isInvalid(name)) {
if (parent.shorthand && parent.value.left && isInvalid(name, false)) {

report(node);
report(node, false);
}

const assignmentKeyEqualsValue = parent.key.name === parent.value.name;
Expand All @@ -187,11 +212,11 @@ module.exports = {
return;
}

const valueIsInvalid = parent.value.name && isInvalid(name);
const valueIsInvalid = parent.value.name && isInvalid(name, false);

// ignore destructuring if the option is set, unless a new identifier is created
if (valueIsInvalid && !(assignmentKeyEqualsValue && ignoreDestructuring)) {
report(node);
report(node, false);
}
}

Expand All @@ -201,21 +226,21 @@ module.exports = {
}

// don't check right hand side of AssignmentExpression to prevent duplicate warnings
if (parent.right !== node && shouldReport(effectiveParent, name)) {
report(node);
if (parent.right !== node && shouldReport(effectiveParent, name, true)) {
report(node, true);
}

// Check if it's an import specifier
} else if (IMPORT_TYPES.has(parent.type)) {

// Report only if the local imported identifier is invalid
if (parent.local && parent.local.name === node.name && isInvalid(name)) {
report(node);
if (parent.local && parent.local.name === node.name && isInvalid(name, false)) {
report(node, false);
}

// Report anything that is invalid that isn't a CallExpression
} else if (shouldReport(effectiveParent, name)) {
report(node);
} else if (shouldReport(effectiveParent, name, false)) {
report(node, false);
}
}

Expand Down
48 changes: 48 additions & 0 deletions tests/lib/rules/id-match.js
Expand Up @@ -183,6 +183,22 @@ ruleTester.run("id-match", rule, {
options: ["^[^_]+$", {
properties: false
}]
},
{
code: "var x = { [Math.max(1, 2)]: false };",
options: ["^[^_]+$", {
properties: true,
propertiesPattern: "^[a-z][a-zA-Z0-9]*$"
}],
parserOptions: { ecmaVersion: 6 }
},
{
code: "var ClassA = { name: 'class-a' };\nvar x = { [ClassA.name]: false };",
options: ["^[^_]+$", {
properties: true,
propertiesPattern: "^[a-z][a-zA-Z0-9]*$"
}],
parserOptions: { ecmaVersion: 6 }
}
],
invalid: [
Expand Down Expand Up @@ -599,6 +615,38 @@ ruleTester.run("id-match", rule, {
type: "Identifier"
}
]
},
{
code: "var ClassA = { Name: 'class-a' };",
options: ["^[^_]+$", {
properties: true,
propertiesPattern: "^[a-z][a-zA-Z0-9]*$"
}],
errors: [
{
message: "Identifier 'Name' does not match the pattern '^[a-z][a-zA-Z0-9]*$'.",
type: "Identifier"
}
]
},
{
code: "var Name = { Key: 1 };",
options: ["^[a-z][a-zA-Z0-9]*$", {
properties: true,
propertiesPattern: "^[a-z][a-zA-Z0-9]*$",
errorMessage: "Identifier '{{name}}' in not in lower camelcase.",
propertiesErrorMessage: "Property name '{{name}}' in not in lower camelcase."
}],
errors: [
{
message: "Identifier 'Name' in not in lower camelcase.",
type: "Identifier"
},
{
message: "Property name 'Key' in not in lower camelcase.",
type: "Identifier"
}
]
}
]
});

0 comments on commit 50095f4

Please sign in to comment.