Skip to content

Instantly share code, notes, and snippets.

@zlepper
Created August 26, 2020 12:28
Show Gist options
  • Save zlepper/df282c06bafe46342d5b43f4e049726a to your computer and use it in GitHub Desktop.
Save zlepper/df282c06bafe46342d5b43f4e049726a to your computer and use it in GitHub Desktop.
import { Rules, RuleWalker } from 'tslint';
import * as Lint from 'tslint';
import { SourceFile, ClassDeclaration, SyntaxKind, FunctionExpression } from 'typescript';
const DESTROY_HOOK_NAME = 'ngOnDestroy';
export class Rule extends Rules.AbstractRule {
public static FAILURE_STRING = 'SubSinkService not destroyed. This will likely cause a subscription leak.';
public apply(sourceFile: SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(new DestroySubSinkWalker(sourceFile, this.getOptions()));
}
}
// tslint:disable-next-line deprecation
class DestroySubSinkWalker extends RuleWalker {
protected visitClassDeclaration(node: ClassDeclaration) {
super.visitClassDeclaration(node);
const subSinkServiceName = this.getOptions()[0];
const sinkServiceMembers = node.members
.filter(member => member.kind === SyntaxKind.PropertyDeclaration)
.filter((member: any) => {
// Type defined as in "private foo: SubSinkService"
if (member.type?.getText() === subSinkServiceName) {
return true;
}
// type infered from assigment "private foo = new SubSinkService()"
if (member?.initializer?.expression?.getText() === subSinkServiceName) {
return true;
}
// No match, no detection
return false;
});
// no sinks declared
if (sinkServiceMembers.length === 0) {
return;
}
// A set of all the sink services instance names
const sinkServicesSet = new Set(sinkServiceMembers.map(member => member.name.getText()));
// A set of destroyed sink services instance names
const destroyMethod = this.getDestroyedMethod(node);
const destroyedSinkServices = this.getDestroyedServices(destroyMethod);
destroyedSinkServices.forEach(service => sinkServicesSet.delete(service));
// Report error
sinkServiceMembers.forEach(member => {
const name = member.name.getText();
if (sinkServicesSet.has(name)) {
this.addFailureAtNode(member, Rule.FAILURE_STRING);
}
});
}
protected getDestroyedServices(destroyMethod: FunctionExpression): string[] {
if (!destroyMethod) {
return [];
}
return destroyMethod.body.statements
.filter((statement: any) => statement.expression.expression.name.getText() === DESTROY_HOOK_NAME)
.map((statement: any) => statement.expression.expression.expression.name.getText());
}
protected getDestroyedMethod(node: ClassDeclaration): FunctionExpression {
// Search for ngOnDestroy on the class
return (node.members.find(
member => member.kind === SyntaxKind.MethodDeclaration && member.name.getText() === DESTROY_HOOK_NAME,
) as any) as FunctionExpression;
}
}
import * as Lint from 'tslint';
import { SourceFile, TypeReferenceNode, ClassDeclaration, ConstructorDeclaration, Decorator, SyntaxKind } from 'typescript';
import { RuleWalker, Rules } from 'tslint';
const COMPONENT_DECORATOR = 'Component';
const DIRECTIVE_DECORATOR = 'Directive';
const PROVIDERS_PROPRIERY_NAME = 'providers';
export class Rule extends Rules.AbstractRule {
public static FAILURE_STRING(serviceName: string): string {
return `"${serviceName}" is not provided in the component. This will likely cause a subscription leak.`;
}
public apply(sourceFile: SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(new ProvideServiceWalker(sourceFile, this.getOptions()));
}
}
// tslint:disable-next-line deprecation
class ProvideServiceWalker extends RuleWalker {
protected visitConstructorDeclaration(node: ConstructorDeclaration) {
super.visitConstructorDeclaration(node);
// console.log(`---------visitConstructorDeclaration----------`);
const lintingServicesSet = new Set(this.getOptions());
// We can skip abstract class as they can't be initialized on their own
const isAbstractClass = this.isAbstractClass(node.parent as ClassDeclaration);
if (isAbstractClass) {
return;
}
// List of services Injected
const DIServices = node.parameters
.map(parameter => this.getParameterTypeName(parameter))
.filter(typeName => lintingServicesSet.has(typeName));
const DIServicesSet = new Set(DIServices);
const providedServices = this.getProvidedServicesForClass(node.parent as ClassDeclaration);
providedServices.forEach(service => DIServicesSet.delete(service));
if (DIServicesSet.size > 0) {
node.parameters.forEach(parameter => {
const typeName = this.getParameterTypeName(parameter);
if (DIServicesSet.has(typeName)) {
this.addFailureAtNode(parameter, Rule.FAILURE_STRING(typeName));
}
});
}
// console.log('-------------------');
}
protected getParameterTypeName(parameter): string {
return (parameter.type as TypeReferenceNode)?.typeName?.getText() || '';
}
protected isAbstractClass(node: ClassDeclaration): boolean {
return node.modifiers?.some(modifier => modifier.kind === SyntaxKind.AbstractKeyword);
}
protected getProvidedServicesForClass(node: ClassDeclaration): string[] {
// Search for the @Component decorator
const ngDecorator = node.decorators?.find((decorator: Decorator) => {
const decoratorName = (decorator.expression as any)?.expression.getText();
return [DIRECTIVE_DECORATOR, COMPONENT_DECORATOR].includes(decoratorName);
});
if (!ngDecorator) {
return [];
}
// Search for { ... }
const componentArgs = (ngDecorator.expression as any).arguments[0];
if (!componentArgs) {
return [];
}
// Search for providers: [...]
const providersPropriety = componentArgs.properties.find(propriety => propriety.name.getText() === PROVIDERS_PROPRIERY_NAME);
if (!providersPropriety) {
return [];
}
// Get the content of the proviers array
return providersPropriety.initializer.elements.map(element => element.getText());
}
}
{
"rulesDirectory": [
"scripts/lint-sub-sink/build"
],
"rules": {
"provide-service": [true, "SubSinkService"],
"destroy-sub-sink": [true, "SubSinkService"]
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment