diff --git a/src/configs/all.ts b/src/configs/all.ts index 9cdd2e7fe01..8d7904928d6 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -106,6 +106,7 @@ export const rules = { true, "check-parameters", ], + "no-dynamic-delete": true, "no-empty": true, "no-eval": true, "no-floating-promises": true, diff --git a/src/configuration.ts b/src/configuration.ts index 0890c96af0c..3fd4c328444 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -242,6 +242,7 @@ export function loadConfigurationFromPath(configFilePath?: string, originalFileP } } else { rawConfigFile = require(resolvedConfigFilePath) as RawConfigFile; + // tslint:disable-next-line:no-dynamic-delete delete (require.cache as { [key: string]: any })[resolvedConfigFilePath]; } diff --git a/src/rules/noDynamicDeleteRule.ts b/src/rules/noDynamicDeleteRule.ts new file mode 100644 index 00000000000..ad7ae0f6629 --- /dev/null +++ b/src/rules/noDynamicDeleteRule.ts @@ -0,0 +1,102 @@ +/** + * @license + * Copyright 2013 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 * as tsutils from "tsutils"; +import * as ts from "typescript"; + +import * as Lint from "../index"; + +export class Rule extends Lint.Rules.AbstractRule { + public static metadata: Lint.IRuleMetadata = { + description: "Bans usage of the delete operator with computed key expressions.", + optionExamples: [true], + options: null, + optionsDescription: "Not configurable.", + rationale: "Deleting dynamically computed keys is dangerous and not well optimized.", + ruleName: "no-dynamic-delete", + type: "functionality", + typescriptOnly: false, + }; + + public static FAILURE_STRING = "Do not delete dynamically computed property keys."; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithFunction(sourceFile, walk); + } +} + +function walk(context: Lint.WalkContext) { + function checkDeleteAccessExpression(node: ts.Expression | undefined): void { + if (node === undefined || !tsutils.isElementAccessExpression(node)) { + return; + } + + const { argumentExpression } = node; + if (argumentExpression === undefined || isNecessaryDynamicAccess(argumentExpression)) { + return; + } + + const start = argumentExpression.getStart(context.sourceFile) - 1; + const width = argumentExpression.getWidth() + 2; + let fix: Lint.Replacement | undefined; + + if (tsutils.isPrefixUnaryExpression(argumentExpression)) { + const convertedOperand = convertUnaryOperand(argumentExpression); + if (convertedOperand !== undefined) { + fix = Lint.Replacement.replaceFromTo(start, start + width, `[${convertedOperand}]`); + } + } else if (tsutils.isStringLiteral(argumentExpression)) { + fix = Lint.Replacement.replaceFromTo(start, start + width, `.${argumentExpression.text}`); + } + + context.addFailureAt(start, width, Rule.FAILURE_STRING, fix); + } + + return ts.forEachChild(context.sourceFile, function callback(node: ts.Node): void { + if (isDeleteExpression(node)) { + checkDeleteAccessExpression(node.expression); + } + + return ts.forEachChild(node, callback); + }); +} + +function convertUnaryOperand(node: ts.PrefixUnaryExpression) { + return tsutils.isNumericLiteral(node.operand) + ? node.operand.text + : undefined; +} + +function isDeleteExpression(node: ts.Node): node is ts.DeleteExpression { + return node.kind === ts.SyntaxKind.DeleteExpression; +} + +function isNumberLike(node: ts.Node): boolean { + if (tsutils.isPrefixUnaryExpression(node)) { + return tsutils.isNumericLiteral(node.operand) && node.operator === ts.SyntaxKind.MinusToken; + } + + return tsutils.isNumericLiteral(node); +} + +function isNecessaryDynamicAccess(argumentExpression: ts.Expression): boolean { + if (isNumberLike(argumentExpression)) { + return true; + } + + return tsutils.isStringLiteral(argumentExpression) && !tsutils.isValidPropertyAccess(argumentExpression.text); +} diff --git a/test/rules/no-dynamic-delete/test.ts.fix b/test/rules/no-dynamic-delete/test.ts.fix new file mode 100644 index 00000000000..0cefd09ec54 --- /dev/null +++ b/test/rules/no-dynamic-delete/test.ts.fix @@ -0,0 +1,19 @@ +const container: { [i: string]: 0 } = {}; + +const getName = () => ""; + +delete container.aaa; +delete container["bb" + "b"]; +delete container.ccc; +delete container.delete; +delete container.delete; +delete container[7]; +delete container[-7]; +delete container[7]; +delete container[-Infinity]; +delete container["-Infinity"]; +delete container[+Infinity]; +delete container["+Infinity"]; +delete container[NaN]; +delete container.NaN; +delete container[getName()]; diff --git a/test/rules/no-dynamic-delete/test.ts.lint b/test/rules/no-dynamic-delete/test.ts.lint new file mode 100644 index 00000000000..51dbc6295d2 --- /dev/null +++ b/test/rules/no-dynamic-delete/test.ts.lint @@ -0,0 +1,29 @@ +const container: { [i: string]: 0 } = {}; + +const getName = () => ""; + +delete container.aaa; +delete container["bb" + "b"]; + ~~~~~~~~~~~~ [0] +delete container["ccc"]; + ~~~~~~~ [0] +delete container.delete; +delete container["delete"]; + ~~~~~~~~~~ [0] +delete container[7]; +delete container[-7]; +delete container[+7]; + ~~~~ [0] +delete container[-Infinity]; + ~~~~~~~~~~~ [0] +delete container["-Infinity"]; +delete container[+Infinity]; + ~~~~~~~~~~~ [0] +delete container["+Infinity"]; +delete container[NaN]; + ~~~~~ [0] +delete container["NaN"]; + ~~~~~~~ [0] +delete container[getName()]; + ~~~~~~~~~~~ [0] +[0]: Do not delete dynamically computed property keys. diff --git a/test/rules/no-dynamic-delete/tslint.json b/test/rules/no-dynamic-delete/tslint.json new file mode 100644 index 00000000000..f0c7404cfbf --- /dev/null +++ b/test/rules/no-dynamic-delete/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-dynamic-delete": true + } +}