Skip to content

Commit 7b54829

Browse files
committed
fix: allow prefer-takeuntil and prefer-composition rules to support private JS properties
1 parent 55883b0 commit 7b54829

File tree

6 files changed

+501
-6
lines changed

6 files changed

+501
-6
lines changed

.husky/pre-commit

100644100755
File mode changed.

source/rules/prefer-composition.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
isMemberExpression,
1515
isVariableDeclarator,
1616
} from "eslint-etc";
17-
import { ruleCreator } from "../utils";
17+
import { ruleCreator, isPrivateIdentifier } from "../utils";
1818

1919
const defaultOptions: readonly {
2020
checkDecorators?: string[];
@@ -145,7 +145,11 @@ const rule = ruleCreator({
145145
const { callee } = callExpression;
146146
if (isMemberExpression(callee)) {
147147
const { object } = callee;
148-
if (isMemberExpression(object) && isIdentifier(object.property)) {
148+
if (
149+
isMemberExpression(object) &&
150+
(isIdentifier(object.property) ||
151+
isPrivateIdentifier(object.property))
152+
) {
149153
return object.property.name;
150154
}
151155
if (isIdentifier(object)) {
@@ -186,6 +190,7 @@ const rule = ruleCreator({
186190
// subscription or if it's assigned to a variable that is added to a
187191
// subscription.
188192
const { addCallExpressions, subscriptions } = entry;
193+
189194
const parent = getParent(callExpression);
190195
if (!parent) {
191196
return false;
@@ -208,6 +213,7 @@ const rule = ruleCreator({
208213
subscriptions.add(name);
209214
return true;
210215
}
216+
211217
if (isVariableDeclarator(parent) && isIdentifier(parent.id)) {
212218
return isVariableComposed(parent.id, entry);
213219
}

source/rules/prefer-takeuntil.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
isMemberExpression,
1616
isThisExpression,
1717
} from "eslint-etc";
18-
import { ruleCreator } from "../utils";
18+
import { ruleCreator, isPrivateIdentifier } from "../utils";
1919

2020
const messages = {
2121
noDestroy: "`ngOnDestroy` is not implemented.",
@@ -207,7 +207,7 @@ const rule = ruleCreator({
207207
if (
208208
isMemberExpression(arg) &&
209209
isThisExpression(arg.object) &&
210-
isIdentifier(arg.property)
210+
(isIdentifier(arg.property) || isPrivateIdentifier(arg.property))
211211
) {
212212
return { found: true, name: arg.property.name };
213213
} else if (arg && isIdentifier(arg)) {
@@ -233,7 +233,8 @@ const rule = ruleCreator({
233233
(isMemberExpression(callee) &&
234234
isMemberExpression(callee.object) &&
235235
isThisExpression(callee.object.object) &&
236-
isIdentifier(callee.object.property) &&
236+
(isIdentifier(callee.object.property) ||
237+
isPrivateIdentifier(callee.object.property)) &&
237238
callee.object.property.name === name)
238239
);
239240
return Boolean(callExpression);

source/utils.ts

+8
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,16 @@
44
*/
55

66
import { ESLintUtils } from "@typescript-eslint/experimental-utils";
7+
import { TSESTree as es } from "@typescript-eslint/experimental-utils";
78

89
export const ruleCreator = ESLintUtils.RuleCreator(
910
(name) =>
1011
`https://github.com/cartant/eslint-plugin-rxjs-angular/tree/main/docs/rules/${name}.md`
1112
);
13+
14+
// TODO: Remove when eslint-etc supports isPrivateIdentifier
15+
export function isPrivateIdentifier(
16+
node: es.Node
17+
): node is es.PrivateIdentifier {
18+
return node.type === "PrivateIdentifier";
19+
}

tests/rules/prefer-composition.ts

+90-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,28 @@ ruleTester({ types: true }).run("prefer-composition", rule, {
3232
}
3333
`,
3434
},
35+
{
36+
code: stripIndent`
37+
// composed component with private JavaScript property
38+
import { Component, OnDestroy, OnInit } from "@angular/core";
39+
import { of, Subscription } from "rxjs";
40+
41+
@Component({
42+
selector: "composed-component",
43+
template: "<span>{{ value }}</span>"
44+
})
45+
export class ComposedComponent implements OnInit, OnDestroy {
46+
value: string;
47+
#subscription = new Subscription();
48+
ngOnInit() {
49+
this.#subscription.add(of("foo").subscribe(value => this.value = value));
50+
}
51+
ngOnDestroy() {
52+
this.#subscription.unsubscribe();
53+
}
54+
}
55+
`,
56+
},
3557
{
3658
code: stripIndent`
3759
// variable composed component
@@ -47,7 +69,7 @@ ruleTester({ types: true }).run("prefer-composition", rule, {
4769
private subscription = new Subscription();
4870
ngOnInit() {
4971
let subscription = of("foo").subscribe(value => this.value = value);
50-
this.subscription.add(subscription);1
72+
this.subscription.add(subscription);
5173
subscription = of("bar").subscribe(value => this.value = value);
5274
this.subscription.add(subscription);
5375
}
@@ -57,6 +79,31 @@ ruleTester({ types: true }).run("prefer-composition", rule, {
5779
}
5880
`,
5981
},
82+
{
83+
code: stripIndent`
84+
// variable composed component with private JavaScript property
85+
import { Component, OnDestroy, OnInit } from "@angular/core";
86+
import { of, Subscription } from "rxjs";
87+
88+
@Component({
89+
selector: "variable-composed-component",
90+
template: "<span>{{ value }}</span>"
91+
})
92+
export class VariableComposedComponent implements OnInit, OnDestroy {
93+
value: string;
94+
#subscription = new Subscription();
95+
ngOnInit() {
96+
let subscription = of("foo").subscribe(value => this.value = value);
97+
this.#subscription.add(subscription);
98+
subscription = of("bar").subscribe(value => this.value = value);
99+
this.#subscription.add(subscription);
100+
}
101+
ngOnDestroy() {
102+
this.#subscription.unsubscribe();
103+
}
104+
}
105+
`,
106+
},
60107
{
61108
code: stripIndent`
62109
// destructured composed component
@@ -145,6 +192,28 @@ ruleTester({ types: true }).run("prefer-composition", rule, {
145192
}
146193
`
147194
),
195+
fromFixture(
196+
stripIndent`
197+
// not unsubscribed component with private JavaScript property
198+
import { Component, OnDestroy, OnInit } from "@angular/core";
199+
import { of, Subscription } from "rxjs";
200+
201+
@Component({
202+
selector: "not-unsubscribed-component",
203+
template: "<span>{{ value }}</span>"
204+
})
205+
export class NotUnsubscribedComponent implements OnInit, OnDestroy {
206+
value: string;
207+
#subscription = new Subscription();
208+
~~~~~~~~~~~~~ [notUnsubscribed]
209+
ngOnInit() {
210+
this.#subscription.add(of("foo").subscribe(value => this.value = value));
211+
}
212+
ngOnDestroy() {
213+
}
214+
}
215+
`
216+
),
148217
fromFixture(
149218
stripIndent`
150219
// not destroyed component
@@ -165,6 +234,26 @@ ruleTester({ types: true }).run("prefer-composition", rule, {
165234
}
166235
`
167236
),
237+
fromFixture(
238+
stripIndent`
239+
// not destroyed component with Private JavaScript property
240+
import { Component, OnDestroy, OnInit } from "@angular/core";
241+
import { of, Subscription } from "rxjs";
242+
243+
@Component({
244+
selector: "not-destroyed-component",
245+
template: "<span>{{ value }}</span>"
246+
})
247+
export class NotDestroyedComponent implements OnInit {
248+
~~~~~~~~~~~~~~~~~~~~~ [notImplemented]
249+
value: string;
250+
#subscription = new Subscription();
251+
ngOnInit() {
252+
this.#subscription.add(of("foo").subscribe(value => this.value = value));
253+
}
254+
}
255+
`
256+
),
168257
fromFixture(
169258
stripIndent`
170259
// not declared

0 commit comments

Comments
 (0)