eslint-plugin-rxjs-angular
Version:
ESLint rules for RxJS and Angular
264 lines (263 loc) • 12.2 kB
JavaScript
;
const common_tags_1 = require("common-tags");
const eslint_etc_1 = require("eslint-etc");
const utils_1 = require("../utils");
const messages = {
noDestroy: "`ngOnDestroy` is not implemented.",
noTakeUntil: "Forbids calling `subscribe` without an accompanying `takeUntil`.",
notCalled: "`{{name}}.{{method}}()` not called.",
notDeclared: "Subject `{{name}}` not a class property.",
};
const defaultOptions = [];
const rule = (0, utils_1.ruleCreator)({
defaultOptions,
meta: {
docs: {
description: "Forbids `subscribe` calls without an accompanying `takeUntil` within Angular components (and, optionally, within services, directives, and pipes).",
recommended: false,
},
fixable: undefined,
hasSuggestions: false,
messages,
schema: [
{
properties: {
alias: { type: "array", items: { type: "string" } },
checkComplete: { type: "boolean" },
checkDecorators: { type: "array", items: { type: "string" } },
checkDestroy: { type: "boolean" },
},
type: "object",
description: (0, common_tags_1.stripIndent) `
An optional object with optional \`alias\`, \`checkComplete\`, \`checkDecorators\` and \`checkDestroy\` properties.
The \`alias\` property is an array containing the names of operators that aliases for \`takeUntil\`.
The \`checkComplete\` property is a boolean that determines whether or not \`complete\` must be called after \`next\`.
The \`checkDecorators\` property is an array containing the names of the decorators that determine whether or not a class is checked.
The \`checkDestroy\` property is a boolean that determines whether or not a \`Subject\`-based \`ngOnDestroy\` must be implemented.
`,
},
],
type: "problem",
},
name: "prefer-takeuntil",
create: (context, unused) => {
const { couldBeObservable } = (0, eslint_etc_1.getTypeServices)(context);
const [config = {}] = context.options;
const { alias = [], checkComplete = false, checkDecorators = ["Component"], checkDestroy = alias.length === 0, } = config;
const entries = [];
function checkEntry(entry) {
const { subscribeCallExpressions } = entry;
subscribeCallExpressions.forEach((callExpression) => {
const { callee } = callExpression;
if (!(0, eslint_etc_1.isMemberExpression)(callee)) {
return;
}
const { object } = callee;
if (!couldBeObservable(object)) {
return;
}
checkSubscribe(callExpression, entry);
});
if (checkDestroy) {
checkNgOnDestroy(entry);
}
}
function checkNgOnDestroy(entry) {
var _a;
const { classDeclaration, completeCallExpressions, nextCallExpressions, ngOnDestroyDefinition, subscribeCallExpressionsToNames, } = entry;
if (subscribeCallExpressionsToNames.size === 0) {
return;
}
if (!ngOnDestroyDefinition) {
context.report({
messageId: "noDestroy",
node: (_a = classDeclaration.id) !== null && _a !== void 0 ? _a : classDeclaration,
});
return;
}
const namesToChecks = new Map();
const names = new Set();
subscribeCallExpressionsToNames.forEach((value) => value.forEach((name) => names.add(name)));
names.forEach((name) => {
var _a;
const check = {
descriptors: [],
report: false,
};
namesToChecks.set(name, check);
if (!checkSubjectProperty(name, entry)) {
check.descriptors.push({
data: { name },
messageId: "notDeclared",
node: (_a = classDeclaration.id) !== null && _a !== void 0 ? _a : classDeclaration,
});
}
if (!checkSubjectCall(name, nextCallExpressions)) {
check.descriptors.push({
data: { method: "next", name },
messageId: "notCalled",
node: ngOnDestroyDefinition.key,
});
}
if (checkComplete && !checkSubjectCall(name, completeCallExpressions)) {
check.descriptors.push({
data: { method: "complete", name },
messageId: "notCalled",
node: ngOnDestroyDefinition.key,
});
}
});
subscribeCallExpressionsToNames.forEach((names) => {
const report = [...names].every((name) => namesToChecks.get(name).descriptors.length > 0);
if (report) {
names.forEach((name) => (namesToChecks.get(name).report = true));
}
});
namesToChecks.forEach((check) => {
if (check.report) {
check.descriptors.forEach((descriptor) => context.report(descriptor));
}
});
}
function checkOperator(callExpression) {
const { callee } = callExpression;
if (!(0, eslint_etc_1.isIdentifier)(callee)) {
return { found: false };
}
if (callee.name === "takeUntil" || alias.includes(callee.name)) {
const [arg] = callExpression.arguments;
if (arg) {
if ((0, eslint_etc_1.isMemberExpression)(arg) &&
(0, eslint_etc_1.isThisExpression)(arg.object) &&
(0, eslint_etc_1.isIdentifier)(arg.property)) {
return { found: true, name: arg.property.name };
}
else if (arg && (0, eslint_etc_1.isIdentifier)(arg)) {
return { found: true, name: arg.name };
}
}
if (!checkDestroy) {
return { found: true };
}
}
return { found: false };
}
function checkSubjectCall(name, callExpressions) {
const callExpression = callExpressions.find(({ callee }) => ((0, eslint_etc_1.isMemberExpression)(callee) &&
(0, eslint_etc_1.isIdentifier)(callee.object) &&
callee.object.name === name) ||
((0, eslint_etc_1.isMemberExpression)(callee) &&
(0, eslint_etc_1.isMemberExpression)(callee.object) &&
(0, eslint_etc_1.isThisExpression)(callee.object.object) &&
(0, eslint_etc_1.isIdentifier)(callee.object.property) &&
callee.object.property.name === name));
return Boolean(callExpression);
}
function checkSubjectProperty(name, entry) {
const { propertyDefinitions } = entry;
const propertyDefinition = propertyDefinitions.find((propertyDefinition) => propertyDefinition.key.name === name);
return Boolean(propertyDefinition);
}
function checkSubscribe(callExpression, entry) {
const { subscribeCallExpressionsToNames } = entry;
const names = subscribeCallExpressionsToNames.get(callExpression);
let takeUntilFound = false;
const { callee } = callExpression;
if (!(0, eslint_etc_1.isMemberExpression)(callee)) {
return;
}
const { object, property } = callee;
if ((0, eslint_etc_1.isCallExpression)(object) &&
(0, eslint_etc_1.isMemberExpression)(object.callee) &&
(0, eslint_etc_1.isIdentifier)(object.callee.property) &&
object.callee.property.name === "pipe") {
const operators = object.arguments;
operators.forEach((operator) => {
if ((0, eslint_etc_1.isCallExpression)(operator)) {
const { found, name } = checkOperator(operator);
takeUntilFound = takeUntilFound || found;
if (name) {
names.add(name);
}
}
});
}
if (!takeUntilFound) {
context.report({
messageId: "noTakeUntil",
node: property,
});
}
}
function getEntry() {
const { length, [length - 1]: entry } = entries;
return entry;
}
function hasDecorator(node) {
const { decorators } = node;
return (decorators &&
decorators.some((decorator) => {
const { expression } = decorator;
if (!(0, eslint_etc_1.isCallExpression)(expression)) {
return false;
}
if (!(0, eslint_etc_1.isIdentifier)(expression.callee)) {
return false;
}
const { name } = expression.callee;
return checkDecorators.some((check) => name === check);
}));
}
return {
"CallExpression[callee.property.name='subscribe']": (node) => {
const entry = getEntry();
if (entry && entry.hasDecorator) {
entry.subscribeCallExpressions.push(node);
entry.subscribeCallExpressionsToNames.set(node, new Set());
}
},
ClassDeclaration: (node) => {
entries.push({
classDeclaration: node,
propertyDefinitions: [],
completeCallExpressions: [],
nextCallExpressions: [],
hasDecorator: hasDecorator(node),
subscribeCallExpressions: [],
subscribeCallExpressionsToNames: new Map(),
});
},
"ClassDeclaration:exit": (node) => {
const entry = entries.pop();
if (entry && entry.hasDecorator) {
checkEntry(entry);
}
},
PropertyDefinition: (node) => {
const entry = getEntry();
if (entry && entry.hasDecorator) {
entry.propertyDefinitions.push(node);
}
},
"MethodDefinition[key.name='ngOnDestroy'][kind='method']": (node) => {
const entry = getEntry();
if (entry && entry.hasDecorator) {
entry.ngOnDestroyDefinition = node;
}
},
"MethodDefinition[key.name='ngOnDestroy'][kind='method'] CallExpression[callee.property.name='next']": (node) => {
const entry = getEntry();
if (entry && entry.hasDecorator) {
entry.nextCallExpressions.push(node);
}
},
"MethodDefinition[key.name='ngOnDestroy'][kind='method'] CallExpression[callee.property.name='complete']": (node) => {
const entry = getEntry();
if (entry && entry.hasDecorator) {
entry.completeCallExpressions.push(node);
}
},
};
},
});
module.exports = rule;