tslint-etc
Version:
More rules for TSLint
186 lines (185 loc) • 7.18 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Rule = void 0;
const Lint = require("tslint");
const tsutils = require("tsutils");
const ts = require("typescript");
const support_1 = require("../support");
const util_1 = require("../support/util");
const knownGlobalRegExp = /^(Array|BigInt|Date|Intl|JSON|Math|Number|Object|Promise|Proxy|Reflect|String|Symbol|console)$/;
class Rule extends Lint.Rules.TypedRule {
applyWithProgram(sourceFile, program) {
return this.applyWithWalker(new Walker(sourceFile, this.getOptions(), program));
}
}
exports.Rule = Rule;
Rule.metadata = {
description: "Disallows the use of variables/properties from unsafe/outer scopes in callbacks.",
options: {
properties: {
allowMethods: { type: "boolean" },
allowParameters: { type: "boolean" },
allowProperties: { type: "boolean" },
},
type: "object",
},
optionsDescription: Lint.Utils.dedent `
An optional object with optional \`allowDo\`, \`allowParameters\` and \`allowTap\` properties all of which default to \`true\`.
If the \`allowParameters\` option is \`true\`, referencing function parameters from outer scopes is allowed.
If the \`allowMethods\` option is \`true\`, calling methods via \`this\` is allowed.
If the \`allowProperties\` option is \`true\`, accessing properties via \`this\` is allowed.`,
requiresTypeInfo: true,
ruleName: "no-unsafe-callback-scope",
type: "functionality",
typescriptOnly: true,
};
Rule.FAILURE_STRING = "Unsafe scopes are forbidden";
class Walker extends support_1.ScopeWalker {
constructor(sourceFile, rawOptions, program) {
super(sourceFile, rawOptions, program);
this.allowMethods = true;
this.allowParameters = true;
this.allowProperties = false;
const [options] = this.getOptions();
if (options) {
this.allowMethods =
options.allowMethods !== undefined
? options.allowMethods
: this.allowMethods;
this.allowParameters =
options.allowParameters !== undefined
? options.allowParameters
: this.allowParameters;
this.allowProperties =
options.allowProperties !== undefined
? options.allowProperties
: this.allowProperties;
}
}
visitNode(node) {
if (this.callbackStack.length) {
const validateNode = tsutils.isIdentifier(node) || util_1.isThis(node);
if (validateNode) {
const failureNode = this.isUnsafe(node);
if (failureNode) {
this.addFailureAtNode(failureNode, Rule.FAILURE_STRING);
}
}
}
super.visitNode(node);
}
isUnsafe(node) {
const { callbackMap, callbackStack } = this;
const leafCallback = callbackStack[callbackStack.length - 1];
const leafOperator = callbackMap.get(leafCallback);
const rootCallback = callbackStack[0];
const typeChecker = this.getTypeChecker();
if (tsutils.isPropertyAccessExpression(node.parent)) {
if (!isPropertyAccessExpressionLeaf(node)) {
return undefined;
}
const declaration = util_1.findDeclaration(node, typeChecker);
if (!declaration) {
return undefined;
}
if (tsutils.hasModifier(declaration.modifiers, ts.SyntaxKind.ReadonlyKeyword)) {
return undefined;
}
if (tsutils.isTypeFlagSet(typeChecker.getTypeAtLocation(node), ts.TypeFlags.EnumLiteral)) {
return undefined;
}
const called = util_1.isWithinCallExpressionExpression(node);
const root = getPropertyAccessExpressionRoot(node.parent);
if (!root) {
return undefined;
}
if (util_1.isThis(root)) {
if (called) {
return this.allowMethods ? undefined : root;
}
else {
return this.allowProperties ? undefined : root;
}
}
const rootText = root.getText();
if (knownGlobalRegExp.test(rootText)) {
return undefined;
}
if (/^[A-Z]/.test(rootText)) {
if (called) {
return this.allowMethods ? undefined : root;
}
else {
return this.allowProperties ? undefined : root;
}
}
return this.isUnsafeRoot(root, rootCallback);
}
return this.isUnsafeRoot(node, rootCallback);
}
isUnsafeRoot(node, callback) {
const typeChecker = this.getTypeChecker();
if (ts.isQualifiedName(node.parent)) {
return undefined;
}
if (util_1.isInstanceofCtor(node)) {
return undefined;
}
const declaration = util_1.findDeclaration(node, typeChecker);
if (!declaration) {
return undefined;
}
if (isWithinClosure(declaration, callback)) {
return undefined;
}
if (this.allowParameters && util_1.isWithinParameterDeclaration(declaration)) {
return undefined;
}
if (tsutils.isCallExpression(node.parent) &&
node === node.parent.expression) {
return undefined;
}
if (tsutils.isTaggedTemplateExpression(node.parent) &&
node === node.parent.tag) {
return undefined;
}
if (tsutils.isNewExpression(node.parent)) {
return undefined;
}
if (tsutils.isTypeReferenceNode(node.parent)) {
return undefined;
}
if (util_1.isConstDeclaration(declaration)) {
return undefined;
}
if (tsutils.isImportSpecifier(declaration)) {
return undefined;
}
if (tsutils.isNamespaceImport(declaration)) {
return undefined;
}
return node;
}
}
function getPropertyAccessExpressionRoot(node) {
let { expression } = node;
while (tsutils.isPropertyAccessExpression(expression)) {
expression = expression.expression;
}
return util_1.isThis(expression) || tsutils.isIdentifier(expression)
? expression
: undefined;
}
function isWithinClosure(declaration, callback) {
return declaration.pos >= callback.pos && declaration.pos < callback.end;
}
function isPropertyAccessExpressionLeaf(node) {
const { parent } = node;
if (!tsutils.isPropertyAccessExpression(parent)) {
return false;
}
if (node !== parent.name) {
return false;
}
return !tsutils.isPropertyAccessExpression(parent.parent);
}