rxjs-tslint-rules
Version:
TSLint rules for RxJS
182 lines (181 loc) • 12 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Rule = void 0;
var tslib_1 = require("tslib");
var tsquery_1 = require("@phenomnomnominal/tsquery");
var Lint = tslib_1.__importStar(require("tslint"));
var tsutils = tslib_1.__importStar(require("tsutils"));
var ts = tslib_1.__importStar(require("typescript"));
var peer = tslib_1.__importStar(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 _a = tslib_1.__read(this.getOptions().ruleArguments, 1), options = _a[0];
var _b = options || {}, _c = _b.alias, alias = _c === void 0 ? [] : _c, _d = _b.checkDestroy, checkDestroy = _d === void 0 ? alias.length === 0 : _d, _e = _b.checkDecorators, checkDecorators = _e === void 0 ? ["Component"] : _e;
var decoratorQuery = "/^(" + checkDecorators.join("|") + ")$/";
var classDeclarations = tsquery_1.tsquery(sourceFile, "ClassDeclaration:has(Decorator[expression.expression.name=" + decoratorQuery + "])");
classDeclarations.forEach(function (classDeclaration) {
failures.push.apply(failures, tslib_1.__spreadArray([], tslib_1.__read(_this.checkClassDeclaration(sourceFile, program, { alias: alias, checkDestroy: checkDestroy, checkDecorators: checkDecorators }, classDeclaration))));
});
return failures;
};
Rule.prototype.checkClassDeclaration = function (sourceFile, program, options, classDeclaration) {
var _this = this;
var failures = [];
var typeChecker = program.getTypeChecker();
var destroySubjectNamesBySubscribes = new Map();
var subscribePropertyAccessExpressions = tsquery_1.tsquery(classDeclaration, "CallExpression > PropertyAccessExpression[name.name=\"subscribe\"]");
subscribePropertyAccessExpressions.forEach(function (propertyAccessExpression) {
var type = typeChecker.getTypeAtLocation(propertyAccessExpression.expression);
if (util_1.couldBeType(type, "Observable")) {
failures.push.apply(failures, tslib_1.__spreadArray([], tslib_1.__read(_this.checkSubscribe(sourceFile, options, propertyAccessExpression, function (name) {
var names = destroySubjectNamesBySubscribes.get(propertyAccessExpression.name);
if (!names) {
names = new Set();
destroySubjectNamesBySubscribes.set(propertyAccessExpression.name, names);
}
names.add(name);
}))));
}
});
if (options.checkDestroy && destroySubjectNamesBySubscribes.size > 0) {
failures.push.apply(failures, tslib_1.__spreadArray([], tslib_1.__read(this.checkNgOnDestroy(sourceFile, classDeclaration, destroySubjectNamesBySubscribes))));
}
return failures;
};
Rule.prototype.checkSubscribe = function (sourceFile, options, subscribe, addDestroySubjectName) {
var _this = this;
var failures = [];
var subscribeContext = subscribe.expression;
var takeUntilFound = false;
if (tsutils.isCallExpression(subscribeContext) &&
tsutils.isPropertyAccessExpression(subscribeContext.expression) &&
subscribeContext.expression.name.text === "pipe") {
var pipedOperators = subscribeContext.arguments;
pipedOperators.forEach(function (pipedOperator) {
if (tsutils.isCallExpression(pipedOperator)) {
var _a = _this.checkOperator(options, pipedOperator), found = _a.found, name_1 = _a.name;
takeUntilFound = takeUntilFound || found;
if (name_1) {
addDestroySubjectName(name_1);
}
}
});
}
if (!takeUntilFound) {
failures.push(new Lint.RuleFailure(sourceFile, subscribe.name.getStart(), subscribe.name.getStart() + subscribe.name.getWidth(), Rule.FAILURE_STRING_NO_TAKEUNTIL, this.ruleName));
}
return failures;
};
Rule.prototype.checkOperator = function (options, operator) {
if (!tsutils.isIdentifier(operator.expression)) {
return { found: false };
}
if (operator.expression.text === "takeUntil" ||
options.alias.includes(operator.expression.text)) {
var _a = tslib_1.__read(operator.arguments, 1), arg = _a[0];
if (arg) {
if (ts.isPropertyAccessExpression(arg) && util_1.isThis(arg.expression)) {
return { found: true, name: arg.name.text };
}
else if (arg && ts.isIdentifier(arg)) {
return { found: true, name: arg.text };
}
}
if (!options.checkDestroy) {
return { found: true };
}
}
return { found: false };
};
Rule.prototype.checkNgOnDestroy = function (sourceFile, classDeclaration, destroySubjectNamesBySubscribes) {
var _this = this;
var failures = [];
var ngOnDestroyMethod = classDeclaration.members.find(function (member) { return member.name && member.name.getText() === "ngOnDestroy"; });
if (ngOnDestroyMethod) {
var destroySubjectNames_1 = new Set();
destroySubjectNamesBySubscribes.forEach(function (names) {
return names.forEach(function (name) { return destroySubjectNames_1.add(name); });
});
var destroySubjectResultsByName_1 = new Map();
destroySubjectNames_1.forEach(function (name) {
destroySubjectResultsByName_1.set(name, {
failures: tslib_1.__spreadArray(tslib_1.__spreadArray(tslib_1.__spreadArray([], tslib_1.__read(_this.checkDestroySubjectDeclaration(sourceFile, classDeclaration, name))), tslib_1.__read(_this.checkDestroySubjectMethodInvocation(sourceFile, ngOnDestroyMethod, name, "next"))), tslib_1.__read(_this.checkDestroySubjectMethodInvocation(sourceFile, ngOnDestroyMethod, name, "complete"))),
report: false,
});
});
destroySubjectNamesBySubscribes.forEach(function (names) {
var report = tslib_1.__spreadArray([], tslib_1.__read(names)).every(function (name) { return destroySubjectResultsByName_1.get(name).failures.length > 0; });
if (report) {
names.forEach(function (name) { return (destroySubjectResultsByName_1.get(name).report = true); });
}
});
destroySubjectResultsByName_1.forEach(function (result) {
if (result.report) {
failures.push.apply(failures, tslib_1.__spreadArray([], tslib_1.__read(result.failures)));
}
});
}
else {
failures.push(new Lint.RuleFailure(sourceFile, classDeclaration.name.getStart(), classDeclaration.name.getStart() + classDeclaration.name.getWidth(), Rule.FAILURE_STRING_NO_DESTROY, this.ruleName));
}
return failures;
};
Rule.prototype.checkDestroySubjectDeclaration = function (sourceFile, classDeclaration, destroySubjectName) {
var failures = [];
var propertyDeclarations = tsquery_1.tsquery(classDeclaration, "PropertyDeclaration[name.text=\"" + destroySubjectName + "\"]");
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(destroySubjectName), this.ruleName));
}
return failures;
};
Rule.prototype.checkDestroySubjectMethodInvocation = function (sourceFile, ngOnDestroyMethod, destroySubjectName, methodName) {
var failures = [];
var destroySubjectMethodInvocations = tsquery_1.tsquery(ngOnDestroyMethod, "CallExpression > PropertyAccessExpression[name.name=\"" + methodName + "\"]");
if (!destroySubjectMethodInvocations.some(function (methodInvocation) {
return (tsutils.isPropertyAccessExpression(methodInvocation.expression) &&
util_1.isThis(methodInvocation.expression.expression) &&
methodInvocation.expression.name.text === destroySubjectName) ||
(tsutils.isIdentifier(methodInvocation.expression) &&
methodInvocation.expression.text === destroySubjectName);
})) {
failures.push(new Lint.RuleFailure(sourceFile, ngOnDestroyMethod.name.getStart(), ngOnDestroyMethod.name.getStart() + ngOnDestroyMethod.name.getWidth(), Rule.FAILURE_MESSAGE_NOT_CALLED(destroySubjectName, methodName), this.ruleName));
}
return failures;
};
Rule.metadata = {
deprecationMessage: peer.v5 ? peer.v5NotSupportedMessage : undefined,
description: Lint.Utils.dedent(templateObject_1 || (templateObject_1 = tslib_1.__makeTemplateObject(["\n Enforces the application of the takeUntil operator\n when calling subscribe within Angular components\n (and, optionally, within services, directives, and pipes)."], ["\n Enforces the application of the takeUntil operator\n when calling subscribe within Angular components\n (and, optionally, within services, directives, and pipes)."]))),
options: {
properties: {
alias: { type: "array", items: { type: "string" } },
checkDecorators: { type: "array", items: { type: "string" } },
checkDestroy: { type: "boolean" },
},
type: "object",
},
optionsDescription: Lint.Utils.dedent(templateObject_2 || (templateObject_2 = tslib_1.__makeTemplateObject(["\n An optional object with optional `alias`, `checkDecorators` and `checkDestroy` properties.\n The `alias` property is an array containing the names of operators that aliases for `takeUntil`.\n The `checkDecorators` property is an array containing the names of the decorators that determine whether or not a class is checked.\n The `checkDestroy` property is a boolean that determines whether or not a `Subject`-based `ngOnDestroy` must be implemented."], ["\n An optional object with optional \\`alias\\`, \\`checkDecorators\\` and \\`checkDestroy\\` properties.\n The \\`alias\\` property is an array containing the names of operators that aliases for \\`takeUntil\\`.\n The \\`checkDecorators\\` property is an array containing the names of the decorators that determine whether or not a class is checked.\n The \\`checkDestroy\\` property is a boolean that determines whether or not a \\`Subject\\`-based \\`ngOnDestroy\\` must be implemented."]))),
requiresTypeInfo: true,
ruleName: "rxjs-prefer-angular-takeuntil",
type: "functionality",
typescriptOnly: true,
};
Rule.FAILURE_STRING_NO_TAKEUNTIL = "Subscribing without takeUntil is forbidden";
Rule.FAILURE_STRING_NO_DESTROY = "ngOnDestroy not implemented";
Rule.FAILURE_MESSAGE_NOT_CALLED = function (name, method) {
return "'" + name + "." + method + "()' not called";
};
Rule.FAILURE_MESSAGE_NOT_DECLARED = function (name) {
return "Subject '" + name + "' not a class property";
};
return Rule;
}(Lint.Rules.TypedRule));
exports.Rule = Rule;
var templateObject_1, templateObject_2;