diff --git a/.gitignore b/.gitignore index 248d183b..dbcd095a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ yarn-error.log /dist /node_modules /temp +/.idea diff --git a/README.md b/README.md index f9fc00be..9e75173e 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,7 @@ The package includes the following rules (none of which are enabled by default): | `rxjs-no-unsafe-takeuntil` | Disallows the application of operators after `takeUntil`. Operators placed after `takeUntil` can effect [subscription leaks](https://medium.com/@cartant/rxjs-avoiding-takeuntil-leaks-fb5182d047ef). | [See below](#rxjs-no-unsafe-takeuntil) | | `rxjs-no-unused-add` | Disallows the importation of patched observables or operators that are not used in the module. | None | | `rxjs-no-wholesale` | Disallows the wholesale importation of `rxjs` or `rxjs/Rx`. | None | +| `rxjs-prefer-angular-takeuntil-before-subscribe` | Enforces the application of the `takeUntil` operator when calling of `subscribe` within an Angular component. | [See below](#rxjs-prefer-angular-takeuntil-before-subscribe) | | `rxjs-prefer-angular-async-pipe` | Disallows the calling of `subscribe` within an Angular component. | None | | `rxjs-prefer-observer` | Enforces the passing of observers to `subscribe` and `tap`. See [this RxJS issue](https://github.com/ReactiveX/rxjs/issues/4159). | [See below](#rxjs-prefer-observer) | | `rxjs-suffix-subjects` | Disalllows subjects that don't end with the specified `suffix` option. | [See below](#rxjs-suffix-subjects) | @@ -395,6 +396,59 @@ The following options are equivalent to the rule's default configuration: } ``` + + +#### rxjs-prefer-angular-takeuntil-before-subscribe + +This rule tries to avoid memory leaks in angular components when calling `.subscribe()` without properly unsubscribing +by enforcing the application of the `takeUntil(this.destroy$)` operator before the `.subscribe()` +as well as before certain operators (`publish`, `publishBehavior`, `publishLast`, `publishReplay`, `shareReplay`) +and ensuring the component implements the `ngOnDestroy` +method invoking `this.destroy$.next()` and `this.destroy$.complete()`. + +##### Example +This should trigger an error: +```typescript +@Component({ + selector: 'app-my', + template: '
{{k$ | async}}
' +}) +class MyComponent { + ~~~~~~~~~~~ component containing subscribe must implement the ngOnDestroy() method + + + k$ = a.pipe(shareReplay(1)); + ~~~~~~~~~~~~~~ the shareReplay operator used within a component must be preceded by takeUntil + + someMethod() { + const e = a.pipe(switchMap(_ => b)).subscribe(); + ~~~~~~~~~ subscribe within a component must be preceded by takeUntil + } +} +``` + +while this should be fine: +```typescript +@Component({ + selector: 'app-my', + template: '
{{k$ | async}}
' +}) +class MyComponent implements SomeInterface, OnDestroy { + private destroy$: Subject = new Subject(); + + k$ = a.pipe(takeUntil(this.destroy$), shareReplay(1)); + + someMethod() { + const e = a.pipe(switchMap(_ => b), takeUntil(this.destroy$)).subscribe(); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } +} +``` + #### rxjs-prefer-observer diff --git a/docs/index.md b/docs/index.md index b4931bfd..4b0c31a1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -53,6 +53,7 @@ The package includes the following rules (none of which are enabled by default): | `rxjs-no-unsafe-takeuntil` | Disallows the application of operators after `takeUntil`. Operators placed after `takeUntil` can effect [subscription leaks](https://medium.com/@cartant/rxjs-avoiding-takeuntil-leaks-fb5182d047ef). | [See below](#rxjs-no-unsafe-takeuntil) | | `rxjs-no-unused-add` | Disallows the importation of patched observables or operators that are not used in the module. | None | | `rxjs-no-wholesale` | Disallows the wholesale importation of `rxjs` or `rxjs/Rx`. | None | +| `rxjs-prefer-angular-takeuntil-before-subscribe` | Enforces the application of the `takeUntil` operator when calling of `subscribe` within an Angular component. | [See below](#rxjs-prefer-angular-takeuntil-before-subscribe) | | `rxjs-prefer-angular-async-pipe` | Disallows the calling of `subscribe` within an Angular component. | None | | `rxjs-prefer-observer` | Enforces the passing of observers to `subscribe` and `tap`. See [this RxJS issue](https://github.com/ReactiveX/rxjs/issues/4159). | [See below](#rxjs-prefer-observer) | | `rxjs-suffix-subjects` | Disalllows subjects that don't end with the specified `suffix` option. | [See below](#rxjs-suffix-subjects) | @@ -331,6 +332,62 @@ The following options are equivalent to the rule's default configuration: } ``` + + + + + +#### rxjs-prefer-angular-takeuntil-before-subscribe + +This rule tries to avoid memory leaks in angular components when calling `.subscribe()` without properly unsubscribing +by enforcing the application of the `takeUntil(this.destroy$)` operator before the `.subscribe()` +as well as before certain operators (`publish`, `publishBehavior`, `publishLast`, `publishReplay`, `shareReplay`) +and ensuring the component implements the `ngOnDestroy` +method invoking `this.destroy$.next()` and `this.destroy$.complete()`. + +##### Example +This should trigger an error: +```typescript +@Component({ + selector: 'app-my', + template: '
{{k$ | async}}
' +}) +class MyComponent { + ~~~~~~~~~~~ component containing subscribe must implement the ngOnDestroy() method + + + k$ = a.pipe(shareReplay(1)); + ~~~~~~~~~~~~~~ the shareReplay operator used within a component must be preceded by takeUntil + + someMethod() { + const e = a.pipe(switchMap(_ => b)).subscribe(); + ~~~~~~~~~ subscribe within a component must be preceded by takeUntil + } +} +``` + +while this should be fine: +```typescript +@Component({ + selector: 'app-my', + template: '
{{k$ | async}}
' +}) +class MyComponent implements SomeInterface, OnDestroy { + private destroy$: Subject = new Subject(); + + k$ = a.pipe(takeUntil(this.destroy$), shareReplay(1)); + + someMethod() { + const e = a.pipe(switchMap(_ => b), takeUntil(this.destroy$)).subscribe(); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } +} +``` + #### rxjs-prefer-observer diff --git a/source/rules/rxjsPreferAngularTakeuntilBeforeSubscribeRule.ts b/source/rules/rxjsPreferAngularTakeuntilBeforeSubscribeRule.ts new file mode 100644 index 00000000..19c90641 --- /dev/null +++ b/source/rules/rxjsPreferAngularTakeuntilBeforeSubscribeRule.ts @@ -0,0 +1,438 @@ +/** + * @license Use of this source code is governed by an MIT-style license that + * can be found in the LICENSE file at https://github.com/cartant/rxjs-tslint-rules + */ +/*tslint:disable:no-use-before-declare*/ + +import * as Lint from "tslint"; +import * as tsutils from "tsutils"; +import * as ts from "typescript"; +import { couldBeType } from "../support/util"; +import { tsquery } from "@phenomnomnominal/tsquery"; +import { dedent } from "tslint/lib/utils"; + +export class Rule extends Lint.Rules.TypedRule { + public static metadata: Lint.IRuleMetadata = { + description: dedent`Enforces the application of the takeUntil operator + when calling of subscribe within an Angular component.`, + options: null, + optionsDescription: "", + requiresTypeInfo: true, + ruleName: "rxjs-prefer-angular-takeuntil-before-subscribe", + type: "functionality", + typescriptOnly: true + }; + + public static FAILURE_STRING = + "subscribe within a component must be preceded by takeUntil"; + + public static FAILURE_STRING_SUBJECT_NAME = + "takeUntil argument must be a property of the class, e.g. takeUntil(this.destroy$)"; + + public static FAILURE_STRING_OPERATOR = + "the {operator} operator used within a component must be preceded by takeUntil"; + + public static FAILURE_STRING_NG_ON_DESTROY = + "component containing subscribe must implement the ngOnDestroy() method"; + + public static FAILURE_STRING_NG_ON_DESTROY_SUBJECT_METHOD_NOT_CALLED = + "there must be an invocation of {destroySubjectName}.{methodName}() in ngOnDestroy()"; + + private operatorsRequiringPrecedingTakeuntil: string[] = [ + "publish", + "publishBehavior", + "publishLast", + "publishReplay", + "shareReplay" + ]; + + public applyWithProgram( + sourceFile: ts.SourceFile, + program: ts.Program + ): Lint.RuleFailure[] { + const failures: Lint.RuleFailure[] = []; + + // find all classes with an @Component() decorator + const componentClassDeclarations = tsquery( + sourceFile, + `ClassDeclaration:has(Decorator[expression.expression.name='Component'])` + ); + componentClassDeclarations.forEach(componentClassDeclaration => { + failures.push( + ...this.checkComponentClassDeclaration( + sourceFile, + program, + componentClassDeclaration as ts.ClassDeclaration + ) + ); + }); + + return failures; + } + + /** + * Checks a component class for occurrences of .subscribe() and corresponding takeUntil() requirements + */ + private checkComponentClassDeclaration( + sourceFile: ts.SourceFile, + program: ts.Program, + componentClassDeclaration: ts.ClassDeclaration + ): Lint.RuleFailure[] { + const failures: Lint.RuleFailure[] = []; + + const typeChecker = program.getTypeChecker(); + /** list of destroy subjects used in takeUntil() operators */ + const destroySubjectNamesUsed: { + [destroySubjectName: string]: boolean; + } = {}; + + // find observable.subscribe() call expressions + const subscribePropertyAccessExpressions = tsquery( + componentClassDeclaration, + `CallExpression > PropertyAccessExpression[name.name="subscribe"]` + ); + + // check whether it is an observable and check the takeUntil before the subscribe + subscribePropertyAccessExpressions.forEach(node => { + const propertyAccessExpression = node as ts.PropertyAccessExpression; + const type = typeChecker.getTypeAtLocation( + propertyAccessExpression.expression + ); + if (couldBeType(type, "Observable")) { + const subscribeFailures = this.checkTakeuntilBeforeSubscribe( + sourceFile, + propertyAccessExpression + ); + failures.push(...subscribeFailures.failures); + if (subscribeFailures.destroySubjectName) { + destroySubjectNamesUsed[subscribeFailures.destroySubjectName] = true; + } + } + }); + + // find observable.pipe() call expressions + const pipePropertyAccessExpressions = tsquery( + componentClassDeclaration, + `CallExpression > PropertyAccessExpression[name.name="pipe"]` + ); + + // check whether it is an observable and check the takeUntil before operators requiring it + pipePropertyAccessExpressions.forEach(node => { + const propertyAccessExpression = node as ts.PropertyAccessExpression; + const pipeCallExpression = node.parent as ts.CallExpression; + const type = typeChecker.getTypeAtLocation( + propertyAccessExpression.expression + ); + if (couldBeType(type, "Observable")) { + const pipeFailures = this.checkTakeuntilBeforeOperatorsInPipe( + sourceFile, + pipeCallExpression.arguments + ); + failures.push(...pipeFailures.failures); + pipeFailures.destroySubjectNames.forEach(destroySubjectName => { + if (destroySubjectName) { + destroySubjectNamesUsed[destroySubjectName] = true; + } + }); + } + }); + + // check the ngOnDestroyMethod + const destroySubjectNamesUsedList = Object.keys(destroySubjectNamesUsed); + if (destroySubjectNamesUsedList.length > 0) { + const ngOnDestroyFailures = this.checkNgOnDestroy( + sourceFile, + componentClassDeclaration as ts.ClassDeclaration, + destroySubjectNamesUsedList + ); + failures.push(...ngOnDestroyFailures); + } + + return failures; + } + + /** + * Checks whether a .subscribe() is preceded by a .pipe(<...>, takeUntil(<...>)) + */ + private checkTakeuntilBeforeSubscribe( + sourceFile: ts.SourceFile, + node: ts.PropertyAccessExpression + ): { failures: Lint.RuleFailure[]; destroySubjectName: string } { + const failures: Lint.RuleFailure[] = []; + const subscribeContext = node.expression; + + /** Whether a takeUntil() operator preceding the .subscribe() was found */ + let lastTakeUntilFound = false; + /** name of the takeUntil() argument */ + let destroySubjectName: string; + + // check whether subscribeContext.expression is .pipe() + if ( + tsutils.isCallExpression(subscribeContext) && + tsutils.isPropertyAccessExpression(subscribeContext.expression) && + subscribeContext.expression.name.getText() === "pipe" + ) { + const pipedOperators = subscribeContext.arguments; + if (pipedOperators.length > 0) { + const lastPipedOperator = pipedOperators[pipedOperators.length - 1]; + // check whether the last operator in the .pipe() call is takeUntil() + if (tsutils.isCallExpression(lastPipedOperator)) { + const lastPipedOperatorFailures = this.checkTakeuntilOperator( + sourceFile, + lastPipedOperator + ); + if (lastPipedOperatorFailures.isTakeUntil) { + lastTakeUntilFound = true; + destroySubjectName = lastPipedOperatorFailures.destroySubjectName; + failures.push(...lastPipedOperatorFailures.failures); + } + } + } + } + + // add failure if there is no takeUntil() in the last position of a .pipe() + if (!lastTakeUntilFound) { + failures.push( + new Lint.RuleFailure( + sourceFile, + node.name.getStart(), + node.name.getStart() + node.name.getWidth(), + Rule.FAILURE_STRING, + this.ruleName + ) + ); + } + + return { failures, destroySubjectName: destroySubjectName }; + } + + /** + * Checks whether there is a takeUntil() operator before operators like shareReplay() + */ + private checkTakeuntilBeforeOperatorsInPipe( + sourceFile: ts.SourceFile, + pipeArguments: ts.NodeArray + ): { failures: Lint.RuleFailure[]; destroySubjectNames: string[] } { + const failures: Lint.RuleFailure[] = []; + const destroySubjectNames: string[] = []; + + // go though all pipe arguments, i.e. rxjs operators + pipeArguments.forEach((pipeArgument, i) => { + // check whether the operator requires a preceding takeuntil + if ( + tsutils.isCallExpression(pipeArgument) && + tsutils.isIdentifier(pipeArgument.expression) && + this.operatorsRequiringPrecedingTakeuntil.includes( + pipeArgument.expression.getText() + ) + ) { + let precedingTakeUntilOperatorFound = false; + // check the preceding operator to be takeuntil + if ( + i > 0 && + pipeArguments[i - 1] && + tsutils.isCallExpression(pipeArguments[i - 1]) + ) { + const precedingOperator = pipeArguments[i - 1] as ts.CallExpression; + const precedingOperatorFailures = this.checkTakeuntilOperator( + sourceFile, + precedingOperator + ); + if (precedingOperatorFailures.isTakeUntil) { + precedingTakeUntilOperatorFound = true; + failures.push(...precedingOperatorFailures.failures); + if (precedingOperatorFailures.destroySubjectName) { + destroySubjectNames.push( + precedingOperatorFailures.destroySubjectName + ); + } + } + } + + if (!precedingTakeUntilOperatorFound) { + failures.push( + new Lint.RuleFailure( + sourceFile, + pipeArgument.getStart(), + pipeArgument.getStart() + pipeArgument.getWidth(), + Rule.FAILURE_STRING_OPERATOR.replace( + "{operator}", + pipeArgument.expression.getText() + ), + this.ruleName + ) + ); + } + } + }); + + return { failures, destroySubjectNames: destroySubjectNames }; + } + + /** + * Checks whether the operator given is takeUntil and uses an allowed destroy subject name + */ + private checkTakeuntilOperator( + sourceFile: ts.SourceFile, + operator: ts.CallExpression + ): { + failures: Lint.RuleFailure[]; + destroySubjectName: string; + isTakeUntil: boolean; + } { + const failures: Lint.RuleFailure[] = []; + let destroySubjectName: string; + let isTakeUntil: boolean = false; + + if ( + tsutils.isIdentifier(operator.expression) && + operator.expression.text === "takeUntil" + ) { + isTakeUntil = true; + // check the argument of takeUntil() + const destroySubjectNameCheck = this.checkDestroySubjectName( + sourceFile, + operator + ); + failures.push(...destroySubjectNameCheck.failures); + destroySubjectName = destroySubjectNameCheck.destroySubjectName; + } + + return { failures, destroySubjectName, isTakeUntil }; + } + + /** + * Checks whether the argument of the given takeUntil(this.destroy$) expression + * is a property of the class + */ + private checkDestroySubjectName( + sourceFile: ts.SourceFile, + takeUntilOperator: ts.CallExpression + ): { failures: Lint.RuleFailure[]; destroySubjectName: string } { + const failures: Lint.RuleFailure[] = []; + + /** name of the takeUntil() argument */ + let destroySubjectName: string; + + /** whether the takeUntil() argument is among the allowed names */ + let isAllowedDestroySubject = false; + + let takeUntilOperatorArgument: ts.PropertyAccessExpression; + let highlightedNode: ts.Expression = takeUntilOperator; + + // check the takeUntil() argument + if ( + takeUntilOperator.arguments.length >= 1 && + takeUntilOperator.arguments[0] + ) { + highlightedNode = takeUntilOperator.arguments[0]; + if (tsutils.isPropertyAccessExpression(takeUntilOperator.arguments[0])) { + takeUntilOperatorArgument = takeUntilOperator + .arguments[0] as ts.PropertyAccessExpression; + destroySubjectName = takeUntilOperatorArgument.name.getText(); + isAllowedDestroySubject = true; + } + } + + if (!isAllowedDestroySubject) { + failures.push( + new Lint.RuleFailure( + sourceFile, + highlightedNode.getStart(), + highlightedNode.getStart() + highlightedNode.getWidth(), + Rule.FAILURE_STRING_SUBJECT_NAME, + this.ruleName + ) + ); + } + + return { failures, destroySubjectName }; + } + + /** + * Checks whether the class implements an ngOnDestroy method and invokes .next() and .complete() on the destroy subjects + */ + private checkNgOnDestroy( + sourceFile: ts.SourceFile, + classDeclaration: ts.ClassDeclaration, + destroySubjectNamesUsed: string[] + ): Lint.RuleFailure[] { + const failures: Lint.RuleFailure[] = []; + const ngOnDestroyMethod = classDeclaration.members.find( + member => member.name && member.name.getText() === "ngOnDestroy" + ); + + // check whether the ngOnDestroy method is implemented + // and contains invocations of .next() and .complete() on all destroy subjects used + if (ngOnDestroyMethod) { + failures.push( + ...this.checkDestroySubjectMethodInvocation( + sourceFile, + ngOnDestroyMethod, + destroySubjectNamesUsed, + "next" + ) + ); + failures.push( + ...this.checkDestroySubjectMethodInvocation( + sourceFile, + ngOnDestroyMethod, + destroySubjectNamesUsed, + "complete" + ) + ); + } else { + failures.push( + new Lint.RuleFailure( + sourceFile, + classDeclaration.name.getStart(), + classDeclaration.name.getStart() + classDeclaration.name.getWidth(), + Rule.FAILURE_STRING_NG_ON_DESTROY, + this.ruleName + ) + ); + } + return failures; + } + + /** + * Checks whether all .() are invoked in the ngOnDestroyMethod + */ + private checkDestroySubjectMethodInvocation( + sourceFile: ts.SourceFile, + ngOnDestroyMethod: ts.ClassElement, + destroySubjectNamesUsed: string[], + methodName: string + ) { + const failures: Lint.RuleFailure[] = []; + const destroySubjectMethodInvocations = tsquery( + ngOnDestroyMethod, + `CallExpression > PropertyAccessExpression[name.name="${methodName}"]` + ) as ts.PropertyAccessExpression[]; + destroySubjectNamesUsed.forEach(destroySubjectName => { + // check whether there is one invocation of .() + if ( + !destroySubjectMethodInvocations.some( + nextInvocation => + tsutils.isPropertyAccessExpression(nextInvocation.expression) && + nextInvocation.expression.name.getText() === destroySubjectName + ) + ) { + failures.push( + new Lint.RuleFailure( + sourceFile, + ngOnDestroyMethod.name.getStart(), + ngOnDestroyMethod.name.getStart() + + ngOnDestroyMethod.name.getWidth(), + Rule.FAILURE_STRING_NG_ON_DESTROY_SUBJECT_METHOD_NOT_CALLED.replace( + "{destroySubjectName}", + `this.${destroySubjectName}` + ).replace("{methodName}", methodName), + this.ruleName + ) + ); + } + }); + return failures; + } +} diff --git a/test/v6/fixtures/prefer-angular-takeuntil-before-subscribe/default/fixture.ts.lint b/test/v6/fixtures/prefer-angular-takeuntil-before-subscribe/default/fixture.ts.lint new file mode 100644 index 00000000..e0a16198 --- /dev/null +++ b/test/v6/fixtures/prefer-angular-takeuntil-before-subscribe/default/fixture.ts.lint @@ -0,0 +1,109 @@ +import { combineLatest, of, Subject } from "rxjs"; +import { switchMap, takeUntil, shareReplay, tap } from "rxjs/operators"; + +const a = of("a"); +const b = of("b"); +const c = of("c"); +const d = of("d"); + +const e = a.pipe(switchMap(_ => b)).subscribe(); + +const f = a.pipe(switchMap(_ => b), takeUntil(d)).subscribe(); + +const g = a.pipe(takeUntil(d), s => switchMap(_ => b)).subscribe(); + +class MyClass { + someMethod() { + const e = a.pipe(switchMap(_ => b)).subscribe(); + + const f = a.pipe(switchMap(_ => b), takeUntil(d)).subscribe(); + + const g = a.pipe(takeUntil(d), s => switchMap(_ => b)).subscribe(); + } +} + +@Component({ + selector: 'app-my' +}) +class MyComponent { + ~~~~~~~~~~~ [enforce-takeuntil-before-subscribe-ondestroy] + + private destroy$: Subject = new Subject(); + + k$ = a.pipe(shareReplay(1)); + ~~~~~~~~~~~~~~ [enforce-takeuntil-before-operator-sharereplay] + + someMethod() { + const d = a.subscribe(); + ~~~~~~~~~ [enforce-takeuntil-before-subscribe] + + const e = a.pipe(switchMap(_ => b)).subscribe(); + ~~~~~~~~~ [enforce-takeuntil-before-subscribe] + + const f = a.pipe(switchMap(_ => b), takeUntil(this.destroy$)).subscribe(); + + const g = a.pipe(takeUntil(this.destroy$), switchMap(_ => b)).subscribe(); + ~~~~~~~~~ [enforce-takeuntil-before-subscribe] + + const h = a.pipe(switchMap(_ => b), takeUntil(d)).subscribe(); + ~ [enforce-takeuntil-before-subscribe-subject-name] + + const k1 = a.pipe(takeUntil(this.destroy$), shareReplay(1)).subscribe(); + ~~~~~~~~~ [enforce-takeuntil-before-subscribe] + + const k = a.pipe(shareReplay(1), takeUntil(this.destroy$)).subscribe(); + ~~~~~~~~~~~~~~ [enforce-takeuntil-before-operator-sharereplay] + + const m = a.pipe(tap(), shareReplay(1), takeUntil(this.destroy$)).subscribe(); + ~~~~~~~~~~~~~~ [enforce-takeuntil-before-operator-sharereplay] + + const n = a.pipe(takeUntil(d), shareReplay(1), takeUntil(this.destroy$)).subscribe(); + ~ [enforce-takeuntil-before-subscribe-subject-name] + + } +} + +@Component({ + selector: 'app-my' +}) +class MyComponent implements OnDestroy { + someMethod() { + const f = a.pipe(switchMap(_ => b), takeUntil(this._destroy$)).subscribe(); + } + + ngOnDestroy() { + ~~~~~~~~~~~ [enforce-takeuntil-before-subscribe-next-missing] + ~~~~~~~~~~~ [enforce-takeuntil-before-subscribe-complete-missing] + // this._destroy$.next() is missing + this.destroy$.next(); + this.destroy$.complete(); + } +} + +@Component({ + selector: 'app-my' +}) +class MyComponent implements SomeInterface, OnDestroy { + private destroy$: Subject = new Subject(); + + k$ = a.pipe(takeUntil(this.destroy$), shareReplay(1)); + + someMethod() { + const e = a.pipe(switchMap(_ => b), takeUntil(this.destroy$)).subscribe(); + + const k = a.pipe(takeUntil(this.destroy$), shareReplay(1), takeUntil(this.destroy$)).subscribe(); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } +} + + +[enforce-takeuntil-before-subscribe]: subscribe within a component must be preceded by takeUntil +[enforce-takeuntil-before-subscribe-subject-name]: takeUntil argument must be a property of the class, e.g. takeUntil(this.destroy$) +[enforce-takeuntil-before-subscribe-ondestroy]: component containing subscribe must implement the ngOnDestroy() method +[enforce-takeuntil-before-subscribe-next-missing]: there must be an invocation of this._destroy$.next() in ngOnDestroy() +[enforce-takeuntil-before-subscribe-complete-missing]: there must be an invocation of this._destroy$.complete() in ngOnDestroy() +[enforce-takeuntil-before-operator-sharereplay]: the shareReplay operator used within a component must be preceded by takeUntil diff --git a/test/v6/fixtures/prefer-angular-takeuntil-before-subscribe/default/tsconfig.json b/test/v6/fixtures/prefer-angular-takeuntil-before-subscribe/default/tsconfig.json new file mode 100644 index 00000000..690be78e --- /dev/null +++ b/test/v6/fixtures/prefer-angular-takeuntil-before-subscribe/default/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "lib": ["es2015"], + "noEmit": true, + "paths": { + "rxjs": ["../../node_modules/rxjs"] + }, + "skipLibCheck": true, + "target": "es5" + }, + "include": ["fixture.ts"] +} diff --git a/test/v6/fixtures/prefer-angular-takeuntil-before-subscribe/default/tslint.json b/test/v6/fixtures/prefer-angular-takeuntil-before-subscribe/default/tslint.json new file mode 100644 index 00000000..a2160ab8 --- /dev/null +++ b/test/v6/fixtures/prefer-angular-takeuntil-before-subscribe/default/tslint.json @@ -0,0 +1,8 @@ +{ + "defaultSeverity": "error", + "jsRules": {}, + "rules": { + "rxjs-prefer-angular-takeuntil-before-subscribe": { "severity": "error" } + }, + "rulesDirectory": "../../../../../build/rules" +}