Skip to content

Commit 0bff440

Browse files
feat(eslint-plugin): add avoid-combining-component-store-selectors rule (#4043)
1 parent f2514ba commit 0bff440

13 files changed

+385
-7
lines changed
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import type {
2+
ESLintUtils,
3+
TSESLint,
4+
} from '@typescript-eslint/experimental-utils';
5+
import { fromFixture } from 'eslint-etc';
6+
import * as path from 'path';
7+
import rule, {
8+
messageId,
9+
} from '../../src/rules/component-store/avoid-combining-component-store-selectors';
10+
import { ruleTester } from '../utils';
11+
12+
type MessageIds = ESLintUtils.InferMessageIdsTypeFromRule<typeof rule>;
13+
type Options = ESLintUtils.InferOptionsTypeFromRule<typeof rule>;
14+
type RunTests = TSESLint.RunTests<MessageIds, Options>;
15+
16+
const validConstructor: () => RunTests['valid'] = () => [
17+
`
18+
import { ComponentStore } from '@ngrx/component-store'
19+
class Ok extends ComponentStore<MoviesState> {
20+
movies$ = this.select((state) => state.movies);
21+
selectedId$ = this.select((state) => state.selectedId);
22+
movie$ = this.select(
23+
this.movies$,
24+
this.selectedId$,
25+
([movies, selectedId]) => movies[selectedId]
26+
);
27+
28+
constructor() {
29+
super({ movies: [] })
30+
}
31+
}`,
32+
`
33+
import { ComponentStore } from '@ngrx/component-store'
34+
class Ok {
35+
readonly movies$ = this.store.select((state) => state.movies);
36+
readonly selectedId$ = this.store.select((state) => state.selectedId);
37+
readonly movie$ = this.store.select(
38+
this.movies$,
39+
this.selectedId$,
40+
([movies, selectedId]) => movies[selectedId]
41+
);
42+
43+
constructor(private readonly store: ComponentStore<MoviesState>) {}
44+
}`,
45+
`
46+
import { ComponentStore } from '@ngrx/component-store'
47+
class Ok {
48+
movie$: Observable<unknown>
49+
50+
constructor(customStore: ComponentStore<MoviesState>) {
51+
const movies = customStore.select((state) => state.movies);
52+
const selectedId = this.customStore.select((state) => state.selectedId);
53+
54+
this.movie$ = this.customStore.select(
55+
this.movies$,
56+
this.selectedId$,
57+
([movies, selectedId]) => movies[selectedId]
58+
);
59+
}
60+
}`,
61+
`
62+
import { ComponentStore } from '@ngrx/component-store'
63+
class Ok {
64+
vm$ = combineLatest(this.somethingElse(), this.customStore.select(selectItems))
65+
66+
constructor(customStore: ComponentStore<MoviesState>) {}
67+
}`,
68+
`
69+
import { ComponentStore } from '@ngrx/component-store'
70+
class Ok extends ComponentStore<MoviesState> {
71+
vm$ = combineLatest(this.select(selectItems), this.somethingElse())
72+
}`,
73+
];
74+
75+
const validInject: () => RunTests['valid'] = () => [
76+
`
77+
import { inject } from '@angular/core'
78+
import { ComponentStore } from '@ngrx/component-store'
79+
class Ok {
80+
readonly store = inject(ComponentStore<MoviesState>)
81+
readonly movies$ = this.store.select((state) => state.movies);
82+
readonly selectedId$ = this.store.select((state) => state.selectedId);
83+
readonly movie$ = this.store.select(
84+
this.movies$,
85+
this.selectedId$,
86+
([movies, selectedId]) => movies[selectedId]
87+
);
88+
}`,
89+
`
90+
import { inject } from '@angular/core'
91+
import { ComponentStore } from '@ngrx/component-store'
92+
class Ok {
93+
readonly store = inject(ComponentStore<MoviesState>)
94+
readonly vm$ = combineLatest(this.store.select(selectItems), somethingElse())
95+
}`,
96+
];
97+
98+
const invalidConstructor: () => RunTests['invalid'] = () => [
99+
fromFixture(`
100+
import { ComponentStore } from '@ngrx/component-store'
101+
102+
class NotOk extends ComponentStore<MoviesState> {
103+
movie$ = combineLatest(
104+
this.select((state) => state.movies),
105+
this.select((state) => state.selectedId),
106+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}]
107+
);
108+
109+
constructor() {
110+
super({ movies: [] })
111+
}
112+
}`),
113+
fromFixture(`
114+
import { ComponentStore } from '@ngrx/component-store'
115+
116+
class NotOk extends ComponentStore<MoviesState> {
117+
movie$ = combineLatest(
118+
this.select((state) => state.movies),
119+
this.select((state) => state.selectedId),
120+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}]
121+
this.select((state) => state.selectedId),
122+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}]
123+
);
124+
125+
constructor() {
126+
super({ movies: [] })
127+
}
128+
}`),
129+
fromFixture(`
130+
import { ComponentStore } from '@ngrx/component-store'
131+
class NotOk {
132+
movie$ = combineLatest(
133+
this.moviesState.select((state) => state.movies),
134+
this.moviesState.select((state) => state.selectedId),
135+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}]
136+
);
137+
138+
constructor(private readonly moviesState: ComponentStore<MoviesState>) {}
139+
}`),
140+
fromFixture(`
141+
import { ComponentStore } from '@ngrx/component-store'
142+
class NotOk {
143+
movie$ = combineLatest(
144+
this.moviesState.select((state) => state.movies),
145+
this.moviesState.select((state) => state.selectedId),
146+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}]
147+
this.moviesState.select((state) => state.selectedId),
148+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}]
149+
);
150+
151+
constructor(private readonly moviesState: ComponentStore<MoviesState>) {}
152+
}`),
153+
fromFixture(`
154+
import { ComponentStore } from '@ngrx/component-store'
155+
class NotOk {
156+
movie$: Observable<unknown>
157+
158+
constructor(store: ComponentStore<MoviesState>) {
159+
this.movie$ = combineLatest(
160+
store.select((state) => state.movies),
161+
store.select((state) => state.selectedId)
162+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}]
163+
);
164+
}
165+
}
166+
`),
167+
fromFixture(`
168+
import { ComponentStore } from '@ngrx/component-store'
169+
class NotOk {
170+
movie$: Observable<unknown>
171+
172+
constructor(store: ComponentStore<MoviesState>) {
173+
this.movie$ = combineLatest(
174+
store.select((state) => state.movies),
175+
store.select((state) => state.selectedId),
176+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}]
177+
store.select((state) => state.selectedId)
178+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}]
179+
);
180+
}
181+
}
182+
`),
183+
];
184+
185+
const invalidInject: () => RunTests['invalid'] = () => [
186+
fromFixture(`
187+
import { inject } from '@angular/core'
188+
import { ComponentStore } from '@ngrx/component-store'
189+
class NotOk {
190+
readonly componentStore = inject(ComponentStore<MoviesState>)
191+
readonly movie$ = combineLatest(
192+
this.componentStore.select((state) => state.movies),
193+
this.componentStore.select((state) => state.selectedId),
194+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}]
195+
);
196+
}`),
197+
fromFixture(`
198+
import { inject } from '@angular/core'
199+
import { ComponentStore } from '@ngrx/component-store'
200+
class NotOk {
201+
readonly store = inject(ComponentStore<MoviesState>)
202+
readonly movie$ = combineLatest(
203+
this.store.select((state) => state.movies),
204+
this.store.select((state) => state.selectedId),
205+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}]
206+
this.store.select((state) => state.selectedId),
207+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}]
208+
);
209+
}`),
210+
];
211+
212+
ruleTester().run(path.parse(__filename).name, rule, {
213+
valid: [...validConstructor(), ...validInject()],
214+
invalid: [...invalidConstructor(), ...invalidInject()],
215+
});

modules/eslint-plugin/spec/rules/avoid-combining-selectors.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ import { inject } from '@angular/core'
8484
8585
class Ok7 {
8686
readonly store = inject(Store)
87-
vm$ = combineLatest(this.store$.select(selectItems), this.somethingElse())
87+
vm$ = combineLatest(this.store.select(selectItems), this.somethingElse())
8888
}`,
8989
`
9090
import { Store } from '@ngrx/store'

modules/eslint-plugin/src/configs/all-requiring-type-checking.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export = {
1212
},
1313
plugins: ['@ngrx'],
1414
rules: {
15+
'@ngrx/avoid-combining-component-store-selectors': 'warn',
1516
'@ngrx/updater-explicit-return-type': 'warn',
1617
'@ngrx/avoid-cyclic-effects': 'warn',
1718
'@ngrx/no-dispatch-in-effects': 'warn',

modules/eslint-plugin/src/configs/all.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export = {
88

99
plugins: ['@ngrx'],
1010
rules: {
11+
'@ngrx/avoid-combining-component-store-selectors': 'warn',
1112
'@ngrx/updater-explicit-return-type': 'warn',
1213
'@ngrx/no-dispatch-in-effects': 'warn',
1314
'@ngrx/no-effects-in-providers': 'error',

modules/eslint-plugin/src/configs/component-store-strict.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,8 @@ export = {
77
parser: '@typescript-eslint/parser',
88

99
plugins: ['@ngrx'],
10-
rules: { '@ngrx/updater-explicit-return-type': 'error' },
10+
rules: {
11+
'@ngrx/avoid-combining-component-store-selectors': 'error',
12+
'@ngrx/updater-explicit-return-type': 'error',
13+
},
1114
};

modules/eslint-plugin/src/configs/component-store.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,8 @@ export = {
77
parser: '@typescript-eslint/parser',
88

99
plugins: ['@ngrx'],
10-
rules: { '@ngrx/updater-explicit-return-type': 'warn' },
10+
rules: {
11+
'@ngrx/avoid-combining-component-store-selectors': 'warn',
12+
'@ngrx/updater-explicit-return-type': 'warn',
13+
},
1114
};

modules/eslint-plugin/src/configs/recommended-requiring-type-checking.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export = {
1212
},
1313
plugins: ['@ngrx'],
1414
rules: {
15+
'@ngrx/avoid-combining-component-store-selectors': 'warn',
1516
'@ngrx/updater-explicit-return-type': 'warn',
1617
'@ngrx/avoid-cyclic-effects': 'warn',
1718
'@ngrx/no-dispatch-in-effects': 'warn',

modules/eslint-plugin/src/configs/recommended.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export = {
88

99
plugins: ['@ngrx'],
1010
rules: {
11+
'@ngrx/avoid-combining-component-store-selectors': 'warn',
1112
'@ngrx/updater-explicit-return-type': 'warn',
1213
'@ngrx/no-dispatch-in-effects': 'warn',
1314
'@ngrx/no-effects-in-providers': 'error',

modules/eslint-plugin/src/configs/strict-requiring-type-checking.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export = {
1212
},
1313
plugins: ['@ngrx'],
1414
rules: {
15+
'@ngrx/avoid-combining-component-store-selectors': 'error',
1516
'@ngrx/updater-explicit-return-type': 'error',
1617
'@ngrx/avoid-cyclic-effects': 'error',
1718
'@ngrx/no-dispatch-in-effects': 'error',

modules/eslint-plugin/src/configs/strict.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export = {
88

99
plugins: ['@ngrx'],
1010
rules: {
11+
'@ngrx/avoid-combining-component-store-selectors': 'error',
1112
'@ngrx/updater-explicit-return-type': 'error',
1213
'@ngrx/no-dispatch-in-effects': 'error',
1314
'@ngrx/no-effects-in-providers': 'error',

0 commit comments

Comments
 (0)