Skip to content

Commit b196fb9

Browse files
Allow super.ngOnDestroy() calls to base class
1 parent 18e39ed commit b196fb9

File tree

3 files changed

+191
-37
lines changed

3 files changed

+191
-37
lines changed

source/rules/prefer-takeuntil.ts

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ const messages = {
2626
} as const;
2727
type MessageIds = keyof typeof messages;
2828

29+
const ngOnDestroyMethodSelector =
30+
"MethodDefinition[key.name='ngOnDestroy'][kind='method']";
31+
2932
const defaultOptions: readonly {
3033
alias?: string[];
3134
checkComplete?: boolean;
@@ -92,6 +95,7 @@ const rule = ruleCreator({
9295
nextCallExpressions: es.CallExpression[];
9396
ngOnDestroyDefinition?: es.MethodDefinition;
9497
extendsSuperClassDeclaration?: es.ClassDeclaration;
98+
superNgOnDestroyCallExpression?: es.CallExpression;
9599
subscribeCallExpressions: es.CallExpression[];
96100
subscribeCallExpressionsToNames: Map<es.CallExpression, Set<string>>;
97101
};
@@ -123,6 +127,7 @@ const rule = ruleCreator({
123127
nextCallExpressions,
124128
ngOnDestroyDefinition,
125129
extendsSuperClassDeclaration,
130+
superNgOnDestroyCallExpression,
126131
subscribeCallExpressionsToNames,
127132
} = entry;
128133
if (subscribeCallExpressionsToNames.size === 0) {
@@ -163,26 +168,39 @@ const rule = ruleCreator({
163168
};
164169
namesToChecks.set(name, check);
165170

166-
if (!checkSubjectProperty(name, entry)) {
167-
check.descriptors.push({
168-
data: { name },
169-
messageId: "notDeclared",
170-
node: classDeclaration.id ?? classDeclaration,
171-
});
172-
}
173-
if (!checkSubjectCall(name, nextCallExpressions)) {
174-
check.descriptors.push({
175-
data: { method: "next", name },
176-
messageId: "notCalled",
177-
node: ngOnDestroyDefinition.key,
178-
});
179-
}
180-
if (checkComplete && !checkSubjectCall(name, completeCallExpressions)) {
181-
check.descriptors.push({
182-
data: { method: "complete", name },
183-
messageId: "notCalled",
184-
node: ngOnDestroyDefinition.key,
185-
});
171+
if (extendsSuperClassDeclaration) {
172+
if (!superNgOnDestroyCallExpression) {
173+
check.descriptors.push({
174+
data: { method: "ngOnDestroy", name: "super" },
175+
messageId: "notCalled",
176+
node: ngOnDestroyDefinition.key,
177+
});
178+
}
179+
} else {
180+
if (!checkSubjectProperty(name, entry)) {
181+
check.descriptors.push({
182+
data: { name },
183+
messageId: "notDeclared",
184+
node: classDeclaration.id ?? classDeclaration,
185+
});
186+
}
187+
if (!checkSubjectCall(name, nextCallExpressions)) {
188+
check.descriptors.push({
189+
data: { method: "next", name },
190+
messageId: "notCalled",
191+
node: ngOnDestroyDefinition.key,
192+
});
193+
}
194+
if (
195+
checkComplete &&
196+
!checkSubjectCall(name, completeCallExpressions)
197+
) {
198+
check.descriptors.push({
199+
data: { method: "complete", name },
200+
messageId: "notCalled",
201+
node: ngOnDestroyDefinition.key,
202+
});
203+
}
186204
}
187205
});
188206

@@ -367,23 +385,28 @@ const rule = ruleCreator({
367385
entry.propertyDefinitions.push(node);
368386
}
369387
},
370-
"MethodDefinition[key.name='ngOnDestroy'][kind='method']": (
371-
node: es.MethodDefinition
372-
) => {
388+
[ngOnDestroyMethodSelector]: (node: es.MethodDefinition) => {
373389
const entry = getEntry();
374390
if (entry && entry.hasDecorator) {
375391
entry.ngOnDestroyDefinition = node;
376392
}
377393
},
378394
...extendsSuperClassDeclaration,
379-
"MethodDefinition[key.name='ngOnDestroy'][kind='method'] CallExpression[callee.property.name='next']":
395+
[`${ngOnDestroyMethodSelector} CallExpression[callee.object.type='Super'][callee.property.name='ngOnDestroy']`]:
396+
(node: es.CallExpression) => {
397+
const entry = getEntry();
398+
if (entry && entry.hasDecorator) {
399+
entry.superNgOnDestroyCallExpression = node;
400+
}
401+
},
402+
[`${ngOnDestroyMethodSelector} CallExpression[callee.property.name='next']`]:
380403
(node: es.CallExpression) => {
381404
const entry = getEntry();
382405
if (entry && entry.hasDecorator) {
383406
entry.nextCallExpressions.push(node);
384407
}
385408
},
386-
"MethodDefinition[key.name='ngOnDestroy'][kind='method'] CallExpression[callee.property.name='complete']":
409+
[`${ngOnDestroyMethodSelector} CallExpression[callee.property.name='complete']`]:
387410
(node: es.CallExpression) => {
388411
const entry = getEntry();
389412
if (entry && entry.hasDecorator) {

tests/rules/prefer-composition.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,20 +100,21 @@ ruleTester({ types: true }).run("prefer-composition", rule, {
100100
},
101101
{
102102
code: stripIndent`
103+
// extends superClass
103104
// https://github.com/cartant/eslint-plugin-rxjs-angular/issues/1
104-
import { Component } from "@angular/core";
105-
import { of } from "rxjs";
106-
import { switchMap, take } from "rxjs/operators";
105+
import { Component, Directive, OnDestroy } from "@angular/core";
106+
import { of, Subject } from "rxjs";
107+
import { switchMap, takeUntil } from "rxjs/operators";
107108
108109
const o = of("o");
109110
110111
@Directive()
111112
abstract class BaseComponent implements OnDestroy {
112113
private readonly destroySubject = new Subject<void>();
113-
protected readonly destroy = this.destroy.asObservable();
114+
protected readonly destroy = this.destroySubject.asObservable();
114115
ngOnDestroy() {
115-
this.destroy.next();
116-
this.destroy.complete();
116+
this.destroySubject.next();
117+
this.destroySubject.complete();
117118
}
118119
}
119120

tests/rules/prefer-takeuntil.ts

Lines changed: 136 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -371,21 +371,22 @@ ruleTester({ types: true }).run("prefer-takeuntil", rule, {
371371
},
372372
{
373373
code: stripIndent`
374+
// extends superClass
374375
// https://github.com/cartant/eslint-plugin-rxjs-angular/issues/1
375-
import { Component } from "@angular/core";
376-
import { of } from "rxjs";
377-
import { switchMap, take } from "rxjs/operators";
376+
import { Component, Directive, OnDestroy } from "@angular/core";
377+
import { of, Subject } from "rxjs";
378+
import { switchMap, takeUntil } from "rxjs/operators";
378379
379380
const o = of("o");
380381
381382
@Directive()
382383
abstract class BaseComponent implements OnDestroy {
383384
private readonly destroySubject = new Subject<void>();
384-
protected readonly destroy = this.destroy.asObservable();
385+
protected readonly destroy = this.destroySubject.asObservable();
385386
386387
ngOnDestroy() {
387-
this.destroy.next();
388-
this.destroy.complete();
388+
this.destroySubject.next();
389+
this.destroySubject.complete();
389390
}
390391
}
391392
@@ -404,6 +405,51 @@ ruleTester({ types: true }).run("prefer-takeuntil", rule, {
404405
options: [
405406
{
406407
superClass: ["BaseComponent"],
408+
checkComplete: true,
409+
},
410+
],
411+
},
412+
{
413+
code: stripIndent`
414+
// extends superClass and implements OnDestroy
415+
// https://github.com/cartant/eslint-plugin-rxjs-angular/issues/1
416+
import { Component, Directive, OnDestroy } from "@angular/core";
417+
import { of, Subject } from "rxjs";
418+
import { switchMap, takeUntil } from "rxjs/operators";
419+
420+
const o = of("o");
421+
422+
@Directive()
423+
abstract class BaseComponent implements OnDestroy {
424+
private readonly destroySubject = new Subject<void>();
425+
protected readonly destroy = this.destroySubject.asObservable();
426+
427+
ngOnDestroy() {
428+
this.destroySubject.next();
429+
this.destroySubject.complete();
430+
}
431+
}
432+
433+
@Component({
434+
selector: "component-with-super-class-and-destroy"
435+
})
436+
class CorrectDestroyComponent extends BaseComponent implements OnDestroy {
437+
someMethod() {
438+
o.pipe(
439+
switchMap(_ => o),
440+
takeUntil(this.destroy)
441+
).subscribe();
442+
}
443+
444+
override ngOnDestroy(): void {
445+
super.ngOnDestroy()
446+
}
447+
}
448+
`,
449+
options: [
450+
{
451+
superClass: ["BaseComponent"],
452+
checkComplete: true,
407453
},
408454
],
409455
},
@@ -695,5 +741,89 @@ ruleTester({ types: true }).run("prefer-takeuntil", rule, {
695741
],
696742
}
697743
),
744+
fromFixture(
745+
stripIndent`
746+
// extends superClass and implements OnDestroy, missing super.ngOnDestroy()
747+
// https://github.com/cartant/eslint-plugin-rxjs-angular/issues/1
748+
import { Component, Directive, OnDestroy } from "@angular/core";
749+
import { of, Subject } from "rxjs";
750+
import { switchMap, takeUntil } from "rxjs/operators";
751+
752+
const o = of("o");
753+
754+
@Directive()
755+
abstract class BaseComponent implements OnDestroy {
756+
private readonly destroySubject = new Subject<void>();
757+
protected readonly destroy = this.destroySubject.asObservable();
758+
759+
ngOnDestroy() {
760+
this.destroySubject.next();
761+
this.destroySubject.complete();
762+
}
763+
}
764+
765+
@Component({
766+
selector: "missing-super-call"
767+
})
768+
class MissingSuperCallComponent extends BaseComponent implements OnDestroy {
769+
someMethod() {
770+
o.pipe(
771+
switchMap(_ => o),
772+
takeUntil(this.destroy)
773+
).subscribe();
774+
}
775+
776+
override ngOnDestroy(): void {
777+
~~~~~~~~~~~ [notCalled { "method": "ngOnDestroy", "name": "super" }]
778+
}
779+
}
780+
`,
781+
{
782+
options: [
783+
{
784+
superClass: ["BaseComponent"],
785+
checkComplete: true,
786+
},
787+
],
788+
}
789+
),
790+
fromFixture(
791+
stripIndent`
792+
// Calls super.ngOnDestroy() w/o extending base class
793+
// https://github.com/cartant/eslint-plugin-rxjs-angular/issues/1
794+
import { Component, OnDestroy } from "@angular/core";
795+
import { of } from "rxjs";
796+
import { switchMap, takeUntil } from "rxjs/operators";
797+
798+
const o = of("o");
799+
800+
@Component({
801+
selector: "missing-base"
802+
})
803+
class MissingBaseComponent extends SomeClass implements OnDestroy {
804+
~~~~~~~~~~~~~~~~~~~~ [notDeclared { "name": "destroy" }]
805+
someMethod() {
806+
o.pipe(
807+
switchMap(_ => o),
808+
takeUntil(this.destroy)
809+
).subscribe();
810+
}
811+
812+
override ngOnDestroy(): void {
813+
~~~~~~~~~~~ [notCalled { "method": "next", "name": "destroy" }]
814+
~~~~~~~~~~~ [notCalled { "method": "complete", "name": "destroy" }]
815+
super.ngOnDestroy()
816+
}
817+
}
818+
`,
819+
{
820+
options: [
821+
{
822+
superClass: ["BaseComponent"],
823+
checkComplete: true,
824+
},
825+
],
826+
}
827+
),
698828
],
699829
});

0 commit comments

Comments
 (0)