Skip to content

Commit 1860831

Browse files
Samuel-Therrien-BeslogicAvasam
authored andcommitted
Added superClass option. Fixes #1
1 parent f0e0f75 commit 1860831

File tree

7 files changed

+142
-6
lines changed

7 files changed

+142
-6
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ yarn-error.log
33
/dist
44
/node_modules
55
/temp
6+
package-lock.json

docs/rules/prefer-composition.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@ export class SomeComponent implements OnInit, OnDestroy {
4848

4949
## Options
5050

51-
This rule accepts a single option which is an object with a `checkDecorators` property which is an array containing the names of the decorators that determine whether or not a class is checked. By default, `checkDecorators` is `["Component"]`.
51+
This rule accepts a single option which is an object with a `checkDecorators` and `superClass` properties
52+
53+
The `checkDecorators` property is an array containing the names of the decorators that determine whether or not a class is checked. By default, `checkDecorators` is `["Component"]`.
54+
55+
The `superClass` property is an array containing the names of classes to extend from that already implements a `Subject`-based `ngOnDestroy`.
5256

5357
```json
5458
{
@@ -61,4 +65,4 @@ This rule accepts a single option which is an object with a `checkDecorators` pr
6165

6266
## Further reading
6367

64-
- [Composing Subscriptions](https://ncjamieson.com/composing-subscriptions/)
68+
- [Composing Subscriptions](https://ncjamieson.com/composing-subscriptions/)

docs/rules/prefer-takeuntil.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,16 @@ class SomeComponent implements OnDestroy, OnInit {
5050

5151
## Options
5252

53-
This rule accepts a single option which is an object with `checkComplete`, `checkDecorators`, `checkDestroy` and `alias` properties.
53+
This rule accepts a single option which is an object with `checkComplete`, `checkDecorators`, `checkDestroy`, `superClass` and `alias` properties.
5454

5555
The `checkComplete` property is a boolean that determines whether or not `complete` must be called after `next` and the `checkDestroy` property is a boolean that determines whether or not a `Subject`-based `ngOnDestroy` must be implemented.
5656

5757
The `checkDecorators` property is an array containing the names of the decorators that determine whether or not a class is checked. By default, `checkDecorators` is `["Component"]`.
5858

59+
The `checkDestroy` property is a boolean that determines whether or not a `Subject`-based `ngOnDestroy` must be implemented.
60+
61+
The `superClass` property is an array containing the names of classes to extend from that already implements a `Subject`-based `ngOnDestroy`.
62+
5963
The `alias` property is an array of names of operators that should be treated similarly to `takeUntil`.
6064

6165
```json
@@ -66,12 +70,13 @@ The `alias` property is an array of names of operators that should be treated si
6670
"alias": ["untilDestroyed"],
6771
"checkComplete": true,
6872
"checkDecorators": ["Component"],
69-
"checkDestroy": true
73+
"checkDestroy": true,
74+
"superClass": []
7075
}
7176
]
7277
}
7378
```
7479

7580
## Further reading
7681

77-
- [Avoiding takeUntil leaks](https://ncjamieson.com/avoiding-takeuntil-leaks/)
82+
- [Avoiding takeUntil leaks](https://ncjamieson.com/avoiding-takeuntil-leaks/)

source/rules/prefer-composition.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { ruleCreator } from "../utils";
1818

1919
const defaultOptions: readonly {
2020
checkDecorators?: string[];
21+
superClass?: string[];
2122
}[] = [];
2223

2324
const rule = ruleCreator({
@@ -40,11 +41,13 @@ const rule = ruleCreator({
4041
{
4142
properties: {
4243
checkDecorators: { type: "array", items: { type: "string" } },
44+
superClass: { type: "array", items: { type: "string" } },
4345
},
4446
type: "object",
4547
description: stripIndent`
4648
An optional object with an optional \`checkDecorators\` property.
4749
The \`checkDecorators\` property is an array containing the names of the decorators that determine whether or not a class is checked.
50+
The \`superClass\` property is an array containing the names of classes to extend from that already implements a \`Subject\`-based \`ngOnDestroy\`.
4851
`,
4952
},
5053
],
@@ -53,14 +56,16 @@ const rule = ruleCreator({
5356
name: "prefer-composition",
5457
create: (context, unused: typeof defaultOptions) => {
5558
const { couldBeObservable, couldBeSubscription } = getTypeServices(context);
56-
const [{ checkDecorators = ["Component"] } = {}] = context.options;
59+
const [{ checkDecorators = ["Component"], superClass = [] } = {}] =
60+
context.options;
5761

5862
type Entry = {
5963
addCallExpressions: es.CallExpression[];
6064
classDeclaration: es.ClassDeclaration;
6165
propertyDefinitions: es.PropertyDefinition[];
6266
hasDecorator: boolean;
6367
ngOnDestroyDefinition?: es.MethodDefinition;
68+
extendsSuperClassDeclaration?: es.ClassDeclaration;
6469
subscribeCallExpressions: es.CallExpression[];
6570
subscriptions: Set<string>;
6671
unsubscribeCallExpressions: es.CallExpression[];
@@ -72,6 +77,7 @@ const rule = ruleCreator({
7277
classDeclaration,
7378
propertyDefinitions,
7479
ngOnDestroyDefinition,
80+
extendsSuperClassDeclaration,
7581
subscribeCallExpressions,
7682
subscriptions,
7783
unsubscribeCallExpressions,
@@ -83,6 +89,9 @@ const rule = ruleCreator({
8389
subscribeCallExpressions.forEach((callExpression) => {
8490
const { callee } = callExpression;
8591
if (isMemberExpression(callee)) {
92+
if (extendsSuperClassDeclaration) {
93+
return;
94+
}
8695
const { object, property } = callee;
8796
if (!couldBeObservable(object)) {
8897
return;
@@ -98,6 +107,9 @@ const rule = ruleCreator({
98107
});
99108

100109
if (!ngOnDestroyDefinition) {
110+
if (extendsSuperClassDeclaration) {
111+
return;
112+
}
101113
context.report({
102114
messageId: "notImplemented",
103115
node: classDeclaration.id ?? classDeclaration,
@@ -240,6 +252,20 @@ const rule = ruleCreator({
240252
return true;
241253
}
242254

255+
const extendsSuperClassDeclaration =
256+
superClass.length === 0
257+
? {}
258+
: {
259+
[`ClassDeclaration:matches(${superClass
260+
.map((className) => `[superClass.name="${className}"]`)
261+
.join()})`]: (node: es.ClassDeclaration) => {
262+
const entry = getEntry();
263+
if (entry && entry.hasDecorator) {
264+
entry.extendsSuperClassDeclaration = node;
265+
}
266+
},
267+
};
268+
243269
return {
244270
"CallExpression[callee.property.name='add']": (
245271
node: es.CallExpression
@@ -280,6 +306,7 @@ const rule = ruleCreator({
280306
entry.propertyDefinitions.push(node);
281307
}
282308
},
309+
...extendsSuperClassDeclaration,
283310
"MethodDefinition[key.name='ngOnDestroy'][kind='method']": (
284311
node: es.MethodDefinition
285312
) => {

source/rules/prefer-takeuntil.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const defaultOptions: readonly {
3131
checkComplete?: boolean;
3232
checkDecorators?: string[];
3333
checkDestroy?: boolean;
34+
superClass?: string[];
3435
}[] = [];
3536

3637
const rule = ruleCreator({
@@ -51,6 +52,7 @@ const rule = ruleCreator({
5152
checkComplete: { type: "boolean" },
5253
checkDecorators: { type: "array", items: { type: "string" } },
5354
checkDestroy: { type: "boolean" },
55+
superClass: { type: "array", items: { type: "string" } },
5456
},
5557
type: "object",
5658
description: stripIndent`
@@ -59,6 +61,7 @@ const rule = ruleCreator({
5961
The \`checkComplete\` property is a boolean that determines whether or not \`complete\` must be called after \`next\`.
6062
The \`checkDecorators\` property is an array containing the names of the decorators that determine whether or not a class is checked.
6163
The \`checkDestroy\` property is a boolean that determines whether or not a \`Subject\`-based \`ngOnDestroy\` must be implemented.
64+
The \`superClass\` property is an array containing the names of classes to extend from that already implements a \`Subject\`-based \`ngOnDestroy\`.
6265
`,
6366
},
6467
],
@@ -78,6 +81,7 @@ const rule = ruleCreator({
7881
checkComplete = false,
7982
checkDecorators = ["Component"],
8083
checkDestroy = alias.length === 0,
84+
superClass = [],
8185
} = config;
8286

8387
type Entry = {
@@ -87,6 +91,7 @@ const rule = ruleCreator({
8791
hasDecorator: boolean;
8892
nextCallExpressions: es.CallExpression[];
8993
ngOnDestroyDefinition?: es.MethodDefinition;
94+
extendsSuperClassDeclaration?: es.ClassDeclaration;
9095
subscribeCallExpressions: es.CallExpression[];
9196
subscribeCallExpressionsToNames: Map<es.CallExpression, Set<string>>;
9297
};
@@ -117,13 +122,17 @@ const rule = ruleCreator({
117122
completeCallExpressions,
118123
nextCallExpressions,
119124
ngOnDestroyDefinition,
125+
extendsSuperClassDeclaration,
120126
subscribeCallExpressionsToNames,
121127
} = entry;
122128
if (subscribeCallExpressionsToNames.size === 0) {
123129
return;
124130
}
125131

126132
if (!ngOnDestroyDefinition) {
133+
if (extendsSuperClassDeclaration) {
134+
return;
135+
}
127136
context.report({
128137
messageId: "noDestroy",
129138
node: classDeclaration.id ?? classDeclaration,
@@ -308,6 +317,20 @@ const rule = ruleCreator({
308317
);
309318
}
310319

320+
const extendsSuperClassDeclaration =
321+
superClass.length === 0
322+
? {}
323+
: {
324+
[`ClassDeclaration:matches(${superClass
325+
.map((className) => `[superClass.name="${className}"]`)
326+
.join()})`]: (node: es.ClassDeclaration) => {
327+
const entry = getEntry();
328+
if (entry && entry.hasDecorator) {
329+
entry.extendsSuperClassDeclaration = node;
330+
}
331+
},
332+
};
333+
311334
return {
312335
"CallExpression[callee.property.name='subscribe']": (
313336
node: es.CallExpression
@@ -352,6 +375,7 @@ const rule = ruleCreator({
352375
entry.ngOnDestroyDefinition = node;
353376
}
354377
},
378+
...extendsSuperClassDeclaration,
355379
"MethodDefinition[key.name='ngOnDestroy'][kind='method'] CallExpression[callee.property.name='next']":
356380
(node: es.CallExpression) => {
357381
const entry = getEntry();

tests/rules/prefer-composition.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,43 @@ ruleTester({ types: true }).run("prefer-composition", rule, {
9898
}
9999
`,
100100
},
101+
{
102+
code: stripIndent`
103+
// 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";
107+
108+
const o = of("o");
109+
110+
@Directive()
111+
abstract class BaseComponent implements OnDestroy {
112+
private readonly destroySubject = new Subject<void>();
113+
protected readonly destroy = this.destroy.asObservable();
114+
ngOnDestroy() {
115+
this.destroy.next();
116+
this.destroy.complete();
117+
}
118+
}
119+
120+
@Component({
121+
selector: "component-with-super-class"
122+
})
123+
class CorrectComponent extends BaseComponent {
124+
someMethod() {
125+
o.pipe(
126+
switchMap(_ => o),
127+
takeUntil(this.destroy)
128+
).subscribe();
129+
}
130+
}
131+
`,
132+
options: [
133+
{
134+
superClass: ["BaseComponent"],
135+
},
136+
],
137+
},
101138
],
102139
invalid: [
103140
fromFixture(

tests/rules/prefer-takeuntil.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,44 @@ ruleTester({ types: true }).run("prefer-takeuntil", rule, {
369369
},
370370
],
371371
},
372+
{
373+
code: stripIndent`
374+
// 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";
378+
379+
const o = of("o");
380+
381+
@Directive()
382+
abstract class BaseComponent implements OnDestroy {
383+
private readonly destroySubject = new Subject<void>();
384+
protected readonly destroy = this.destroy.asObservable();
385+
386+
ngOnDestroy() {
387+
this.destroy.next();
388+
this.destroy.complete();
389+
}
390+
}
391+
392+
@Component({
393+
selector: "component-with-super-class"
394+
})
395+
class CorrectComponent extends BaseComponent {
396+
someMethod() {
397+
o.pipe(
398+
switchMap(_ => o),
399+
takeUntil(this.destroy)
400+
).subscribe();
401+
}
402+
}
403+
`,
404+
options: [
405+
{
406+
superClass: ["BaseComponent"],
407+
},
408+
],
409+
},
372410
],
373411
invalid: [
374412
fromFixture(

0 commit comments

Comments
 (0)