Skip to content

Commit 653efa5

Browse files
fix(eslint-plugin): Support factory by with-state-no-arrays-at-root-level (#5115)
Closes #5104
1 parent 92d6cdd commit 653efa5

File tree

3 files changed

+98
-3
lines changed

3 files changed

+98
-3
lines changed

modules/eslint-plugin/spec/rules/signals/with-state-no-arrays-at-root-level.spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,20 @@ const valid: () => (string | ValidTestCase<Options>)[] = () => [
2222
const initialState = {};
2323
const Store = signalStore(withState(initialState));
2424
`,
25+
`const store = withState(() => ({ foo: 'bar' }))`,
26+
`const store = withState(function() { return { foo: 'bar' }; })`,
27+
`
28+
const initialState = { books: [] };
29+
const store = withState(() => initialState);
30+
`,
31+
`
32+
const initialState = { books: [] };
33+
const store = withState(function() { return initialState; });
34+
`,
35+
`
36+
function getState() { return { count: 0 }; }
37+
const store = withState(getState);
38+
`,
2539
];
2640

2741
const invalid: () => InvalidTestCase<MessageIds, Options>[] = () => [
@@ -95,6 +109,25 @@ const store = withState(function() {});
95109
fromFixture(`
96110
const store = withState(() => {});
97111
~~~~~~~~ [${messageId} { "property": "Function" }]`),
112+
fromFixture(`
113+
const store = withState(() => () => ({ foo: 'bar' }));
114+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId} { "property": "Function" }]`),
115+
fromFixture(`
116+
const store = withState(() => [1, 2, 3]);
117+
~~~~~~~~~~~~~~~ [${messageId} { "property": "Array" }]`),
118+
fromFixture(`
119+
const initialState: string[] = [];
120+
const store = withState(() => initialState);
121+
~~~~~~~~~~~~~~~~~~ [${messageId} { "property": "Array" }]`),
122+
fromFixture(`
123+
const store = withState(() => new Set());
124+
~~~~~~~~~~~~~~~ [${messageId} { "property": "Set" }]`),
125+
fromFixture(`
126+
const store = withState(() => new Map());
127+
~~~~~~~~~~~~~~~ [${messageId} { "property": "Map" }]`),
128+
fromFixture(`
129+
const store = withState(function() { return function() { return { foo: 'bar' }; }; });
130+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId} { "property": "Function" }]`),
98131
];
99132

100133
ruleTester(rule.meta.docs?.requiresTypeChecking).run(

modules/eslint-plugin/src/rules/signals/with-state-no-arrays-at-root-level.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,12 @@ export default createRule<Options, MessageIds>({
5252
} else if (argument) {
5353
const services = ESLintUtils.getParserServices(context);
5454
const typeChecker = services.program.getTypeChecker();
55-
const type = services.getTypeAtLocation(argument);
55+
56+
let type = services.getTypeAtLocation(argument);
57+
const callSignatures = type.getCallSignatures();
58+
if (callSignatures.length > 0) {
59+
type = typeChecker.getReturnTypeOfSignature(callSignatures[0]);
60+
}
5661

5762
if (typeChecker.isArrayType(type) || typeChecker.isTupleType(type)) {
5863
context.report({
@@ -73,8 +78,7 @@ export default createRule<Options, MessageIds>({
7378
return;
7479
}
7580

76-
const callSignatures = type.getCallSignatures();
77-
if (callSignatures.length > 0) {
81+
if (type.getCallSignatures().length > 0) {
7882
context.report({
7983
node: argument,
8084
messageId,
@@ -84,6 +88,16 @@ export default createRule<Options, MessageIds>({
8488
}
8589

8690
const typeString = typeChecker.typeToString(type);
91+
92+
if (typeString === 'void') {
93+
context.report({
94+
node: argument,
95+
messageId,
96+
data: { property: 'Function' },
97+
});
98+
return;
99+
}
100+
87101
const matchedType = NON_RECORD_TYPES.find((t) =>
88102
typeString.startsWith(`${t}<`)
89103
);

projects/www/src/app/pages/guide/eslint-plugin/rules/with-state-no-arrays-at-root-level.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,51 @@ withState should accept a record or dictionary as an input argument.
1010

1111
<!-- Everything above this generated, do not edit -->
1212
<!-- MANUAL-DOC:START -->
13+
14+
## Rule Details
15+
16+
This rule ensures that `withState` does not accept a non-record type at the root level. Arrays, sets, maps, and other non-plain-object types are forbidden as the initial state argument. A factory function is also accepted, in which case its return type is checked instead.
17+
18+
Examples of **correct** code for this rule:
19+
20+
<ngrx-code-example>
21+
22+
```ts
23+
const store = signalStore(withState({ count: 0 }));
24+
25+
const store = signalStore(withState({ items: [] }));
26+
27+
const initialState = { count: 0 };
28+
const store = signalStore(withState(initialState));
29+
30+
// Factory function - return type is checked
31+
const store = signalStore(withState(() => ({ count: 0 })));
32+
33+
// Factory function with dependency injection
34+
const INITIAL_STATE = new InjectionToken('InitialState', {
35+
factory: () => ({ count: 0 }),
36+
});
37+
const store = signalStore(withState(() => inject(INITIAL_STATE)));
38+
```
39+
40+
</ngrx-code-example>
41+
42+
Examples of **incorrect** code for this rule:
43+
44+
<ngrx-code-example>
45+
46+
```ts
47+
const store = signalStore(withState([1, 2, 3]));
48+
49+
const store = signalStore(withState(new Set()));
50+
51+
const store = signalStore(withState(new Map()));
52+
53+
const initialState: number[] = [];
54+
const store = signalStore(withState(initialState));
55+
56+
// Factory function returning a forbidden type
57+
const store = signalStore(withState(() => [1, 2, 3]));
58+
```
59+
60+
</ngrx-code-example>

0 commit comments

Comments
 (0)