From 51e42eca3e87d8259815d736ffe81e604f184057 Mon Sep 17 00:00:00 2001 From: David Gasperoni Date: Fri, 26 Jun 2020 21:56:44 +0200 Subject: [PATCH] Update: Add option "ignoreGlobals" to camelcase rule (fixes #11716) (#12782) * Update: Add option "ignoreGlobals" to camelcase rule (fixes 11716) * Change reference check to look for global variable * Fix behavior for global references and add tests * Add more valid tests to camelcase rule * Add more invalid tests for camelcase rule * Don't ignore non-camelcase global variable used as object key * Corrections to camelcase documentation --- docs/rules/camelcase.md | 24 +++ lib/rules/camelcase.js | 47 +++++ tests/lib/rules/camelcase.js | 349 +++++++++++++++++++++++++++++++++++ 3 files changed, 420 insertions(+) diff --git a/docs/rules/camelcase.md b/docs/rules/camelcase.md index 96135f8c932..164dfab7c46 100644 --- a/docs/rules/camelcase.md +++ b/docs/rules/camelcase.md @@ -16,6 +16,8 @@ This rule has an object option: * `"ignoreDestructuring": true` does not check destructured identifiers (but still checks any use of those identifiers later in the code) * `"ignoreImports": false` (default) enforces camelcase style for ES2015 imports * `"ignoreImports": true` does not check ES2015 imports (but still checks any use of the imports later in the code except function arguments) +* `"ignoreGlobals": false` (default) enforces camelcase style for global variables +* `"ignoreGlobals": true` does not enforce camelcase style for global variables * `allow` (`string[]`) list of properties to accept. Accept regex. ### properties: "always" @@ -217,6 +219,28 @@ Examples of **correct** code for this rule with the `{ "ignoreImports": true }` import { snake_cased } from 'mod'; ``` +### ignoreGlobals: false + +Examples of **incorrect** code for this rule with the default `{ "ignoreGlobals": false }` option: + +```js +/*eslint camelcase: ["error", {ignoreGlobals: false}]*/ +/* global no_camelcased */ + +const foo = no_camelcased; +``` + +### ignoreGlobals: true + +Examples of **correct** code for this rule with the `{ "ignoreGlobals": true }` option: + +```js +/*eslint camelcase: ["error", {ignoreGlobals: true}]*/ +/* global no_camelcased */ + +const foo = no_camelcased; +``` + ## allow Examples of **correct** code for this rule with the `allow` option: diff --git a/lib/rules/camelcase.js b/lib/rules/camelcase.js index 04360837294..d34656cfabe 100644 --- a/lib/rules/camelcase.js +++ b/lib/rules/camelcase.js @@ -32,6 +32,10 @@ module.exports = { type: "boolean", default: false }, + ignoreGlobals: { + type: "boolean", + default: false + }, properties: { enum: ["always", "never"] }, @@ -61,8 +65,11 @@ module.exports = { let properties = options.properties || ""; const ignoreDestructuring = options.ignoreDestructuring; const ignoreImports = options.ignoreImports; + const ignoreGlobals = options.ignoreGlobals; const allow = options.allow || []; + let globalScope; + if (properties !== "always" && properties !== "never") { properties = "always"; } @@ -159,6 +166,37 @@ module.exports = { return false; } + /** + * Checks whether the given node represents a reference to a global variable that is not declared in the source code. + * These identifiers will be allowed, as it is assumed that user has no control over the names of external global variables. + * @param {ASTNode} node `Identifier` node to check. + * @returns {boolean} `true` if the node is a reference to a global variable. + */ + function isReferenceToGlobalVariable(node) { + const variable = globalScope.set.get(node.name); + + return variable && variable.defs.length === 0 && + variable.references.some(ref => ref.identifier === node); + } + + /** + * Checks whether the given node represents a reference to a property of an object in an object literal expression. + * This allows to differentiate between a global variable that is allowed to be used as a reference, and the key + * of the expressed object (which shouldn't be allowed). + * @param {ASTNode} node `Identifier` node to check. + * @returns {boolean} `true` if the node is a property name of an object literal expression + */ + function isPropertyNameInObjectLiteral(node) { + const parent = node.parent; + + return ( + parent.type === "Property" && + parent.parent.type === "ObjectExpression" && + !parent.computed && + parent.key === node + ); + } + /** * Reports an AST node as a rule violation. * @param {ASTNode} node The node to report. @@ -174,6 +212,10 @@ module.exports = { return { + Program() { + globalScope = context.getScope(); + }, + Identifier(node) { /* @@ -189,6 +231,11 @@ module.exports = { return; } + // Check if it's a global variable + if (ignoreGlobals && isReferenceToGlobalVariable(node) && !isPropertyNameInObjectLiteral(node)) { + return; + } + // MemberExpressions get special rules if (node.parent.type === "MemberExpression") { diff --git a/tests/lib/rules/camelcase.js b/tests/lib/rules/camelcase.js index 7c7a4576c77..eab0f354aec 100644 --- a/tests/lib/rules/camelcase.js +++ b/tests/lib/rules/camelcase.js @@ -169,6 +169,104 @@ ruleTester.run("camelcase", rule, { options: [{ ignoreImports: false }], parserOptions: { ecmaVersion: 6, sourceType: "module" } }, + { + code: "var _camelCased = aGlobalVariable", + options: [{ ignoreGlobals: false }], + globals: { aGlobalVariable: "readonly" } + }, + { + code: "var camelCased = _aGlobalVariable", + options: [{ ignoreGlobals: false }], + globals: { _aGlobalVariable: "readonly" } + }, + { + code: "var camelCased = a_global_variable", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "a_global_variable.foo()", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "a_global_variable[undefined]", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "var foo = a_global_variable.bar", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "a_global_variable.foo = bar", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "( { foo: a_global_variable.bar } = baz )", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "a_global_variable = foo", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "writable" } // eslint-disable-line camelcase + }, + { + code: "a_global_variable = foo", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "({ a_global_variable } = foo)", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" } // eslint-disable-line camelcase + }, + { + code: "({ snake_cased: a_global_variable } = foo)", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" } // eslint-disable-line camelcase + }, + { + code: "({ snake_cased: a_global_variable = foo } = bar)", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" } // eslint-disable-line camelcase + }, + { + code: "[a_global_variable] = bar", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" } // eslint-disable-line camelcase + }, + { + code: "[a_global_variable = foo] = bar", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" } // eslint-disable-line camelcase + }, + { + code: "foo[a_global_variable] = bar", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "var foo = { [a_global_variable]: bar }", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "var { [a_global_variable]: foo } = bar", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, { code: "function foo({ no_camelcased: camelCased }) {};", parserOptions: { ecmaVersion: 6 } @@ -652,6 +750,257 @@ ruleTester.run("camelcase", rule, { } ] }, + { + code: "var camelCased = snake_cased", + options: [{ ignoreGlobals: false }], + globals: { snake_cased: "readonly" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "snake_cased" }, + type: "Identifier" + } + ] + }, + { + code: "a_global_variable.foo()", + options: [{ ignoreGlobals: false }], + globals: { snake_cased: "readonly" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "a_global_variable[undefined]", + options: [{ ignoreGlobals: false }], + globals: { snake_cased: "readonly" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "var camelCased = snake_cased", + globals: { snake_cased: "readonly" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "snake_cased" }, + type: "Identifier" + } + ] + }, + { + code: "var camelCased = snake_cased", + options: [{}], + globals: { snake_cased: "readonly" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "snake_cased" }, + type: "Identifier" + } + ] + }, + { + code: "foo.a_global_variable = bar", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "var foo = { a_global_variable: bar }", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "var foo = { a_global_variable: a_global_variable }", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 13 + } + ] + }, + { + code: "var foo = { a_global_variable() {} }", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "class Foo { a_global_variable() {} }", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "a_global_variable: for (;;);", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "if (foo) { let a_global_variable; a_global_variable = bar; }", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 16 + }, + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 35 + } + ] + }, + { + code: "function foo(a_global_variable) { foo = a_global_variable; }", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 14 + }, + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 41 + } + ] + }, + { + code: "var a_global_variable", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "function a_global_variable () {}", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "const a_global_variable = foo; bar = a_global_variable", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 7 + }, + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 38 + } + ] + }, + { + code: "bar = a_global_variable; var a_global_variable;", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 7 + }, + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 30 + } + ] + }, + { + code: "var foo = { a_global_variable }", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "readonly" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, { code: "export * as snake_cased from 'mod'", parserOptions: { ecmaVersion: 2020, sourceType: "module" },