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"
+}