Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/src/extend/scope-manager-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ Those members are defined but not used in ESLint.
| `"CatchClause"` | `CatchClause` |
| `"ClassName"` | `ClassDeclaration` or `ClassExpression` |
| `"FunctionName"` | `FunctionDeclaration` or `FunctionExpression` |
| `"ImplicitGlobalVariable"` | `AssignmentExpression` |
| `"ImplicitGlobalVariable"` | `AssignmentExpression` or `ForInStatement` or `ForOfStatement` |
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for (foo in { a: 5 }); and for (bar of [5]); also create implicit globals.

| `"ImportBinding"` | `ImportSpecifier`, `ImportDefaultSpecifier`, or `ImportNamespaceSpecifier` |
| `"Parameter"` | `FunctionDeclaration`, `FunctionExpression`, or `ArrowFunctionExpression` |
| `"Variable"` | `VariableDeclarator` |
Expand Down
30 changes: 30 additions & 0 deletions lib/languages/js/source-code/source-code.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,36 @@ function addDeclaredGlobals(

return true;
});

/*
* "implicit" contains information about implicit global variables (those created
* implicitly by assigning values to undeclared variables in non-strict code).
* Since we augment the global scope using configuration, we need to remove
* the ones that were added by configuration, as they are either built-in
* or declared elsewhere, therefore not implicit.
* Since the "implicit" property was not documented, first we'll check if it exists
* because it's possible that not all custom scope managers create this property.
* If it exists, we assume it has properties `variables` and `set`. Property
* `left` is considered optional (for example, typescript-eslint's scope manage
* has this property named `leftToBeResolved`).
*/
const { implicit } = globalScope;
if (typeof implicit === "object" && implicit !== null) {
implicit.variables = implicit.variables.filter(variable => {
const name = variable.name;
if (globalScope.set.has(name)) {
implicit.set.delete(name);
return false;
}
return true;
});

if (implicit.left) {
implicit.left = implicit.left.filter(
reference => !globalScope.set.has(reference.identifier.name),
);
}
}
}

/**
Expand Down
34 changes: 19 additions & 15 deletions lib/rules/no-implicit-globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,27 +142,31 @@ module.exports = {
}
}
});

if (
isReadonlyEslintGlobalVariable &&
variable.defs.length === 0
) {
variable.references.forEach(reference => {
if (
reference.isWrite() &&
!reference.init &&
!reference.isRead()
) {
report(
reference.identifier.parent,
"assignmentToReadonlyGlobal",
);
}
});
}
});

// Undeclared assigned variables.
scope.implicit.variables.forEach(variable => {
const scopeVariable = scope.set.get(variable.name);
let messageId;

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 => {
report(def.node, messageId);
report(def.node, "globalVariableLeak");
});
});
},
Expand Down
151 changes: 151 additions & 0 deletions tests/lib/languages/js/source-code/source-code.js
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to add a test with a custom scope manager that doesn't contain implicit?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added tests in c44db85

Original file line number Diff line number Diff line change
Expand Up @@ -3606,4 +3606,155 @@ describe("SourceCode", () => {
assert.isNull(problem.ruleId);
});
});

describe("finalize()", () => {
it("should remove ECMAScript globals from global scope's `implicit`", () => {
const code = "Array = 1; Foo = 1; Promise = 1; Array; Foo; Promise";
const ast = espree.parse(code, DEFAULT_CONFIG);
const scopeManager = eslintScope.analyze(ast, {
ignoreEval: true,
ecmaVersion: 6,
});
const sourceCode = new SourceCode({
text: code,
ast,
scopeManager,
});

sourceCode.applyLanguageOptions({
ecmaVersion: 2015,
});

sourceCode.finalize();

const globalScope = sourceCode.scopeManager.scopes[0];
const { implicit } = globalScope;

assert.deepStrictEqual(
[...implicit.set].map(([name]) => name),
["Foo"],
);
assert.deepStrictEqual(
implicit.variables.map(({ name }) => name),
["Foo"],
);
assert.deepStrictEqual(
implicit.left.map(reference => reference.identifier.name),
["Foo", "Foo"],
);
});

it("should remove custom globals from global scope's `implicit`", () => {
const code = "Bar = 1; Foo = 1; Baz = 1; Bar; Foo; Baz";
const ast = espree.parse(code, DEFAULT_CONFIG);
const scopeManager = eslintScope.analyze(ast, {
ignoreEval: true,
ecmaVersion: 6,
});
const sourceCode = new SourceCode({
text: code,
ast,
scopeManager,
});

sourceCode.applyLanguageOptions({
ecmaVersion: 2015,
globals: {
Bar: "writable",
Baz: "readonly",
},
});

sourceCode.finalize();

const globalScope = sourceCode.scopeManager.scopes[0];
const { implicit } = globalScope;

assert.deepStrictEqual(
[...implicit.set].map(([name]) => name),
["Foo"],
);
assert.deepStrictEqual(
implicit.variables.map(({ name }) => name),
["Foo"],
);
assert.deepStrictEqual(
implicit.left.map(reference => reference.identifier.name),
["Foo", "Foo"],
);
});

it("should remove commonjs globals from global scope's `implicit`", () => {
const code =
"exports = {}; Foo = 1; require = () => {}; exports; Foo; require";
const ast = espree.parse(code, DEFAULT_CONFIG);
const scopeManager = eslintScope.analyze(ast, {
ignoreEval: true,
nodejsScope: true,
ecmaVersion: 6,
});
const sourceCode = new SourceCode({
text: code,
ast,
scopeManager,
});

sourceCode.applyLanguageOptions({
ecmaVersion: 2015,
sourceType: "commonjs",
});

sourceCode.finalize();

const globalScope = sourceCode.scopeManager.scopes[0];
const { implicit } = globalScope;

assert.deepStrictEqual(
[...implicit.set].map(([name]) => name),
["Foo"],
);
assert.deepStrictEqual(
implicit.variables.map(({ name }) => name),
["Foo"],
);
assert.deepStrictEqual(
implicit.left.map(reference => reference.identifier.name),
["Foo", "Foo"],
);
});

it("should remove inline globals from global scope's `implicit`", () => {
const code =
"/* globals Bar: writable, Baz: readonly */ Bar = 1; Foo = 1; Baz = 1; Bar; Foo; Baz";
const ast = espree.parse(code, DEFAULT_CONFIG);
const scopeManager = eslintScope.analyze(ast, {
ignoreEval: true,
ecmaVersion: 6,
});
const sourceCode = new SourceCode({
text: code,
ast,
scopeManager,
});

sourceCode.applyInlineConfig();
sourceCode.finalize();

const globalScope = sourceCode.scopeManager.scopes[0];
const { implicit } = globalScope;

assert.deepStrictEqual(
[...implicit.set].map(([name]) => name),
["Foo"],
);
assert.deepStrictEqual(
implicit.variables.map(({ name }) => name),
["Foo"],
);
assert.deepStrictEqual(
implicit.left.map(reference => reference.identifier.name),
["Foo", "Foo"],
);
});
});
});
Loading