Skip to content

Commit a4224e7

Browse files
committedFeb 20, 2020
feat: release version 1.0.0 of @angular-extensions/lint-rules
0 parents  commit a4224e7

22 files changed

+4952
-0
lines changed
 

Diff for: ‎.circleci/config.yml

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
version: 2.1
2+
jobs:
3+
build:
4+
docker:
5+
- image: circleci/node:12
6+
steps:
7+
- checkout
8+
# Download and cache dependencies
9+
- restore_cache:
10+
keys:
11+
- v1-dependencies-{{ checksum "package-lock.json" }}
12+
# fallback to using the latest cache if no exact match is found
13+
- v1-dependencies-
14+
- run: npm install
15+
- save_cache:
16+
paths:
17+
- node_modules
18+
key: v1-dependencies-{{ checksum "package-lock.json" }}
19+
- run: npm run test

Diff for: ‎.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/node_modules
2+
/.idea
3+
/*.iml
4+
/dist

Diff for: ‎README.md

+175
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# @angular-extensions/lint-rules
2+
[https://github.com/angular-extensions/lint-rules](https://github.com/angular-extensions/lint-rules)
3+
4+
[![npm version](https://img.shields.io/npm/v/@angular-extensions/lint-rules.svg?style=flat-square)](https://www.npmjs.com/package/@angular-extensions/lint-rules)
5+
[![npm downloads total](https://img.shields.io/npm/dt/@angular-extensions/lint-rules.svg?style=flat-square)](https://www.npmjs.com/package/@angular-extensions/lint-rules)
6+
[![npm downloads monthly](https://img.shields.io/npm/dm/@angular-extensions/lint-rules.svg?style=flat-square)](https://www.npmjs.com/package/@angular-extensions/lint-rules)
7+
[![CircleCI](https://circleci.com/gh/bithost-gmbh/@angular-extensions/lint-rules.svg?style=svg)](https://circleci.com/gh/bithost-gmbh/@angular-extensions/lint-rules)
8+
9+
## Description
10+
This repository offers some [tslint](https://github.com/palantir/tslint) rules useful for angular projects, see [Rules](#Rules).
11+
12+
## Installation / Usage
13+
* Install the [@angular-extensions/lint-rules](https://www.npmjs.com/package/@angular-extensions/lint-rules) npm package:
14+
```
15+
npm install @angular-extensions/lint-rules --save-dev
16+
```
17+
* Add `@angular-extensions/lint-rules` to the `extensions` list in your `tslint.json`:
18+
```json
19+
{
20+
"extends": [
21+
"tslint:recommended",
22+
"@angular-extensions/lint-rules"
23+
]
24+
}
25+
```
26+
* Lint your project with
27+
```
28+
ng lint
29+
```
30+
31+
## Rules
32+
The package includes the following rules:
33+
34+
| Rule | Description | Details | Enabled by default? |
35+
| --- | --- | --- | --- |
36+
| `angular-call-super-lifecycle-method-in-extended-class` | Enforces the application to call parent lifecycle function e.g. `super.ngOnDestroy()` when using inheritance within an Angular component or directive. | [Details](#angular-call-super-lifecycle-method-in-extended-class) | yes |
37+
| `angular-rxjs-takeuntil-before-subscribe` | Enforces the application of the `takeUntil` operator when calling of `subscribe` within an Angular component or directive. | [Details](#angular-rxjs-takeuntil-before-subscribe) | yes |
38+
39+
### angular-call-super-lifecycle-method-in-extended-class
40+
This rule tries to avoid memory leaks and other problems in angular components and directives by ensuring that
41+
a [life-cycle method](https://angular.io/guide/lifecycle-hooks), e.g. `ngOnDestroy(){}`, overriding its parent implementation
42+
must call the parent implementation with `super.ngOnDestroy()`.
43+
44+
#### Example
45+
This should trigger an error:
46+
```typescript
47+
class MyClass {
48+
ngOnDestroy() {
49+
const a = 5;
50+
}
51+
}
52+
@Component({
53+
selector: 'app-my'
54+
})
55+
class MyComponent2 extends MyClass {
56+
57+
ngOnDestroy() {
58+
~~~~~~~~~~~ call to super.ngOnDestroy() is missing
59+
const b = 6;
60+
}
61+
}
62+
```
63+
while this should be fine:
64+
```typescript
65+
class MyClass {
66+
ngOnDestroy() {
67+
const a = 5;
68+
}
69+
}
70+
@Component({
71+
selector: 'app-my'
72+
})
73+
class MyComponent extends MyClass {
74+
75+
ngOnDestroy() {
76+
super.ngOnDestroy();
77+
const b = 6;
78+
}
79+
}
80+
81+
@Component({
82+
selector: 'app-my2'
83+
})
84+
class MyComponent2 {
85+
ngOnDestroy() {
86+
const b = 6;
87+
}
88+
}
89+
```
90+
91+
92+
### angular-rxjs-takeuntil-before-subscribe
93+
94+
This rule tries to avoid memory leaks in angular components and directives when calling `.subscribe()` without properly unsubscribing
95+
by enforcing the application of the `takeUntil(this.destroy$)` operator before the `.subscribe()`
96+
as well as before certain operators (`shareReplay` without `refCount: true`)
97+
and ensuring the component implements the `ngOnDestroy`
98+
method invoking `this.destroy$.next()` and `this.destroy$.complete()`.
99+
All classes with a `@Component` or `@Directive` decorator and all their parent classes will be checked.
100+
101+
#### Example
102+
This should trigger an error:
103+
```typescript
104+
@Component({
105+
selector: 'app-my',
106+
template: '<div>{{k$ | async}}</div>'
107+
})
108+
class MyComponent {
109+
~~~~~~~~~~~ component containing subscribe must implement the ngOnDestroy() method
110+
111+
112+
k$ = a.pipe(shareReplay(1));
113+
~~~~~~~~~~~~~~ the shareReplay operator used within a component must be preceded by takeUntil
114+
115+
someMethod() {
116+
const e = a.pipe(switchMap(_ => b)).subscribe();
117+
~~~~~~~~~ subscribe within a component must be preceded by takeUntil
118+
}
119+
}
120+
```
121+
122+
while this should be fine:
123+
```typescript
124+
@Component({
125+
selector: 'app-my',
126+
template: '<div>{{k$ | async}}</div>'
127+
})
128+
class MyComponent implements SomeInterface, OnDestroy {
129+
private destroy$: Subject<void> = new Subject<void>();
130+
131+
k$ = a.pipe(takeUntil(this.destroy$), shareReplay(1));
132+
133+
someMethod() {
134+
const e = a.pipe(switchMap(_ => b), takeUntil(this.destroy$)).subscribe();
135+
}
136+
137+
ngOnDestroy() {
138+
this.destroy$.next();
139+
this.destroy$.complete();
140+
}
141+
}
142+
```
143+
144+
145+
## Further reading
146+
* https://slides.com/estebangehring/angular-app-memory-leak
147+
* https://blog.angularindepth.com/the-best-way-to-unsubscribe-rxjs-observable-in-the-angular-applications-d8f9aa42f6a0
148+
* https://github.com/cartant/rxjs-tslint-rules/pull/107
149+
150+
## Contributors
151+
* Esteban Gehring (@macjohnny)
152+
* Lutz Bliska (@lbliska)
153+
154+
Note: this project is based on work in https://github.com/cartant/rxjs-tslint-rules/pull/107
155+
156+
## Development
157+
Clone the repository and install the dependencies with `npm install`.
158+
159+
Note: using the build artifacts with `npm link` does not work correctly,
160+
since there will be a mismatch between the typescript version used by the consumer code
161+
and the typescript version used by the lint rules code.
162+
To test the package in a project, run
163+
```
164+
npm run build
165+
cd dist
166+
npm install --production
167+
```
168+
Then copy the content of the `/dist` folder (including the `node_modules` folder) into `node_modules/@angular-extensions/lint-rules`
169+
in the consumer project.
170+
171+
### Publish
172+
To publish the package, run
173+
```
174+
npm run publish-package
175+
```

Diff for: ‎buildscripts/replace-config.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"from": "\"private\": true,",
3+
"to": "",
4+
"files": [
5+
"dist/package.json"
6+
]
7+
}

Diff for: ‎lint-rules.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"rulesDirectory": ["rules"],
3+
"rules": {
4+
"angular-rxjs-takeuntil-before-subscribe": { "severity": "error" },
5+
"angular-call-super-lifecycle-method-in-extended-class": { "severity": "error" }
6+
}
7+
}

Diff for: ‎package-lock.json

+3,490
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: ‎package.json

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "@angular-extensions/lint-rules",
3+
"version": "1.0.0",
4+
"description": "tslint rules for angular projects",
5+
"main": "lint-rules.json",
6+
"scripts": {
7+
"build": "tsc && cpy ./package.json ./dist/ && cpy ./lint-rules.json ./dist/ && cpy ./README.md ./dist/",
8+
"test": "npm run build && tslint --test test/rules/**/tslint.json",
9+
"publish-package": "standard-version && npm run build && replace-in-file --configFile=buildscripts/replace-config.json && npm publish dist && git push --follow-tags"
10+
},
11+
"author": "Esteban Gehring, Bithost GmbH",
12+
"repository": {
13+
"url": "https://github.com/angular-extensions/lint-rules"
14+
},
15+
"bugs": {
16+
"url": "https://github.com/angular-extensions/lint-rules/issues"
17+
},
18+
"license": "MIT",
19+
"peerDependencies": {
20+
"tslint": "5.*",
21+
"typescript": "^3.0.0"
22+
},
23+
"private": true,
24+
"devDependencies": {
25+
"cpy-cli": "^3.0.0",
26+
"replace-in-file": "^5.0.2",
27+
"rxjs": "^6.5.3",
28+
"standard-version": "^7.1.0",
29+
"tslint": "^5.9.1",
30+
"typescript": "~3.7.2"
31+
},
32+
"dependencies": {
33+
"@phenomnomnominal/tsquery": "^4.0.0",
34+
"tsutils-etc": "^1.1.0"
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import * as Lint from "tslint";
2+
import * as ts from "typescript";
3+
import * as tsutils from "tsutils";
4+
import { tsquery } from "@phenomnomnominal/tsquery";
5+
import { dedent } from "tslint/lib/utils";
6+
7+
export class Rule extends Lint.Rules.TypedRule {
8+
9+
public static FAILURE_STRING = 'call to super.{methodName}() is missing';
10+
11+
public static metadata: Lint.IRuleMetadata = {
12+
description: dedent`Enforces the application to call parent lifecycle function e.g. super.ngOnDestroy()
13+
when using inheritance within an Angular component or directive.`,
14+
options: null,
15+
optionsDescription: "",
16+
requiresTypeInfo: true,
17+
ruleName: "angular-call-super-lifecycle-method-in-extended-class",
18+
type: "functionality",
19+
typescriptOnly: true
20+
};
21+
22+
private componentLifeCycleMethods: string[] = [
23+
"ngOnDestroy",
24+
"ngOnInit",
25+
"ngAfterViewInit",
26+
"ngAfterContentInit",
27+
"ngOnChanges",
28+
"ngDoCheck",
29+
"ngAfterContentChecked",
30+
"ngAfterViewChecked"
31+
];
32+
private directiveLifeCycleMethods: string[] = [
33+
"ngOnDestroy",
34+
"ngOnInit"
35+
];
36+
37+
public applyWithProgram(
38+
sourceFile: ts.SourceFile,
39+
program: ts.Program
40+
): Lint.RuleFailure[] {
41+
const failures: Lint.RuleFailure[] = [];
42+
43+
// find all classes with an @Component() decorator and heritage clause
44+
const componentClassDeclarations = tsquery(
45+
sourceFile,
46+
`ClassDeclaration:has(Decorator[expression.expression.name='Component']):has(HeritageClause:has(ExpressionWithTypeArguments))`
47+
) as ts.ClassDeclaration[];
48+
49+
// find all classes with an @Directive() decorator and heritage clause
50+
const directiveClassDeclarations = tsquery(
51+
sourceFile,
52+
`ClassDeclaration:has(Decorator[expression.expression.name='Directive']):has(HeritageClause:has(ExpressionWithTypeArguments))`
53+
) as ts.ClassDeclaration[];
54+
55+
// check all components
56+
[
57+
...componentClassDeclarations,
58+
...componentClassDeclarations
59+
.map(classDeclaration => this.findParentClasses(program, classDeclaration))
60+
.reduce((allParentClasses, parentClasses) => [...allParentClasses, ...parentClasses], [])
61+
].forEach(classDeclaration => {
62+
failures.push(
63+
...this.checkLifeCycleMethodSuperInvokation(
64+
classDeclaration.getSourceFile(),
65+
program,
66+
classDeclaration as ts.ClassDeclaration,
67+
false
68+
)
69+
);
70+
});
71+
72+
// check all directives
73+
[
74+
...directiveClassDeclarations,
75+
...directiveClassDeclarations
76+
.map(classDeclaration => this.findParentClasses(program, classDeclaration))
77+
.reduce((allParentClasses, parentClasses) => [...allParentClasses, ...parentClasses], [])
78+
].forEach(classDeclaration => {
79+
failures.push(
80+
...this.checkLifeCycleMethodSuperInvokation(
81+
classDeclaration.getSourceFile(),
82+
program,
83+
classDeclaration as ts.ClassDeclaration,
84+
true
85+
)
86+
);
87+
});
88+
89+
return failures;
90+
}
91+
92+
/**
93+
* Verify that a class implementing a lifecycle method calls super.lifeCycleMethod() if it overrides any parent implementation of it
94+
*/
95+
private checkLifeCycleMethodSuperInvokation(sourceFile: ts.SourceFile,
96+
program: ts.Program,
97+
classDeclaration: ts.ClassDeclaration,
98+
isDirective: boolean): Lint.RuleFailure[] {
99+
const lintFailures: Lint.RuleFailure[] = [];
100+
const lifeCycleMethodsToCheck = isDirective ? this.directiveLifeCycleMethods : this.componentLifeCycleMethods;
101+
102+
// check all life cycle methods
103+
lifeCycleMethodsToCheck.forEach(lifeCycleMethodName => {
104+
// find an implementation of the lifecycle method
105+
const lifeCycleMethod = this.findLifeCycleMethod(classDeclaration, lifeCycleMethodName);
106+
107+
// if implementation found, check parent implementations are called when overriding
108+
if (lifeCycleMethod) {
109+
const parentClasses = this.findParentClasses(program, classDeclaration);
110+
if (parentClasses.some(parentClass => !!this.findLifeCycleMethod(parentClass, lifeCycleMethodName))) {
111+
// some parent has life cycle method implementation, ensure super.lifeCycleMethod() is called
112+
const superLifeCycleMethodCall = this.findSuperLifeCycleMethodInvocation(lifeCycleMethod, lifeCycleMethodName);
113+
if (!superLifeCycleMethodCall) {
114+
lintFailures.push(
115+
new Lint.RuleFailure(
116+
sourceFile,
117+
lifeCycleMethod.name ? lifeCycleMethod.name.getStart() : sourceFile.getStart(),
118+
lifeCycleMethod.name ? lifeCycleMethod.name.getStart() +
119+
lifeCycleMethod.name.getWidth() : sourceFile.getStart() + sourceFile.getWidth(),
120+
Rule.FAILURE_STRING.replace("{methodName}", lifeCycleMethod.name.getText()),
121+
this.ruleName
122+
)
123+
)
124+
}
125+
}
126+
}
127+
});
128+
return lintFailures;
129+
}
130+
131+
/**
132+
* Returns the method declaration of the life cycle method implemenation in the class given
133+
*/
134+
private findLifeCycleMethod(
135+
classDeclaration: ts.ClassDeclaration,
136+
lifeCycleMethodName: string
137+
): ts.MethodDeclaration {
138+
return classDeclaration.members.find(
139+
member => member.name && member.name.getText() === lifeCycleMethodName
140+
) as ts.MethodDeclaration;
141+
}
142+
143+
/**
144+
* Returns the property access expression of the invocation of super.lifeCycleMethod(), if any
145+
*/
146+
private findSuperLifeCycleMethodInvocation(
147+
methodDeclaration: ts.MethodDeclaration,
148+
lifeCycleMethodName: string
149+
): ts.PropertyAccessExpression | undefined {
150+
151+
if (!methodDeclaration.body) {
152+
return undefined;
153+
}
154+
155+
const propertyAccessExpressions = tsquery(
156+
methodDeclaration,
157+
`CallExpression > PropertyAccessExpression[expression.kind=${ts.SyntaxKind.SuperKeyword}][name.text="${lifeCycleMethodName}"]`
158+
) as ts.PropertyAccessExpression[];
159+
if (propertyAccessExpressions && propertyAccessExpressions.length > 0) {
160+
return propertyAccessExpressions[0];
161+
}
162+
}
163+
164+
/**
165+
* recursively find all parent classes of the class given
166+
*/
167+
private findParentClasses(
168+
program: ts.Program,
169+
classDeclarationToBeChecked: ts.ClassDeclaration
170+
): ts.ClassDeclaration[] {
171+
const classDeclarationsFound: ts.ClassDeclaration[] = [];
172+
const typeChecker = program.getTypeChecker();
173+
174+
const heritageClauses = classDeclarationToBeChecked.heritageClauses;
175+
176+
if (!heritageClauses) {
177+
return [];
178+
}
179+
heritageClauses.forEach(heritageClause => {
180+
if (heritageClause.token === ts.SyntaxKind.ExtendsKeyword) {
181+
heritageClause.types.forEach(heritageClauseType => {
182+
if (!tsutils.isIdentifier(heritageClauseType.expression)) {
183+
return;
184+
}
185+
const extendType = typeChecker.getTypeAtLocation(heritageClauseType.expression);
186+
if (extendType && extendType.symbol
187+
&& extendType.symbol.declarations
188+
&& extendType.symbol.declarations.length > 0
189+
&& tsutils.isClassDeclaration(extendType.symbol.declarations[0])
190+
) {
191+
const parentClassDeclaration = extendType.symbol.declarations[0] as ts.ClassDeclaration;
192+
classDeclarationsFound.push(parentClassDeclaration);
193+
classDeclarationsFound.push(...this.findParentClasses(program, parentClassDeclaration))
194+
}
195+
})
196+
}
197+
});
198+
return classDeclarationsFound;
199+
};
200+
}

Diff for: ‎rules/angularRxjsTakeuntilBeforeSubscribeRule.ts

+517
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
2+
class MyClass {
3+
ngOnDestroy() {
4+
const a = 5;
5+
}
6+
}
7+
class MySubClass extends MyClass {
8+
9+
}
10+
11+
class MySubClass2 extends MyClass {
12+
ngOnDestroy() {}
13+
}
14+
15+
@Component({
16+
selector: 'app-my'
17+
})
18+
class MyComponent {
19+
ngOnDestroy() {
20+
const b = 6;
21+
}
22+
}
23+
24+
@Component({
25+
selector: 'app-my'
26+
})
27+
class MyComponent2 extends MyClass {
28+
29+
ngOnDestroy() {
30+
~~~~~~~~~~~ [super-ng-on-destroy-missing]
31+
const b = 6;
32+
}
33+
}
34+
35+
@Component({
36+
selector: 'app-my'
37+
})
38+
class MyComponent3 extends MySubClass {
39+
40+
ngOnDestroy() {
41+
~~~~~~~~~~~ [super-ng-on-destroy-missing]
42+
const b = 6;
43+
}
44+
}
45+
46+
47+
@Component({
48+
selector: 'app-my'
49+
})
50+
class MyComponent4 extends MyClass {
51+
52+
ngOnDestroy() {
53+
super.ngOnDestroy();
54+
const b = 6;
55+
}
56+
}
57+
58+
@Component({
59+
selector: 'app-my'
60+
})
61+
class MyComponent5 extends MySubClass {
62+
63+
ngOnDestroy() {
64+
super.ngOnDestroy();
65+
const b = 6;
66+
}
67+
}
68+
69+
70+
class MyClass3 {
71+
ngOnDestroy() {
72+
const a = 5;
73+
}
74+
}
75+
76+
class MySubClass3 extends MyClass3 {
77+
78+
ngOnDestroy() {
79+
~~~~~~~~~~~ [super-ng-on-destroy-missing]
80+
const a = 5;
81+
}
82+
}
83+
84+
@Component({
85+
selector: 'app-my'
86+
})
87+
class MyComponent6 extends MySubClass3 {
88+
89+
}
90+
91+
class MyAbstractDirective {
92+
ngOnDestroy() {
93+
const a = 5;
94+
}
95+
}
96+
97+
[super-ng-on-destroy-missing]: call to super.ngOnDestroy() is missing
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"compilerOptions": {
3+
"baseUrl": ".",
4+
"lib": ["es2015"],
5+
"noEmit": true,
6+
"paths": {
7+
"rxjs": ["../../../../node_modules/rxjs"]
8+
},
9+
"skipLibCheck": true,
10+
"target": "es5"
11+
},
12+
"include": ["fixture.ts"]
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"rulesDirectory": ["../../../../dist/rules"],
3+
"rules": {
4+
"angular-call-super-lifecycle-method-in-extended-class": { "severity": "error" }
5+
}
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
class MyAbstractDirective {
2+
ngOnDestroy() {
3+
const a = 5;
4+
}
5+
}
6+
7+
@Directive({
8+
selector: '[some-attribute]'
9+
})
10+
class MyDirective extends MyAbstractDirective {
11+
12+
ngOnDestroy() {
13+
~~~~~~~~~~~ [super-ng-on-destroy-missing]
14+
const a = 5;
15+
}
16+
}
17+
18+
@Directive({
19+
selector: '[some-attribute]'
20+
})
21+
class MyDirective extends MyAbstractDirective {
22+
23+
ngOnDestroy() {
24+
super.ngOnDestroy();
25+
const a = 5;
26+
}
27+
}
28+
29+
30+
[super-ng-on-destroy-missing]: call to super.ngOnDestroy() is missing
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"compilerOptions": {
3+
"baseUrl": ".",
4+
"lib": ["es2015"],
5+
"noEmit": true,
6+
"paths": {
7+
"rxjs": ["../../../../node_modules/rxjs"]
8+
},
9+
"skipLibCheck": true,
10+
"target": "es5"
11+
},
12+
"include": ["fixture.ts"]
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"rulesDirectory": ["../../../../dist/rules"],
3+
"rules": {
4+
"angular-call-super-lifecycle-method-in-extended-class": { "severity": "error" }
5+
}
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { combineLatest, of, Subject } from "rxjs";
2+
import { switchMap, takeUntil, shareReplay, tap } from "rxjs/operators";
3+
4+
const a = of("a");
5+
const b = of("b");
6+
const c = of("c");
7+
const d = of("d");
8+
9+
const e = a.pipe(switchMap(_ => b)).subscribe();
10+
11+
const f = a.pipe(switchMap(_ => b), takeUntil(d)).subscribe();
12+
13+
const g = a.pipe(takeUntil(d), s => switchMap(_ => b)).subscribe();
14+
15+
class MyClass {
16+
someMethod() {
17+
const e = a.pipe(switchMap(_ => b)).subscribe();
18+
19+
const f = a.pipe(switchMap(_ => b), takeUntil(d)).subscribe();
20+
21+
const g = a.pipe(takeUntil(d), s => switchMap(_ => b)).subscribe();
22+
}
23+
}
24+
25+
@Component({
26+
selector: 'app-my'
27+
})
28+
class MyComponent {
29+
~~~~~~~~~~~ [angular-rxjs-takeuntil-before-subscribe-subscribe-ondestroy]
30+
31+
private destroy$: Subject<void> = new Subject<void>();
32+
33+
k$ = a.pipe(shareReplay(1));
34+
~~~~~~~~~~~~~~ [angular-rxjs-takeuntil-before-subscribe-operator-sharereplay]
35+
36+
someMethod() {
37+
const d = a.subscribe();
38+
~~~~~~~~~ [angular-rxjs-takeuntil-before-subscribe-subscribe]
39+
40+
const e = a.pipe(switchMap(_ => b)).subscribe();
41+
~~~~~~~~~ [angular-rxjs-takeuntil-before-subscribe-subscribe]
42+
43+
const f = a.pipe(switchMap(_ => b), takeUntil(this.destroy$)).subscribe();
44+
45+
const g = a.pipe(takeUntil(this.destroy$), switchMap(_ => b)).subscribe();
46+
~~~~~~~~~ [angular-rxjs-takeuntil-before-subscribe-subscribe]
47+
48+
const h = a.pipe(switchMap(_ => b), takeUntil(d)).subscribe();
49+
~ [angular-rxjs-takeuntil-before-subscribe-subscribe-subject-name]
50+
51+
const k1 = a.pipe(takeUntil(this.destroy$), shareReplay(1)).subscribe();
52+
~~~~~~~~~ [angular-rxjs-takeuntil-before-subscribe-subscribe]
53+
54+
const k = a.pipe(shareReplay(1), takeUntil(this.destroy$)).subscribe();
55+
~~~~~~~~~~~~~~ [angular-rxjs-takeuntil-before-subscribe-operator-sharereplay]
56+
57+
const k2 = a.pipe(shareReplay({bufferSize: 1, refCount: false}), takeUntil(this.destroy$)).subscribe();
58+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [angular-rxjs-takeuntil-before-subscribe-operator-sharereplay]
59+
60+
const k3 = a.pipe(shareReplay({bufferSize: 1, refCount: true}), takeUntil(this.destroy$)).subscribe();
61+
62+
const m = a.pipe(tap(), shareReplay(1), takeUntil(this.destroy$)).subscribe();
63+
~~~~~~~~~~~~~~ [angular-rxjs-takeuntil-before-subscribe-operator-sharereplay]
64+
65+
const n = a.pipe(takeUntil(d), shareReplay(1), takeUntil(this.destroy$)).subscribe();
66+
~ [angular-rxjs-takeuntil-before-subscribe-subscribe-subject-name]
67+
68+
}
69+
}
70+
71+
@Component({
72+
selector: 'app-my'
73+
})
74+
class MyComponent implements OnDestroy {
75+
someMethod() {
76+
const f = a.pipe(switchMap(_ => b), takeUntil(this._destroy$)).subscribe();
77+
}
78+
79+
ngOnDestroy() {
80+
~~~~~~~~~~~ [angular-rxjs-takeuntil-before-subscribe-subscribe-next-missing]
81+
~~~~~~~~~~~ [angular-rxjs-takeuntil-before-subscribe-subscribe-complete-missing]
82+
// this._destroy$.next() is missing
83+
this.destroy$.next();
84+
this.destroy$.complete();
85+
}
86+
}
87+
88+
class MySuperAbstractComponent {
89+
someMethod2() {
90+
const d = a.subscribe();
91+
~~~~~~~~~ [angular-rxjs-takeuntil-before-subscribe-subscribe]
92+
}
93+
}
94+
95+
class MyAbstractComponent extends MySuperAbstractComponent {
96+
someMethod() {
97+
const d = a.subscribe();
98+
~~~~~~~~~ [angular-rxjs-takeuntil-before-subscribe-subscribe]
99+
}
100+
}
101+
102+
@Component({
103+
selector: 'app-my'
104+
})
105+
class MyConcreteComponent extends MyAbstractComponent {
106+
107+
someMethod2() {
108+
const d = a.subscribe();
109+
~~~~~~~~~ [angular-rxjs-takeuntil-before-subscribe-subscribe]
110+
}
111+
}
112+
113+
@Component({
114+
selector: 'app-my'
115+
})
116+
class MyComponent implements SomeInterface, OnDestroy {
117+
private destroy$: Subject<void> = new Subject<void>();
118+
119+
k$ = a.pipe(takeUntil(this.destroy$), shareReplay(1));
120+
121+
someMethod() {
122+
const e = a.pipe(switchMap(_ => b), takeUntil(this.destroy$)).subscribe();
123+
124+
const k = a.pipe(takeUntil(this.destroy$), shareReplay(1), takeUntil(this.destroy$)).subscribe();
125+
}
126+
127+
ngOnDestroy() {
128+
this.destroy$.next();
129+
this.destroy$.complete();
130+
}
131+
}
132+
133+
class MySuperAbstractComponent2 {
134+
protected destroy$: Subject<void> = new Subject();
135+
136+
someMethod2() {
137+
const d = a.pipe(takeUntil(this.destroy$)).subscribe();
138+
}
139+
140+
141+
ngOnDestroy() {
142+
this.destroy$.next();
143+
this.destroy$.complete();
144+
}
145+
}
146+
147+
class MyAbstractComponent2 extends MySuperAbstractComponent2 {
148+
someMethod() {
149+
const d = a.pipe(takeUntil(this.destroy$)).subscribe();
150+
}
151+
}
152+
153+
@Component({
154+
selector: 'app-my'
155+
})
156+
class MyConcreteComponent2 extends MyAbstractComponent2 {
157+
158+
someMethod2() {
159+
const d = a.pipe(takeUntil(this.destroy$)).subscribe();
160+
}
161+
}
162+
163+
164+
[angular-rxjs-takeuntil-before-subscribe-subscribe]: subscribe within a component must be preceded by takeUntil
165+
[angular-rxjs-takeuntil-before-subscribe-subscribe-subject-name]: takeUntil argument must be a property of the class, e.g. takeUntil(this.destroy$)
166+
[angular-rxjs-takeuntil-before-subscribe-subscribe-ondestroy]: component containing subscribe must implement the ngOnDestroy() method
167+
[angular-rxjs-takeuntil-before-subscribe-subscribe-next-missing]: there must be an invocation of this._destroy$.next() in ngOnDestroy()
168+
[angular-rxjs-takeuntil-before-subscribe-subscribe-complete-missing]: there must be an invocation of this._destroy$.complete() in ngOnDestroy()
169+
[angular-rxjs-takeuntil-before-subscribe-operator-sharereplay]: the shareReplay operator used within a component must be preceded by takeUntil
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"compilerOptions": {
3+
"baseUrl": ".",
4+
"lib": ["es2015"],
5+
"noEmit": true,
6+
"paths": {
7+
"rxjs": ["../../../../node_modules/rxjs"]
8+
},
9+
"skipLibCheck": true,
10+
"target": "es5"
11+
},
12+
"include": ["fixture.ts"]
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"rulesDirectory": ["../../../../dist/rules"],
3+
"rules": {
4+
"angular-rxjs-takeuntil-before-subscribe": { "severity": "error" }
5+
}
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { combineLatest, of, Subject } from "rxjs";
2+
import { switchMap, takeUntil, shareReplay, tap } from "rxjs/operators";
3+
4+
const a = of("a");
5+
const b = of("b");
6+
const c = of("c");
7+
const d = of("d");
8+
9+
const e = a.pipe(switchMap(_ => b)).subscribe();
10+
11+
const f = a.pipe(switchMap(_ => b), takeUntil(d)).subscribe();
12+
13+
const g = a.pipe(takeUntil(d), s => switchMap(_ => b)).subscribe();
14+
15+
class MyClass {
16+
someMethod() {
17+
const e = a.pipe(switchMap(_ => b)).subscribe();
18+
19+
const f = a.pipe(switchMap(_ => b), takeUntil(d)).subscribe();
20+
21+
const g = a.pipe(takeUntil(d), s => switchMap(_ => b)).subscribe();
22+
}
23+
}
24+
25+
@Directive({
26+
selector: '[app-my]'
27+
})
28+
class MyDirective {
29+
~~~~~~~~~~~ [angular-rxjs-takeuntil-before-subscribe-subscribe-ondestroy]
30+
31+
private destroy$: Subject<void> = new Subject<void>();
32+
33+
k$ = a.pipe(shareReplay(1));
34+
~~~~~~~~~~~~~~ [angular-rxjs-takeuntil-before-subscribe-operator-sharereplay]
35+
36+
someMethod() {
37+
const d = a.subscribe();
38+
~~~~~~~~~ [angular-rxjs-takeuntil-before-subscribe-subscribe]
39+
40+
const e = a.pipe(switchMap(_ => b)).subscribe();
41+
~~~~~~~~~ [angular-rxjs-takeuntil-before-subscribe-subscribe]
42+
43+
const f = a.pipe(switchMap(_ => b), takeUntil(this.destroy$)).subscribe();
44+
45+
const g = a.pipe(takeUntil(this.destroy$), switchMap(_ => b)).subscribe();
46+
~~~~~~~~~ [angular-rxjs-takeuntil-before-subscribe-subscribe]
47+
48+
const h = a.pipe(switchMap(_ => b), takeUntil(d)).subscribe();
49+
~ [angular-rxjs-takeuntil-before-subscribe-subscribe-subject-name]
50+
51+
const k1 = a.pipe(takeUntil(this.destroy$), shareReplay(1)).subscribe();
52+
~~~~~~~~~ [angular-rxjs-takeuntil-before-subscribe-subscribe]
53+
54+
const k = a.pipe(shareReplay(1), takeUntil(this.destroy$)).subscribe();
55+
~~~~~~~~~~~~~~ [angular-rxjs-takeuntil-before-subscribe-operator-sharereplay]
56+
57+
const m = a.pipe(tap(), shareReplay(1), takeUntil(this.destroy$)).subscribe();
58+
~~~~~~~~~~~~~~ [angular-rxjs-takeuntil-before-subscribe-operator-sharereplay]
59+
60+
const n = a.pipe(takeUntil(d), shareReplay(1), takeUntil(this.destroy$)).subscribe();
61+
~ [angular-rxjs-takeuntil-before-subscribe-subscribe-subject-name]
62+
63+
}
64+
}
65+
66+
@Directive({
67+
selector: '[app-my]'
68+
})
69+
class MyDirective implements OnDestroy {
70+
someMethod() {
71+
const f = a.pipe(switchMap(_ => b), takeUntil(this._destroy$)).subscribe();
72+
}
73+
74+
ngOnDestroy() {
75+
~~~~~~~~~~~ [angular-rxjs-takeuntil-before-subscribe-subscribe-next-missing]
76+
~~~~~~~~~~~ [angular-rxjs-takeuntil-before-subscribe-subscribe-complete-missing]
77+
// this._destroy$.next() is missing
78+
this.destroy$.next();
79+
this.destroy$.complete();
80+
}
81+
}
82+
83+
84+
@Directive({
85+
selector: '[app-my]'
86+
})
87+
class MyDirective implements SomeInterface, OnDestroy {
88+
private destroy$: Subject<void> = new Subject<void>();
89+
90+
k$ = a.pipe(takeUntil(this.destroy$), shareReplay(1));
91+
92+
someMethod() {
93+
const e = a.pipe(switchMap(_ => b), takeUntil(this.destroy$)).subscribe();
94+
95+
const k = a.pipe(takeUntil(this.destroy$), shareReplay(1), takeUntil(this.destroy$)).subscribe();
96+
}
97+
98+
ngOnDestroy() {
99+
this.destroy$.next();
100+
this.destroy$.complete();
101+
}
102+
}
103+
104+
105+
[angular-rxjs-takeuntil-before-subscribe-subscribe]: subscribe within a component must be preceded by takeUntil
106+
[angular-rxjs-takeuntil-before-subscribe-subscribe-subject-name]: takeUntil argument must be a property of the class, e.g. takeUntil(this.destroy$)
107+
[angular-rxjs-takeuntil-before-subscribe-subscribe-ondestroy]: component containing subscribe must implement the ngOnDestroy() method
108+
[angular-rxjs-takeuntil-before-subscribe-subscribe-next-missing]: there must be an invocation of this._destroy$.next() in ngOnDestroy()
109+
[angular-rxjs-takeuntil-before-subscribe-subscribe-complete-missing]: there must be an invocation of this._destroy$.complete() in ngOnDestroy()
110+
[angular-rxjs-takeuntil-before-subscribe-operator-sharereplay]: the shareReplay operator used within a component must be preceded by takeUntil
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"compilerOptions": {
3+
"baseUrl": ".",
4+
"lib": ["es2015"],
5+
"noEmit": true,
6+
"paths": {
7+
"rxjs": ["../../../../node_modules/rxjs"]
8+
},
9+
"skipLibCheck": true,
10+
"target": "es5"
11+
},
12+
"include": ["fixture.ts"]
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"rulesDirectory": ["../../../../dist/rules"],
3+
"rules": {
4+
"angular-rxjs-takeuntil-before-subscribe": { "severity": "error" }
5+
}
6+
}

Diff for: ‎tsconfig.json

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es5",
4+
"outDir": "dist/rules",
5+
"module": "commonjs",
6+
"strict": true,
7+
"esModuleInterop": true,
8+
"lib": ["dom", "es2017"],
9+
"skipLibCheck": true
10+
},
11+
"include": [
12+
"rules/*.ts"
13+
]
14+
15+
}

0 commit comments

Comments
 (0)
Please sign in to comment.