rxjs-tslint-rules
Version:
TSLint rules for RxJS
171 lines (170 loc) • 7.82 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var tsquery_1 = require("@phenomnomnominal/tsquery");
var Lint = require("tslint");
var ts = require("typescript");
var peer = require("../support/peer");
var util_1 = require("../support/util");
var Rule = (function (_super) {
tslib_1.__extends(Rule, _super);
function Rule() {
return _super !== null && _super.apply(this, arguments) || this;
}
Rule.prototype.applyWithProgram = function (sourceFile, program) {
var _this = this;
var failures = [];
var typeChecker = program.getTypeChecker();
var classDeclarations = tsquery_1.tsquery(sourceFile, "ClassDeclaration:has(Decorator[expression.expression.name=\"Component\"])");
classDeclarations.forEach(function (classDeclaration) {
var subscriptions = new Set();
var callExpressions = tsquery_1.tsquery(classDeclaration, "CallExpression[expression.name.text=\"subscribe\"]");
callExpressions.forEach(function (callExpression) {
var expression = callExpression.expression;
if (ts.isPropertyAccessExpression(expression)) {
var object = expression.expression, property = expression.name;
var type = typeChecker.getTypeAtLocation(object);
if (!util_1.couldBeType(type, "Observable")) {
return;
}
if (isComposed(callExpression, typeChecker, subscriptions)) {
return;
}
failures.push(new Lint.RuleFailure(sourceFile, property.getStart(), property.getStart() + property.getWidth(), Rule.FAILURE_STRING_NOT_COMPOSED, _this.ruleName));
}
});
if (callExpressions.length === 0) {
return;
}
var methodDeclarations = tsquery_1.tsquery(classDeclaration, "MethodDeclaration[name.text=\"ngOnDestroy\"]");
if (methodDeclarations.length === 0) {
var name_1 = classDeclaration.name;
failures.push(new Lint.RuleFailure(sourceFile, name_1.getStart(), name_1.getStart() + name_1.getWidth(), Rule.FAILURE_STRING_NOT_IMPLEMENTED, _this.ruleName));
return;
}
var _a = tslib_1.__read(methodDeclarations, 1), methodDeclaration = _a[0];
subscriptions.forEach(function (subscription) {
var propertyDeclarations = tsquery_1.tsquery(classDeclaration, "PropertyDeclaration[name.text=\"" + subscription + "\"]");
if (propertyDeclarations.length === 0) {
var name_2 = classDeclaration.name;
failures.push(new Lint.RuleFailure(sourceFile, name_2.getStart(), name_2.getStart() + name_2.getWidth(), Rule.FAILURE_MESSAGE_NOT_DECLARED(subscription), _this.ruleName));
return;
}
var _a = tslib_1.__read(propertyDeclarations, 1), propertyDeclaration = _a[0];
var callExpressions = tsquery_1.tsquery(methodDeclaration, "CallExpression[expression.expression.name.text=\"" + subscription + "\"][expression.name.text=\"unsubscribe\"], CallExpression[expression.expression.text=\"" + subscription + "\"][expression.name.text=\"unsubscribe\"]");
if (callExpressions.length === 0) {
var name_3 = propertyDeclaration.name;
failures.push(new Lint.RuleFailure(sourceFile, name_3.getStart(), name_3.getStart() + name_3.getWidth(), Rule.FAILURE_STRING_NOT_UNSUBSCRIBED, _this.ruleName));
}
});
});
return failures;
};
Rule.metadata = {
deprecationMessage: peer.v5 ? peer.v5NotSupportedMessage : undefined,
description: "Enforces the composition of subscriptions within an Angular component.",
options: null,
optionsDescription: "Not configurable.",
requiresTypeInfo: true,
ruleName: "rxjs-prefer-angular-composition",
type: "style",
typescriptOnly: true
};
Rule.FAILURE_STRING_NOT_COMPOSED = "Subscription not composed";
Rule.FAILURE_STRING_NOT_IMPLEMENTED = "ngOnDestroy not implemented";
Rule.FAILURE_STRING_NOT_UNSUBSCRIBED = "Composed subscription not unsubscribed";
Rule.FAILURE_MESSAGE_NOT_DECLARED = function (name) {
return "Composed subscription '" + name + "' not a class property";
};
return Rule;
}(Lint.Rules.TypedRule));
exports.Rule = Rule;
function getBlock(node) {
var parent = node.parent;
while (parent && !ts.isBlock(parent)) {
parent = parent.parent;
}
return parent;
}
function getCalledName(node) {
if (ts.isIdentifier(node)) {
return node;
}
else if (ts.isPropertyAccessExpression(node) && util_1.isThis(node.expression)) {
return node.name;
}
return undefined;
}
function getCalledObject(callExpression) {
var expression = callExpression.expression;
if (ts.isPropertyAccessExpression(expression)) {
return expression.expression;
}
return undefined;
}
function isComposed(callExpression, typeChecker, subscriptions) {
var parent = callExpression.parent;
if (ts.isCallExpression(parent) &&
ts.isPropertyAccessExpression(parent.expression)) {
var _a = parent.expression, object = _a.expression, property = _a.name;
var text = property.text;
if (text !== "add") {
return false;
}
if (!util_1.couldBeType(typeChecker.getTypeAtLocation(object), "Subscription")) {
return false;
}
var name_4 = getCalledName(object);
if (!name_4) {
return false;
}
subscriptions.add(name_4.text);
return true;
}
if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
return isVariableComposed(parent.name, typeChecker, subscriptions);
}
if (ts.isBinaryExpression(parent) &&
ts.isIdentifier(parent.left) &&
parent.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
return isVariableComposed(parent.left, typeChecker, subscriptions);
}
return false;
}
function isVariableComposed(identifier, typeChecker, subscriptions) {
var text = identifier.text;
var block = getBlock(identifier);
if (block) {
var callExpressions = tsquery_1.tsquery(block, "CallExpression[expression.name.text=\"add\"] > Identifier[text=\"" + text + "\"]")
.map(function (identifier) { return identifier.parent; })
.filter(function (callExpression) {
if (callExpression.end < identifier.pos) {
return false;
}
var object = getCalledObject(callExpression);
if (!object) {
return false;
}
if (!util_1.couldBeType(typeChecker.getTypeAtLocation(object), "Subscription")) {
return false;
}
return true;
});
if (callExpressions.length === 0) {
return false;
}
var _a = tslib_1.__read(callExpressions, 1), callExpression = _a[0];
var expression = callExpression.expression;
if (!ts.isPropertyAccessExpression(expression)) {
return false;
}
var object = expression.expression;
var name_5 = getCalledName(object);
if (!name_5) {
return false;
}
subscriptions.add(name_5.text);
return true;
}
return false;
}