Created
August 26, 2020 12:28
-
-
Save zlepper/df282c06bafe46342d5b43f4e049726a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Show hidden characters
{ | |
"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