diff --git a/docs/rules/no-implicit-globals.md b/docs/rules/no-implicit-globals.md index d180d4a564a..7bf4caaae00 100644 --- a/docs/rules/no-implicit-globals.md +++ b/docs/rules/no-implicit-globals.md @@ -1,10 +1,34 @@ -# disallow variable and `function` declarations in the global scope (no-implicit-globals) +# Disallow declarations in the global scope (no-implicit-globals) -When working with browser scripts, developers often forget that variable and function declarations at the top-level scope become global variables on the `window` object. As opposed to modules which have their own scope. Globals should be explicitly assigned to `window` or `self` if that is the intent. Otherwise variables intended to be local to the script should be wrapped in an IIFE. +It is the best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. + +Global variables created from a script can produce name collisions with global variables created from another script, which will +usually lead to runtime errors or unexpected behavior. + +This rule disallows the following: + +* Declarations that create one or more variables in the global scope. +* Global variable leaks. +* Redeclarations of read-only global variables and assignments to read-only global variables. + +There is an explicit way to create a global variable when needed, by assigning to a property of the global object. + +This rule is mostly useful for browser scripts. Top-level declarations in ES modules and CommonJS modules create module-scoped +variables. ES modules also have implicit `strict` mode, which prevents global variable leaks. + +By default, this rule does not check `const`, `let` and `class` declarations. + +This rule has an object option with one option: + +* Set `"lexicalBindings"` to `true` if you want this rule to check `const`, `let` and `class` declarations as well. ## Rule Details -This rule disallows `var` and named `function` declarations at the top-level script scope. This does not apply to ES and CommonJS modules since they have a module scope. +### `var` and `function` declarations + +When working with browser scripts, developers often forget that variable and function declarations at the top-level scope become global variables on the `window` object. As opposed to modules which have their own scope. Globals should be explicitly assigned to `window` or `self` if that is the intent. Otherwise variables intended to be local to the script should be wrapped in an IIFE. + +This rule disallows `var` and `function` declarations at the top-level script scope. This does not apply to ES and CommonJS modules since they have a module scope. Examples of **incorrect** code for this rule: @@ -43,10 +67,160 @@ var foo = 1; function bar() {} ``` +### Global variable leaks + +When the code is not in `strict` mode, an assignment to an undeclared variable creates +a new global variable. This will happen even is the code is in a function. + +This does not apply to ES modules since the module code is implicitly in `strict` mode. + +Examples of **incorrect** code for this rule: + +```js +/*eslint no-implicit-globals: "error"*/ + +foo = 1; + +Bar.prototype.baz = function () { + a = 1; // Intended to be this.a = 1; +}; +``` + +### Read-only global variables + +This rule also disallows redeclarations of read-only global variables and assigments to read-only global variables. + +A read-only global variable can be a built-in ES global (e.g. `Array`), an environment specific global +(e.g. `window` in the browser environment), or a global variable defined as `readonly` in the configuration file +or in a `/*global */` comment. + +* [Specifying Environments](../user-guide/configuring#specifying-environments) +* [Specifying Globals](../user-guide/configuring#specifying-globals) + +Examples of **incorrect** code for this rule: + +```js +/*eslint no-implicit-globals: "error"*/ + +/*global foo:readonly*/ + +foo = 1; + +Array = []; +var Object; +``` + +### `const`, `let` and `class` declarations + +Lexical declarations `const` and `let`, as well as `class` declarations, create variables that are block-scoped. + +However, when declared in the top-level of a browser script these variables are not 'script-scoped'. +They are actually created in the global scope and could produce name collisions with +`var`, `const` and `let` variables and `function` and `class` declarations from other scripts. +This does not apply to ES and CommonJS modules. + +If the variable is intended to be local to the script, wrap the code with a block or with an immediately-invoked function expression (IIFE). + +Examples of **correct** code for this rule with `"lexicalBindings"` option set to `false` (default): + +```js +/*eslint no-implicit-globals: ["error", {"lexicalBindings": false}]*/ + +const foo = 1; + +let baz; + +class Bar {} +``` + +Examples of **incorrect** code for this rule with `"lexicalBindings"` option set to `true`: + +```js +/*eslint no-implicit-globals: ["error", {"lexicalBindings": true}]*/ + +const foo = 1; + +let baz; + +class Bar {} +``` + +Examples of **correct** code for this rule with `"lexicalBindings"` option set to `true`: + +```js +/*eslint no-implicit-globals: ["error", {"lexicalBindings": true}]*/ + +{ + const foo = 1; + let baz; + class Bar {} +} + +(function() { + const foo = 1; + let baz; + class Bar {} +}()); +``` + +If you intend to create a global `const` or `let` variable or a global `class` declaration, to be used from other scripts, +be aware that there are certain differences when compared to the traditional methods, which are `var` declarations and assigning to a property of the global `window` object: + +* Lexically declared variables cannot be conditionally created. A script cannot check for the existence of +a variable and then create a new one. `var` variables are also always created, but redeclarations do not +cause runtime exceptions. +* Lexically declared variables do not create properties on the global object, which is what a consuming script might expect. +* Lexically declared variables are shadowing properties of the global object, which might produce errors if a +consuming script is using both the variable and the property. +* Lexically declared variables can produce a permanent Temporal Dead Zone (TDZ) if the initialization throws an exception. +Even the `typeof` check is not safe from TDZ reference exceptions. + +Examples of **incorrect** code for this rule with `"lexicalBindings"` option set to `true`: + +```js +/*eslint no-implicit-globals: ["error", {"lexicalBindings": true}]*/ + +const MyGobalFunction = (function() { + const a = 1; + let b = 2; + return function() { + return a + b; + } +}()); +``` + +Examples of **correct** code for this rule with `"lexicalBindings"` option set to `true`: + +```js +/*eslint no-implicit-globals: ["error", {"lexicalBindings": true}]*/ + +window.MyGobalFunction = (function() { + const a = 1; + let b = 2; + return function() { + return a + b; + } +}()); +``` + ## When Not To Use It -If you want to be able to declare variables and functions in the global scope you can safely disable this rule. Or if you are always using module scoped files, this rule will never apply. +In the case of a browser script, if you want to be able to explicitly declare variables and functions in the global scope, +and your code is in strict mode or you don't want this rule to warn you about undeclared variables, +and you also don't want this rule to warn you about read-only globals, you can disable this rule. + +In the case of a CommonJS module, if your code is in strict mode or you don't want this rule to warn you about undeclared variables, +and you also don't want this rule to warn you about the read-only globals, you can disable this rule. + +In the case of an ES module, if you don't want this rule to warn you about the read-only globals you can disable this rule. ## Further Reading * [Immediately-Invoked Function Expression (IIFE)](http://benalman.com/news/2010/11/immediately-invoked-function-expression/) +* [ReferenceError: assignment to undeclared variable](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Undeclared_var) +* [Temporal Dead Zone](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let#Temporal_dead_zone) + +## Related Rules + +* [no-undef](no-undef.md) +* [no-global-assign](no-global-assign.md) diff --git a/lib/rules/no-implicit-globals.js b/lib/rules/no-implicit-globals.js index 2eea2b28463..d4bfa3af82f 100644 --- a/lib/rules/no-implicit-globals.js +++ b/lib/rules/no-implicit-globals.js @@ -1,5 +1,5 @@ /** - * @fileoverview Rule to check for implicit global variables and functions. + * @fileoverview Rule to check for implicit global variables, functions and classes. * @author Joshua Peek */ @@ -14,41 +14,123 @@ module.exports = { type: "suggestion", docs: { - description: "disallow variable and `function` declarations in the global scope", + description: "disallow declarations in the global scope", category: "Best Practices", recommended: false, url: "https://eslint.org/docs/rules/no-implicit-globals" }, - schema: [] + schema: [{ + type: "object", + properties: { + lexicalBindings: { + type: "boolean", + default: false + } + }, + additionalProperties: false + }], + + messages: { + globalNonLexicalBinding: "Unexpected {{kind}} declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable.", + globalLexicalBinding: "Unexpected {{kind}} declaration in the global scope, wrap in a block or in an IIFE.", + globalVariableLeak: "Global variable leak, declare the variable if it is intended to be local.", + assignmentToReadonlyGlobal: "Unexpected assignment to read-only global variable.", + redeclarationOfReadonlyGlobal: "Unexpected redeclaration of read-only global variable." + } }, create(context) { + + const checkLexicalBindings = context.options[0] && context.options[0].lexicalBindings === true; + + /** + * Reports the node. + * @param {ASTNode} node Node to report. + * @param {string} messageId Id of the message to report. + * @param {string|undefined} kind Declaration kind, can be 'var', 'const', 'let', function or class. + * @returns {void} + */ + function report(node, messageId, kind) { + context.report({ + node, + messageId, + data: { + kind + } + }); + } + return { Program() { const scope = context.getScope(); scope.variables.forEach(variable => { - if (variable.writeable) { + + // Only ESLint global variables have the `writable` key. + const isReadonlyEslintGlobalVariable = variable.writeable === false; + const isWritableEslintGlobalVariable = variable.writeable === true; + + if (isWritableEslintGlobalVariable) { + + // Everything is allowed with writable ESLint global variables. return; } variable.defs.forEach(def => { + const defNode = def.node; + if (def.type === "FunctionName" || (def.type === "Variable" && def.parent.kind === "var")) { - context.report({ node: def.node, message: "Implicit global variable, assign as global property instead." }); + if (isReadonlyEslintGlobalVariable) { + report(defNode, "redeclarationOfReadonlyGlobal"); + } else { + report( + defNode, + "globalNonLexicalBinding", + def.type === "FunctionName" ? "function" : `'${def.parent.kind}'` + ); + } + } + + if (checkLexicalBindings) { + if (def.type === "ClassName" || + (def.type === "Variable" && (def.parent.kind === "let" || def.parent.kind === "const"))) { + if (isReadonlyEslintGlobalVariable) { + report(defNode, "redeclarationOfReadonlyGlobal"); + } else { + report( + defNode, + "globalLexicalBinding", + def.type === "ClassName" ? "class" : `'${def.parent.kind}'` + ); + } + } } }); }); + // Undeclared assigned variables. scope.implicit.variables.forEach(variable => { const scopeVariable = scope.set.get(variable.name); + let messageId; - if (scopeVariable && scopeVariable.writeable) { - return; + if (scopeVariable) { + + // ESLint global variable + if (scopeVariable.writeable) { + return; + } + messageId = "assignmentToReadonlyGlobal"; + + } else { + + // Reference to an unknown variable, possible global leak. + messageId = "globalVariableLeak"; } + // def.node is an AssignmentExpression, ForInStatement or ForOfStatement. variable.defs.forEach(def => { - context.report({ node: def.node, message: "Implicit global variable, assign as global property instead." }); + report(def.node, messageId); }); }); } diff --git a/tests/lib/rules/no-implicit-globals.js b/tests/lib/rules/no-implicit-globals.js index e71e62848cb..58926537ad3 100644 --- a/tests/lib/rules/no-implicit-globals.js +++ b/tests/lib/rules/no-implicit-globals.js @@ -18,251 +18,1227 @@ const rule = require("../../../lib/rules/no-implicit-globals"), const ruleTester = new RuleTester(); +const varMessage = "Unexpected 'var' declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable."; +const functionMessage = "Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable."; +const constMessage = "Unexpected 'const' declaration in the global scope, wrap in a block or in an IIFE."; +const letMessage = "Unexpected 'let' declaration in the global scope, wrap in a block or in an IIFE."; +const classMessage = "Unexpected class declaration in the global scope, wrap in a block or in an IIFE."; +const readonlyRedeclarationMessage = "Unexpected redeclaration of read-only global variable."; +const readonlyAssignmentMessage = "Unexpected assignment to read-only global variable."; +const leakMessage = "Global variable leak, declare the variable if it is intended to be local."; + ruleTester.run("no-implicit-globals", rule, { valid: [ + + //------------------------------------------------------------------------------ + // General + //------------------------------------------------------------------------------ + + // Recommended way to create a global variable in the browser + { + code: "window.foo = 1;", + env: { browser: true } + }, + { + code: "window.foo = function() {};", + env: { browser: true } + }, + { + code: "window.foo = function foo() {};", + env: { browser: true } + }, + { + code: "window.foo = function bar() {};", + env: { browser: true } + }, + { + code: "window.foo = function*() {};", + parserOptions: { ecmaVersion: 2015 }, + env: { browser: true } + }, + { + code: "window.foo = function *foo() {};", + parserOptions: { ecmaVersion: 2015 }, + env: { browser: true } + }, + { + code: "window.foo = async function() {};", + parserOptions: { ecmaVersion: 2017 }, + env: { browser: true } + }, + { + code: "window.foo = async function foo() {};", + parserOptions: { ecmaVersion: 2017 }, + env: { browser: true } + }, + { + code: "window.foo = async function*() {};", + parserOptions: { ecmaVersion: 2018 }, + env: { browser: true } + }, + { + code: "window.foo = async function *foo() {};", + parserOptions: { ecmaVersion: 2018 }, + env: { browser: true } + }, + { + code: "window.foo = class {};", + parserOptions: { ecmaVersion: 2015 }, + env: { browser: true } + }, + { + code: "window.foo = class foo {};", + parserOptions: { ecmaVersion: 2015 }, + env: { browser: true } + }, + { + code: "window.foo = class bar {};", + parserOptions: { ecmaVersion: 2015 }, + env: { browser: true } + }, + { + code: "self.foo = 1;", + env: { browser: true } + }, + { + code: "self.foo = function() {};", + env: { browser: true } + }, + + // Another way to create a global variable. Not the best practice, but that isn't the reposibility of this rule. + "this.foo = 1;", + "this.foo = function() {};", + "this.foo = function bar() {};", + + // Test that the rule does'n report global comments + "/*global foo:readonly*/", + "/*global foo:writable*/", + "/*global Array:readonly*/", + "/*global Array:writable*/", + { + code: "/*global foo:readonly*/", + globals: { foo: "readonly" } + }, + { + code: "/*global foo:writable*/", + globals: { foo: "readonly" } + }, + { + code: "/*global foo:readonly*/", + globals: { foo: "writable" } + }, + { + code: "/*global foo:writable*/", + globals: { foo: "writable" } + }, + + //------------------------------------------------------------------------------ + // `var` and function declarations + //------------------------------------------------------------------------------ + + // Doesn't report function expressions + "typeof function() {}", + "typeof function foo() {}", + "(function() {}) + (function foo() {})", + { + code: "typeof function *foo() {}", + parserOptions: { ecmaVersion: 2015 } + }, + { + code: "typeof async function foo() {}", + parserOptions: { ecmaVersion: 2017 } + }, + { + code: "typeof async function *foo() {}", + parserOptions: { ecmaVersion: 2018 } + }, + + // Recommended way to create local variables + "(function() { var foo = 1; })();", + "(function() { function foo() {} })();", + { + code: "(function() { function *foo() {} })();", + parserOptions: { ecmaVersion: 2015 } + }, + { + code: "(function() { async function foo() {} })();", + parserOptions: { ecmaVersion: 2017 } + }, + { + code: "(function() { async function *foo() {} })();", + parserOptions: { ecmaVersion: 2018 } + }, + { + code: "window.foo = (function() { var bar; function foo () {}; return function bar() {} })();", + env: { browser: true } + }, + + // Different scoping + { + code: "var foo = 1;", + parserOptions: { ecmaVersion: 2015, sourceType: "module" } + }, + { + code: "function foo() {}", + parserOptions: { ecmaVersion: 2015, sourceType: "module" } + }, + { + code: "function *foo() {}", + parserOptions: { ecmaVersion: 2015, sourceType: "module" } + }, + { + code: "var foo = 1;", + parserOptions: { ecmaFeatures: { globalReturn: true } } + }, + { + code: "function foo() {}", + parserOptions: { ecmaFeatures: { globalReturn: true } } + }, + { + code: "var foo = 1;", + env: { node: true } + }, + { + code: "function foo() {}", + env: { node: true } + }, + + //------------------------------------------------------------------------------ + // `const`, `let` and class declarations + //------------------------------------------------------------------------------ + + // Test default option + { + code: "const foo = 1; let bar; class Baz {}", + parserOptions: { ecmaVersion: 2015 } + }, + { + code: "const foo = 1; let bar; class Baz {}", + options: [{ lexicalBindings: false }], + parserOptions: { ecmaVersion: 2015 } + }, + + // If the option is not set to true, even the redeclarations of read-only global variables are allowed. + { + code: "const Array = 1; let Object; class Math {}", + parserOptions: { ecmaVersion: 2015 } + }, + { + code: "/*global foo:readonly, bar:readonly, Baz:readonly*/ const foo = 1; let bar; class Baz {}", + parserOptions: { ecmaVersion: 2015 } + }, + + // Doesn't report class expressions + { + code: "typeof class {}", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 } + }, + { + code: "typeof class foo {}", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 } + }, + + // Recommended ways to create local variables + { + code: "{ const foo = 1; let bar; class Baz {} }", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 } + }, + { + code: "(function() { const foo = 1; let bar; class Baz {} })();", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 } + }, + { + code: "window.foo = (function() { const bar = 1; let baz; class Quux {} return function () {} })();", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 } + }, + + // different scoping + { + code: "const foo = 1; let bar; class Baz {}", + parserOptions: { ecmaVersion: 2015, sourceType: "module" } + }, + { + code: "const foo = 1; let bar; class Baz {}", + parserOptions: { ecmaVersion: 2015 }, + env: { node: true } + }, + { + code: "const foo = 1; let bar; class Baz {}", + parserOptions: { ecmaVersion: 2015, ecmaFeatures: { globalReturn: true } } + }, + + // Regression tests + { + code: "const foo = 1;", + parserOptions: { ecmaVersion: 2015 } + }, + { + code: "let foo = 1;", + parserOptions: { ecmaVersion: 2015 } + }, + { + code: "let foo = function() {};", + parserOptions: { ecmaVersion: 2015 } + }, + { + code: "const foo = function() {};", + parserOptions: { ecmaVersion: 2015 } + }, + { + code: "class Foo {}", + parserOptions: { ecmaVersion: 2015 } + }, + { + code: "(function() { let foo = 1; })();", + parserOptions: { ecmaVersion: 2015 } + }, + { + code: "(function() { const foo = 1; })();", + parserOptions: { ecmaVersion: 2015 } + }, + { + code: "let foo = 1;", + parserOptions: { ecmaVersion: 2015, sourceType: "module" } + }, + { + code: "const foo = 1;", + parserOptions: { ecmaVersion: 2015, sourceType: "module" } + }, + { + code: "let foo = 1;", + parserOptions: { ecmaVersion: 2015, ecmaFeatures: { globalReturn: true } } + }, + { + code: "const foo = 1;", + parserOptions: { ecmaVersion: 2015, ecmaFeatures: { globalReturn: true } } + }, + + //------------------------------------------------------------------------------ + // leaks + //------------------------------------------------------------------------------ + + // This rule doesn't report all undeclared variables, just leaks (assigments to an undeclared variable) + "foo", + "foo + bar", + "foo(bar)", + + // Leaks are not possible in strict mode (explicit or implicit). Therefore, rule doesn't report assignments in strict mode. + "'use strict';foo = 1;", + "(function() {'use strict'; foo = 1; })();", + { + code: "{ class Foo { constructor() { bar = 1; } baz() { bar = 1; } } }", + parserOptions: { ecmaVersion: 2015 } + }, + { + code: "foo = 1;", + parserOptions: { ecmaVersion: 2015, sourceType: "module" } + }, + + // This rule doesn't check the existence of the objects in property assignments. These are reference errors, not leaks. Note that the env is not set. + "Foo.bar = 1;", + "Utils.foo = 1;", + "Utils.foo = function() {};", + "window.foo = 1;", + "window.foo = function() {};", + "window.foo = function foo() {};", + "self.foo = 1;", + "self.foo = function() {};", + + // These are also just reference errors, thus not reported as leaks + "++foo", + "foo--", + + // Not a leak + { + code: "foo = 1;", + globals: { foo: "writable" } + }, + { + code: "window.foo = function bar() { bar = 1; };", + env: { browser: true } + }, + { + code: "window.foo = function bar(baz) { baz = 1; };", + env: { browser: true } + }, + { + code: "window.foo = function bar() { var baz; function quux() { quux = 1; } };", + env: { browser: true } + }, + + //------------------------------------------------------------------------------ + // globals + //------------------------------------------------------------------------------ + + // Redeclarations of writable global variables are allowed + "/*global foo:writable*/ var foo = 1;", + { + code: "function foo() {}", + globals: { foo: "writable" } + }, + { + code: "/*global foo:writable*/ function *foo() {}", + parserOptions: { ecmaVersion: 2015 } + }, + { + code: "/*global foo:writable*/ const foo = 1;", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 } + }, + { + code: "/*global foo:writable*/ let foo;", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 } + }, + { + code: "/*global Foo:writable*/ class Foo {}", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 } + }, + + // Assignments to writable global variables are allowed + "/*global foo:writable*/ foo = 1;", + { + code: "foo = 1", + globals: { foo: "writable" } + }, + + + // This rule doesn't dissalow assignments to properties of readonly globals + "Array.from = 1;", + "Object['assign'] = 1;", + "/*global foo:readonly*/ foo.bar = 1;" + ], + + invalid: [ + + //------------------------------------------------------------------------------ + // `var` and function declarations + //------------------------------------------------------------------------------ + + { + code: "var foo = 1;", + errors: [ + { + message: varMessage, + type: "VariableDeclarator" + } + ] + }, + { + code: "function foo() {}", + errors: [ + { + message: functionMessage, + type: "FunctionDeclaration" + } + ] + }, + { + code: "function *foo() {}", + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + message: functionMessage, + type: "FunctionDeclaration" + } + ] + }, + { + code: "async function foo() {}", + parserOptions: { ecmaVersion: 2017 }, + errors: [ + { + message: functionMessage, + type: "FunctionDeclaration" + } + ] + }, + { + code: "async function *foo() {}", + parserOptions: { ecmaVersion: 2018 }, + errors: [ + { + message: functionMessage, + type: "FunctionDeclaration" + } + ] + }, + { + code: "var foo = function() {};", + errors: [ + { + message: varMessage, + type: "VariableDeclarator" + } + ] + }, + { + code: "var foo = function foo() {};", + errors: [ + { + message: varMessage, + type: "VariableDeclarator" + } + ] + }, + { + code: "var foo = function*() {};", + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + message: varMessage, + type: "VariableDeclarator" + } + ] + }, + { + code: "var foo = function *foo() {};", + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + message: varMessage, + type: "VariableDeclarator" + } + ] + }, + { + code: "var foo = 1, bar = 2;", + errors: [ + { + message: varMessage, + type: "VariableDeclarator" + }, + { + message: varMessage, + type: "VariableDeclarator" + } + ] + }, + + + //------------------------------------------------------------------------------ + // `const`, `let` and class declarations + //------------------------------------------------------------------------------ + + // Basic tests + { + code: "const a = 1;", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 }, + errors: [{ + message: constMessage + }] + }, + { + code: "let a;", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 }, + errors: [{ + message: letMessage + }] + }, + { + code: "let a = 1;", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 }, + errors: [{ + message: letMessage + }] + }, + { + code: "class A {}", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 }, + errors: [{ + message: classMessage + }] + }, + + // Multiple and mixed tests + { + code: "const a = 1; const b = 2;", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { message: constMessage }, + { message: constMessage } + ] + }, + { + code: "const a = 1, b = 2;", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { message: constMessage }, + { message: constMessage } + ] + }, + { + code: "let a, b = 1;", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { message: letMessage }, + { message: letMessage } + ] + }, + { + code: "const a = 1; let b; class C {}", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { message: constMessage }, + { message: letMessage }, + { message: classMessage } + ] + }, + { + code: "const [a, b, ...c] = [];", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { message: constMessage }, + { message: constMessage }, + { message: constMessage } + ] + }, + { + code: "let { a, foo: b, bar: { c } } = {};", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { message: letMessage }, + { message: letMessage }, + { message: letMessage } + ] + }, + + //------------------------------------------------------------------------------ + // leaks + //------------------------------------------------------------------------------ + + // Basic tests + { + code: "foo = 1", + errors: [ + { + message: leakMessage, + type: "AssignmentExpression" + } + ] + }, + { + code: "foo = function() {};", + errors: [ + { + message: leakMessage, + type: "AssignmentExpression" + } + ] + }, + { + code: "foo = function*() {};", + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + message: leakMessage, + type: "AssignmentExpression" + } + ] + }, + { + code: "window.foo = function() { bar = 1; }", + errors: [ + { + message: leakMessage, + type: "AssignmentExpression" + } + ] + }, + { + code: "(function() {}(foo = 1));", + errors: [ + { + message: leakMessage, + type: "AssignmentExpression" + } + ] + }, + { + code: "for (foo in {});", + errors: [ + { + message: leakMessage, + type: "ForInStatement" + } + ] + }, + { + code: "for (foo of []);", + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + message: leakMessage, + type: "ForOfStatement" + } + ] + }, + + // Not implicit strict + { + code: "window.foo = { bar() { foo = 1 } }", + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + message: leakMessage, + type: "AssignmentExpression" + } + ] + }, + { + code: "foo = 1", + env: { node: true }, + errors: [ + { + message: leakMessage, + type: "AssignmentExpression" + } + ] + }, + { + code: "foo = 1;", + parserOptions: { ecmaFeatures: { globalReturn: true } }, + errors: [ + { + message: leakMessage, + type: "AssignmentExpression" + } + ] + }, + + // Multiple and mixed + { + code: "foo = 1, bar = 2;", + errors: [ + { + message: leakMessage, + type: "AssignmentExpression" + }, + { + message: leakMessage, + type: "AssignmentExpression" + } + ] + }, + { + code: "foo = bar = 1", + errors: [ + { + message: leakMessage, + type: "AssignmentExpression" + }, + { + message: leakMessage, + type: "AssignmentExpression" + } + ] + }, + { + code: "/*global foo:writable*/ foo = bar = 1", + errors: [ + { + message: leakMessage, + type: "AssignmentExpression" + } + ] + }, { - code: "const foo = 1;", - parserOptions: { ecmaVersion: 6 } + code: "/*global bar:writable*/ foo = bar = 1", + errors: [ + { + message: leakMessage, + type: "AssignmentExpression" + } + ] }, { - code: "let foo = 1;", - parserOptions: { ecmaVersion: 6 } + code: "foo = 1; var bar;", + errors: [ + { + message: leakMessage, + type: "AssignmentExpression" + }, + { + message: varMessage, + type: "VariableDeclarator" + } + ] }, { - code: "let foo = function() {};", - parserOptions: { ecmaVersion: 6 } + code: "var foo = bar = 1;", + errors: [ + { + message: varMessage, + type: "VariableDeclarator" + }, + { + message: leakMessage, + type: "AssignmentExpression" + } + ] }, { - code: "const foo = function() {};", - parserOptions: { ecmaVersion: 6 } + code: "/*global foo:writable*/ var foo = bar = 1;", + errors: [ + { + message: leakMessage, + type: "AssignmentExpression" + } + ] }, { - code: "class Foo {}", - parserOptions: { ecmaVersion: 6 } + code: "/*global bar:writable*/ var foo = bar = 1;", + errors: [ + { + message: varMessage, + type: "VariableDeclarator" + } + ] }, - "window.foo = 1;", - "window.foo = function() {};", - "window.foo = function foo() {};", { - code: "window.foo = function*() {};", - parserOptions: { ecmaVersion: 6 } + code: "[foo, bar] = [];", + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + message: leakMessage, + type: "AssignmentExpression" + }, + { + message: leakMessage, + type: "AssignmentExpression" + } + ] }, - "self.foo = 1;", - "self.foo = function() {};", - "this.foo = 1;", - "this.foo = function() {};", - "Utils.foo = 1;", - "Utils.foo = function() {};", - "(function() { var foo = 1; })();", { - code: "(function() { let foo = 1; })();", - parserOptions: { ecmaVersion: 6 } + code: "/*global foo:writable*/ [foo, bar] = [];", + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + message: leakMessage, + type: "AssignmentExpression" + } + ] }, { - code: "(function() { const foo = 1; })();", - parserOptions: { ecmaVersion: 6 } + code: "/*global bar:writable*/ [foo, bar] = [];", + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + message: leakMessage, + type: "AssignmentExpression" + } + ] }, - "(function() { function foo() {} })();", + + //------------------------------------------------------------------------------ + // globals + //------------------------------------------------------------------------------ + + // Basic assigment tests { - code: "(function() { function *foo() {} })();", - parserOptions: { ecmaVersion: 6 } + code: "Array = 1", + errors: [ + { + message: readonlyAssignmentMessage, + type: "AssignmentExpression" + } + ] }, { - code: "var foo = 1;", - parserOptions: { ecmaVersion: 6, sourceType: "module" } + code: "window = 1;", + env: { browser: true }, + errors: [ + { + message: readonlyAssignmentMessage, + type: "AssignmentExpression" + } + ] }, { - code: "let foo = 1;", - parserOptions: { ecmaVersion: 6, sourceType: "module" } + code: "/*global foo:readonly*/ foo = 1", + errors: [ + { + message: readonlyAssignmentMessage, + type: "AssignmentExpression" + } + ] }, { - code: "const foo = 1;", - parserOptions: { ecmaVersion: 6, sourceType: "module" } + code: "foo = 1;", + globals: { foo: "readonly" }, + errors: [ + { + message: readonlyAssignmentMessage, + type: "AssignmentExpression" + } + ] }, { - code: "function foo() {}", - parserOptions: { ecmaVersion: 6, sourceType: "module" } + code: "/*global foo:readonly*/ for (foo in {});", + errors: [ + { + message: readonlyAssignmentMessage, + type: "ForInStatement" + } + ] }, { - code: "function *foo() {}", - parserOptions: { ecmaVersion: 6, sourceType: "module" } + code: "/*global foo:readonly*/ for (foo of []);", + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + message: readonlyAssignmentMessage, + type: "ForOfStatement" + } + ] }, + + // Basic redeclaration tests { - code: "var foo = 1;", - parserOptions: { ecmaFeatures: { globalReturn: true } } + code: "var Array = 1", + errors: [ + { + message: readonlyRedeclarationMessage, + type: "VariableDeclarator" + } + ] }, { - code: "let foo = 1;", - parserOptions: { ecmaVersion: 6, ecmaFeatures: { globalReturn: true } } + code: "/*global foo:readonly*/ var foo", + errors: [ + { + message: readonlyRedeclarationMessage, + type: "VariableDeclarator" + } + ] }, { - code: "const foo = 1;", - parserOptions: { ecmaVersion: 6, ecmaFeatures: { globalReturn: true } } + code: "/*global foo:readonly*/ var foo = 1", + errors: [ + { + message: readonlyRedeclarationMessage, + type: "VariableDeclarator" + } + ] }, { - code: "function foo() {}", - parserOptions: { ecmaFeatures: { globalReturn: true } } + code: "/*global foo:readonly*/ function foo() {}", + errors: [ + { + message: readonlyRedeclarationMessage, + type: "FunctionDeclaration" + } + ] + }, + { + code: "/*global foo:readonly*/ const foo = 1", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + message: readonlyRedeclarationMessage, + type: "VariableDeclarator" + } + ] + }, + { + code: "/*global foo:readonly*/ let foo", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + message: readonlyRedeclarationMessage, + type: "VariableDeclarator" + } + ] + }, + { + code: "/*global foo:readonly*/ let foo = 1", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + message: readonlyRedeclarationMessage, + type: "VariableDeclarator" + } + ] + }, + { + code: "/*global Foo:readonly*/ class Foo {}", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + message: readonlyRedeclarationMessage, + type: "ClassDeclaration" + } + ] }, - "/*global foo:true*/ var foo = 1;", - "/*global foo:true*/ foo = 1;", - "/*global foo:true*/ function foo() {}" - ], - invalid: [ + // Multiple and mixed assigments { - code: "foo = 1;", + code: "/*global foo:readonly, bar: readonly*/ foo = bar = 1", errors: [ { - message: "Implicit global variable, assign as global property instead.", + message: readonlyAssignmentMessage, + type: "AssignmentExpression" + }, + { + message: readonlyAssignmentMessage, type: "AssignmentExpression" } ] }, { - code: "foo = 1, bar = 2;", + code: "/*global foo:writable, bar: readonly*/ foo = bar = 1", + errors: [ + { + message: readonlyAssignmentMessage, + type: "AssignmentExpression" + } + ] + }, + { + code: "/*global foo:readonly, bar: writable*/ foo = bar = 1", + errors: [ + { + message: readonlyAssignmentMessage, + type: "AssignmentExpression" + } + ] + }, + { + code: "/*global foo: readonly*/ foo = bar = 1", errors: [ { - message: "Implicit global variable, assign as global property instead.", + message: readonlyAssignmentMessage, type: "AssignmentExpression" }, { - message: "Implicit global variable, assign as global property instead.", + message: leakMessage, type: "AssignmentExpression" } ] }, { - code: "var foo = 1;", + code: "/*global bar: readonly*/ foo = bar = 1", errors: [ { - message: "Implicit global variable, assign as global property instead.", - type: "VariableDeclarator" + message: leakMessage, + type: "AssignmentExpression" + }, + { + message: readonlyAssignmentMessage, + type: "AssignmentExpression" } ] }, + + // Multiple and mixed redeclarations { - code: "var foo = 1, bar = 2;", + code: "/*global foo:readonly, bar: readonly*/ var foo, bar;", errors: [ { - message: "Implicit global variable, assign as global property instead.", + message: readonlyRedeclarationMessage, type: "VariableDeclarator" }, { - message: "Implicit global variable, assign as global property instead.", + message: readonlyRedeclarationMessage, type: "VariableDeclarator" } ] }, { - code: "function foo() {}", + code: "/*global foo:writable, bar: readonly*/ var foo, bar;", errors: [ { - message: "Implicit global variable, assign as global property instead.", - type: "FunctionDeclaration" + message: readonlyRedeclarationMessage, + type: "VariableDeclarator" } ] }, { - code: "foo = function() {};", + code: "/*global foo:readonly, bar: writable*/ var foo, bar;", errors: [ { - message: "Implicit global variable, assign as global property instead.", - type: "AssignmentExpression" + message: readonlyRedeclarationMessage, + type: "VariableDeclarator" } ] }, { - code: "var foo = function() {};", + code: "/*global foo:readonly*/ var foo, bar;", errors: [ { - message: "Implicit global variable, assign as global property instead.", + message: readonlyRedeclarationMessage, + type: "VariableDeclarator" + }, + { + message: varMessage, type: "VariableDeclarator" } ] }, { - code: "var foo = function foo() {};", + code: "/*global bar: readonly*/ var foo, bar;", errors: [ { - message: "Implicit global variable, assign as global property instead.", + message: varMessage, + type: "VariableDeclarator" + }, + { + message: readonlyRedeclarationMessage, type: "VariableDeclarator" } ] }, { - code: "function *foo() {}", - parserOptions: { ecmaVersion: 6 }, + code: "/*global foo:readonly, bar: readonly*/ const foo = 1, bar = 2;", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 }, errors: [ { - message: "Implicit global variable, assign as global property instead.", - type: "FunctionDeclaration" + message: readonlyRedeclarationMessage, + type: "VariableDeclarator" + }, + { + message: readonlyRedeclarationMessage, + type: "VariableDeclarator" } ] }, { - code: "foo = function*() {};", - parserOptions: { ecmaVersion: 6 }, + code: "/*global foo:writable, bar: readonly*/ const foo = 1, bar = 2;", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 }, errors: [ { - message: "Implicit global variable, assign as global property instead.", - type: "AssignmentExpression" + message: readonlyRedeclarationMessage, + type: "VariableDeclarator" } ] }, { - code: "var foo = function*() {};", - parserOptions: { ecmaVersion: 6 }, + code: "/*global foo:readonly, bar: writable*/ const foo = 1, bar = 2;", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 }, errors: [ { - message: "Implicit global variable, assign as global property instead.", + message: readonlyRedeclarationMessage, type: "VariableDeclarator" } ] }, { - code: "var foo = function *foo() {};", - parserOptions: { ecmaVersion: 6 }, + code: "/*global foo:readonly*/ const foo = 1, bar = 2;", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 }, errors: [ { - message: "Implicit global variable, assign as global property instead.", + message: readonlyRedeclarationMessage, + type: "VariableDeclarator" + }, + { + message: constMessage, type: "VariableDeclarator" } ] }, { - code: "foo = 1;", - parserOptions: { ecmaFeatures: { globalReturn: true } }, + code: "/*global bar: readonly*/ const foo = 1, bar = 2;", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 }, errors: [ { - message: "Implicit global variable, assign as global property instead.", - type: "AssignmentExpression" + message: constMessage, + type: "VariableDeclarator" + }, + { + message: readonlyRedeclarationMessage, + type: "VariableDeclarator" } ] }, { - code: "/*global foo:false*/ var foo = 1;", + code: "/*global foo:readonly, bar: readonly*/ let foo, bar;", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 }, errors: [ { - message: "Implicit global variable, assign as global property instead.", + message: readonlyRedeclarationMessage, + type: "VariableDeclarator" + }, + { + message: readonlyRedeclarationMessage, type: "VariableDeclarator" } ] }, { - code: "/*global foo:false*/ foo = 1;", + code: "/*global foo:writable, bar: readonly*/ let foo, bar;", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 }, errors: [ { - message: "Implicit global variable, assign as global property instead.", - type: "AssignmentExpression" + message: readonlyRedeclarationMessage, + type: "VariableDeclarator" } ] }, { - code: "/*global foo:false*/ function foo() {}", + code: "/*global foo:readonly, bar: writable*/ let foo, bar;", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 }, errors: [ { - message: "Implicit global variable, assign as global property instead.", - type: "FunctionDeclaration" + message: readonlyRedeclarationMessage, + type: "VariableDeclarator" + } + ] + }, + { + code: "/*global foo:readonly*/ let foo, bar;", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + message: readonlyRedeclarationMessage, + type: "VariableDeclarator" + }, + { + message: letMessage, + type: "VariableDeclarator" + } + ] + }, + { + code: "/*global bar: readonly*/ let foo, bar;", + options: [{ lexicalBindings: true }], + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + message: letMessage, + type: "VariableDeclarator" + }, + { + message: readonlyRedeclarationMessage, + type: "VariableDeclarator" } ] }