diff --git a/src/configs/all.ts b/src/configs/all.ts index a5658c6382c..f59981ee30f 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -59,6 +59,7 @@ export const rules = { "no-namespace": true, "no-non-null-assertion": true, "no-reference": true, + "no-restricted-globals": true, "no-this-assignment": true, "no-var-requires": true, "only-arrow-functions": true, diff --git a/src/rules/noRestrictedGlobalsRule.ts b/src/rules/noRestrictedGlobalsRule.ts new file mode 100644 index 00000000000..a0a88a17316 --- /dev/null +++ b/src/rules/noRestrictedGlobalsRule.ts @@ -0,0 +1,122 @@ +/** + * @license + * Copyright 2014 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { isBindingElement, isComputedPropertyName, isIdentifier, isPropertyAccessExpression } from "tsutils"; +import * as ts from "typescript"; +import * as Lint from "../index"; + +export class Rule extends Lint.Rules.TypedRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "no-restricted-globals", + description: "Disallow specific global variables.", + rationale: Lint.Utils.dedent` + \`\`\`ts + function broken(evt: Event) { + // Meant to do something with \`evt\` but typed it incorrectly. + Event.target; // compiler error + event.target; // should be a lint failure + } + + Early Internet Explorer versions exposed the current DOM event as a global variable 'event', + but using this variable has been considered a bad practice for a long time. + Restricting this will make sure this variable isn’t used in browser code. + \`\`\` + `, + descriptionDetails: Lint.Utils.dedent` + Disallowing usage of specific global variables can be useful if you want to allow + a set of global variables by enabling an environment, but still want to disallow + some of those. + `, + optionsDescription: Lint.Utils.dedent` + This rule takes a list of strings, where each string is a global to be restricted. + \`event\`, \`name\` and \`length\` are restricted by default. + `, + options: { + type: "list", + items: { type: "string" }, + }, + optionExamples: [ + [ + true, + "name", + "length", + "event", + ], + ], + type: "functionality", + typescriptOnly: false, + requiresTypeInfo: true, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING(name: string) { + return `Unexpected global variable '${name}'. Use a local parameter or variable instead.`; + } + + public applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { + const bannedList = this.ruleArguments.length > 0 ? this.ruleArguments : ["event", "name", "length"]; + const bannedGlobals = new Set(bannedList); + if (sourceFile.isDeclarationFile) { + return []; + } else { + return this.applyWithFunction(sourceFile, walk, bannedGlobals, program.getTypeChecker()); + } + } +} + +function walk(ctx: Lint.WalkContext>, checker: ts.TypeChecker): void { + return ts.forEachChild(ctx.sourceFile, function recur(node: ts.Node | undefined): void { + if (node == undefined) { + return; + } + + // Handles `const { bar, length: { x: y = () => event } } = foo` + if (isBindingElement(node)) { + recur(node.initializer); + recur(node.name); + if (node.propertyName != undefined && isComputedPropertyName(node.propertyName)) { + recur(node.propertyName); + } + } else if (isPropertyAccessExpression(node)) { + // Ignore `y` in `x.y`, but recurse to `x`. + recur(node.expression); + } else if (isIdentifier(node)) { + checkIdentifier(node); + } else { + ts.forEachChild(node, recur); + } + }); + + function checkIdentifier(node: ts.Identifier): void { + if (!ctx.options.has(node.text)) { + return; + } + + const symbol = checker.getSymbolAtLocation(node); + const declarations = symbol === undefined ? undefined : symbol.declarations; + if (declarations === undefined || declarations.length === 0) { + return; + } + + const isAmbientGlobal = declarations.some((decl) => decl.getSourceFile().isDeclarationFile); + + if (isAmbientGlobal) { + ctx.addFailureAtNode(node, Rule.FAILURE_STRING(node.text)); + } + } +} diff --git a/test/rules/no-restricted-globals/custom-global.d.ts b/test/rules/no-restricted-globals/custom-global.d.ts new file mode 100644 index 00000000000..e981c8a88cf --- /dev/null +++ b/test/rules/no-restricted-globals/custom-global.d.ts @@ -0,0 +1,2 @@ +declare var badGlobal: any; +declare var goodGlobal: any; \ No newline at end of file diff --git a/test/rules/no-restricted-globals/foo.d.ts b/test/rules/no-restricted-globals/foo.d.ts new file mode 100644 index 00000000000..30f811d837f --- /dev/null +++ b/test/rules/no-restricted-globals/foo.d.ts @@ -0,0 +1 @@ +export var name: string; \ No newline at end of file diff --git a/test/rules/no-restricted-globals/namespace.ts.lint b/test/rules/no-restricted-globals/namespace.ts.lint new file mode 100644 index 00000000000..5fe8939d5c6 --- /dev/null +++ b/test/rules/no-restricted-globals/namespace.ts.lint @@ -0,0 +1,10 @@ +console.log(name); + ~~~~ [Unexpected global variable 'name'. Use a local parameter or variable instead.] + +namespace A { + const foo = name; +} + +namespace A { + export var name = 23; +} \ No newline at end of file diff --git a/test/rules/no-restricted-globals/test.ts.lint b/test/rules/no-restricted-globals/test.ts.lint new file mode 100644 index 00000000000..2fb6b4c5521 --- /dev/null +++ b/test/rules/no-restricted-globals/test.ts.lint @@ -0,0 +1,75 @@ +function foo(evt: Event) { + let length: number = 1; + console.log(length); + + Event.target; + + event.target; + ~~~~~ [Unexpected global variable 'event'. Use a local parameter or variable instead.] +} + +console.log(length); + ~~~~~~ [Unexpected global variable 'length'. Use a local parameter or variable instead.] + +import { name } from "./foo"; +import "./custom-global" +console.log(name); + +console.log(badGlobal); + ~~~~~~~~~ [Unexpected global variable 'badGlobal'. Use a local parameter or variable instead.] + +console.log(goodGlobal); + +let { foo = event } = bar + ~~~~~ [Unexpected global variable 'event'. Use a local parameter or variable instead.] + +let { foo = (() => event)() } = bar; + ~~~~~ [Unexpected global variable 'event'. Use a local parameter or variable instead.] + +let foo: typeof event; + ~~~~~ [Unexpected global variable 'event'. Use a local parameter or variable instead.] + +function rest() { + const { bar, ...event } = foo; +} + +function nested() { + const { bar, event: { x: y } } = foo; +} + +function initializer() { + const { bar, length = () => event } = foo; + ~~~~~ [Unexpected global variable 'event'. Use a local parameter or variable instead.] +} + +function nestedInitializer() { + const { bar, length: { x: y = () => event } } = foo; + ~~~~~ [Unexpected global variable 'event'. Use a local parameter or variable instead.] +} + +function computedProperty() { + const { [event.type]: x } = foo; + ~~~~~ [Unexpected global variable 'event'. Use a local parameter or variable instead.] +} + +function typeTest() { + type name = any; +} + +function functionTest () { + const foo = name; + + function name () { + + } +} + +function interfaceTest() { + var foo: name; + interface name {} +} + +function classTest() { + const foo = new name(); + class name {} +} \ No newline at end of file diff --git a/test/rules/no-restricted-globals/tsconfig.json b/test/rules/no-restricted-globals/tsconfig.json new file mode 100644 index 00000000000..3039d056863 --- /dev/null +++ b/test/rules/no-restricted-globals/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "module": "commonjs", + "lib": [ + "dom" + ] + } +} diff --git a/test/rules/no-restricted-globals/tslint.json b/test/rules/no-restricted-globals/tslint.json new file mode 100644 index 00000000000..4c2950318f4 --- /dev/null +++ b/test/rules/no-restricted-globals/tslint.json @@ -0,0 +1,11 @@ +{ + "rules": { + "no-restricted-globals": [ + true, + "name", + "length", + "event", + "badGlobal" + ] + } +}