Skip to content

Commit bab4d12

Browse files
feat(eslint-plugin): extend NonRecord type checks in state rules (#5045)
Closes #4615 Co-authored-by: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com>
1 parent b26550e commit bab4d12

File tree

10 files changed

+251
-21
lines changed

10 files changed

+251
-21
lines changed

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

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,87 @@ import rule, {
1010
import { ruleTester, fromFixture } from '../../utils';
1111

1212
type MessageIds = ESLintUtils.InferMessageIdsTypeFromRule<typeof rule>;
13-
type Options = readonly ESLintUtils.InferOptionsTypeFromRule<typeof rule>[];
13+
type Options = ESLintUtils.InferOptionsTypeFromRule<typeof rule>;
1414

1515
const valid: () => (string | ValidTestCase<Options>)[] = () => [
1616
`const state = signalState({ numbers: [1, 2, 3] });`,
17+
`const state = signalState({ date: new Date() });`,
18+
`const state = signalState({ set: new Set() });`,
19+
`const state = signalState({ map: new Map() });`,
1720
`const state = state([1, 2, 3]);`,
1821
];
1922

2023
const invalid: () => InvalidTestCase<MessageIds, Options>[] = () => [
2124
fromFixture(`
2225
const state = signalState([1, 2, 3]);
23-
~~~~~~~~~ [${messageId}]`),
26+
~~~~~~~~~ [${messageId} { "property": "Array" }]`),
2427
fromFixture(`
2528
class Fixture {
2629
state = signalState([{ foo: 'bar' }]);
27-
~~~~~~~~~~~~~~~~ [${messageId}]
30+
~~~~~~~~~~~~~~~~ [${messageId} { "property": "Array" }]
2831
}`),
2932
fromFixture(`
3033
const state = signalState<string[]>([]);
31-
~~ [${messageId}]`),
34+
~~ [${messageId} { "property": "Array" }]`),
35+
fromFixture(`
36+
const initialState: string[] = [];
37+
const state = signalState(initialState);
38+
~~~~~~~~~~~~ [${messageId} { "property": "Array" }]`),
39+
fromFixture(`
40+
const state = signalState(new Set());
41+
~~~~~~~~~ [${messageId} { "property": "Set" }]`),
42+
fromFixture(`
43+
const initialState = new Set<string>();
44+
const state = signalState(initialState);
45+
~~~~~~~~~~~~ [${messageId} { "property": "Set" }]`),
46+
fromFixture(`
47+
const state = signalState(new Map());
48+
~~~~~~~~~ [${messageId} { "property": "Map" }]`),
49+
fromFixture(`
50+
const initialState = new Map<string, number>();
51+
const state = signalState(initialState);
52+
~~~~~~~~~~~~ [${messageId} { "property": "Map" }]`),
53+
fromFixture(`
54+
const state = signalState(new WeakSet());
55+
~~~~~~~~~~~~~ [${messageId} { "property": "WeakSet" }]`),
56+
fromFixture(`
57+
const state = signalState(new WeakMap());
58+
~~~~~~~~~~~~~ [${messageId} { "property": "WeakMap" }]`),
59+
fromFixture(`
60+
const state = signalState(new Date());
61+
~~~~~~~~~~ [${messageId} { "property": "Date" }]`),
62+
fromFixture(`
63+
const initialState = new Date();
64+
const state = signalState(initialState);
65+
~~~~~~~~~~~~ [${messageId} { "property": "Date" }]`),
66+
fromFixture(`
67+
const state = signalState(new Error());
68+
~~~~~~~~~~~ [${messageId} { "property": "Error" }]`),
69+
fromFixture(`
70+
const state = signalState(new RegExp('test'));
71+
~~~~~~~~~~~~~~~~~~ [${messageId} { "property": "RegExp" }]`),
72+
fromFixture(`
73+
const state = signalState(/test/);
74+
~~~~~~ [${messageId} { "property": "RegExp" }]`),
75+
fromFixture(`
76+
const state = signalState(new ArrayBuffer(8));
77+
~~~~~~~~~~~~~~~~~~ [${messageId} { "property": "ArrayBuffer" }]`),
78+
fromFixture(`
79+
const buffer = new ArrayBuffer(8);
80+
const state = signalState(new DataView(buffer));
81+
~~~~~~~~~~~~~~~~~~~~ [${messageId} { "property": "DataView" }]`),
82+
fromFixture(`
83+
const state = signalState(new Promise(() => {}));
84+
~~~~~~~~~~~~~~~~~~~~~ [${messageId} { "property": "Promise" }]`),
85+
fromFixture(`
86+
const state = signalState(Promise.resolve({}));
87+
~~~~~~~~~~~~~~~~~~~ [${messageId} { "property": "Promise" }]`),
88+
fromFixture(`
89+
const state = signalState(function() {});
90+
~~~~~~~~~~~~~ [${messageId} { "property": "Function" }]`),
91+
fromFixture(`
92+
const state = signalState(() => {});
93+
~~~~~~~~ [${messageId} { "property": "Function" }]`),
3294
];
3395

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

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

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ import rule, {
1010
import { ruleTester, fromFixture } from '../../utils';
1111

1212
type MessageIds = ESLintUtils.InferMessageIdsTypeFromRule<typeof rule>;
13-
type Options = readonly ESLintUtils.InferOptionsTypeFromRule<typeof rule>[];
13+
type Options = ESLintUtils.InferOptionsTypeFromRule<typeof rule>;
1414

1515
const valid: () => (string | ValidTestCase<Options>)[] = () => [
1616
`const store = withState({ foo: 'bar' })`,
17+
`const store = withState({ date: new Date() })`,
18+
`const store = withState({ set: new Set() })`,
19+
`const store = withState({ map: new Map() })`,
1720
`const Store = signalStore(withState(initialState));`,
1821
`
1922
const initialState = {};
@@ -24,19 +27,74 @@ const valid: () => (string | ValidTestCase<Options>)[] = () => [
2427
const invalid: () => InvalidTestCase<MessageIds, Options>[] = () => [
2528
fromFixture(`
2629
const store = withState([1, 2, 3]);
27-
~~~~~~~~~ [${messageId}]`),
30+
~~~~~~~~~ [${messageId} { "property": "Array" }]`),
2831
fromFixture(`
2932
class Fixture {
3033
store = withState([{ foo: 'bar' }]);
31-
~~~~~~~~~~~~~~~~ [${messageId}]
34+
~~~~~~~~~~~~~~~~ [${messageId} { "property": "Array" }]
3235
}`),
3336
fromFixture(`
3437
const store = withState<string[]>([]);
35-
~~ [${messageId}]`),
38+
~~ [${messageId} { "property": "Array" }]`),
3639
fromFixture(`
37-
const initialState = [];
38-
const store = withState(initialState);
39-
~~~~~~~~~~~~ [${messageId}]`),
40+
const initialState = [];
41+
const store = withState(initialState);
42+
~~~~~~~~~~~~ [${messageId} { "property": "Array" }]`),
43+
fromFixture(`
44+
const store = withState(new Set());
45+
~~~~~~~~~ [${messageId} { "property": "Set" }]`),
46+
fromFixture(`
47+
const initialState = new Set<string>();
48+
const store = withState(initialState);
49+
~~~~~~~~~~~~ [${messageId} { "property": "Set" }]`),
50+
fromFixture(`
51+
const store = withState(new Map());
52+
~~~~~~~~~ [${messageId} { "property": "Map" }]`),
53+
fromFixture(`
54+
const initialState = new Map<string, number>();
55+
const store = withState(initialState);
56+
~~~~~~~~~~~~ [${messageId} { "property": "Map" }]`),
57+
fromFixture(`
58+
const store = withState(new WeakSet());
59+
~~~~~~~~~~~~~ [${messageId} { "property": "WeakSet" }]`),
60+
fromFixture(`
61+
const store = withState(new WeakMap());
62+
~~~~~~~~~~~~~ [${messageId} { "property": "WeakMap" }]`),
63+
fromFixture(`
64+
const store = withState(new Date());
65+
~~~~~~~~~~ [${messageId} { "property": "Date" }]`),
66+
fromFixture(`
67+
const initialState = new Date();
68+
const store = withState(initialState);
69+
~~~~~~~~~~~~ [${messageId} { "property": "Date" }]`),
70+
fromFixture(`
71+
const store = withState(new Error());
72+
~~~~~~~~~~~ [${messageId} { "property": "Error" }]`),
73+
fromFixture(`
74+
const store = withState(new RegExp('test'));
75+
~~~~~~~~~~~~~~~~~~ [${messageId} { "property": "RegExp" }]`),
76+
fromFixture(`
77+
const store = withState(/test/);
78+
~~~~~~ [${messageId} { "property": "RegExp" }]`),
79+
fromFixture(`
80+
const store = withState(new ArrayBuffer(8));
81+
~~~~~~~~~~~~~~~~~~ [${messageId} { "property": "ArrayBuffer" }]`),
82+
fromFixture(`
83+
const buffer = new ArrayBuffer(8);
84+
const store = withState(new DataView(buffer));
85+
~~~~~~~~~~~~~~~~~~~~ [${messageId} { "property": "DataView" }]`),
86+
fromFixture(`
87+
const store = withState(new Promise(() => {}));
88+
~~~~~~~~~~~~~~~~~~~~~ [${messageId} { "property": "Promise" }]`),
89+
fromFixture(`
90+
const store = withState(Promise.resolve({}));
91+
~~~~~~~~~~~~~~~~~~~ [${messageId} { "property": "Promise" }]`),
92+
fromFixture(`
93+
const store = withState(function() {});
94+
~~~~~~~~~~~~~ [${messageId} { "property": "Function" }]`),
95+
fromFixture(`
96+
const store = withState(() => {});
97+
~~~~~~~~ [${messageId} { "property": "Function" }]`),
4098
];
4199

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

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
"@ngrx/prefer-concat-latest-from": "error",
1515
"@ngrx/enforce-type-call": "error",
1616
"@ngrx/prefer-protected-state": "error",
17-
"@ngrx/signal-state-no-arrays-at-root-level": "error",
1817
"@ngrx/signal-store-feature-should-use-generic-type": "error",
1918
"@ngrx/avoid-combining-selectors": "error",
2019
"@ngrx/avoid-dispatching-multiple-actions-sequentially": "error",

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ export default (
3636
'@ngrx/prefer-concat-latest-from': 'error',
3737
'@ngrx/enforce-type-call': 'error',
3838
'@ngrx/prefer-protected-state': 'error',
39-
'@ngrx/signal-state-no-arrays-at-root-level': 'error',
4039
'@ngrx/signal-store-feature-should-use-generic-type': 'error',
4140
'@ngrx/avoid-combining-selectors': 'error',
4241
'@ngrx/avoid-dispatching-multiple-actions-sequentially': 'error',

modules/eslint-plugin/src/configs/signals.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
"rules": {
55
"@ngrx/enforce-type-call": "error",
66
"@ngrx/prefer-protected-state": "error",
7-
"@ngrx/signal-state-no-arrays-at-root-level": "error",
87
"@ngrx/signal-store-feature-should-use-generic-type": "error"
98
}
109
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ export default (
2626
rules: {
2727
'@ngrx/enforce-type-call': 'error',
2828
'@ngrx/prefer-protected-state': 'error',
29-
'@ngrx/signal-state-no-arrays-at-root-level': 'error',
3029
'@ngrx/signal-store-feature-should-use-generic-type': 'error',
3130
},
3231
},

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

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type TSESTree } from '@typescript-eslint/utils';
1+
import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils';
22
import * as path from 'path';
33
import { createRule } from '../../rule-creator';
44
import { isArrayExpression } from '../../utils';
@@ -8,17 +8,34 @@ export const messageId = 'signalStateNoArraysAtRootLevel';
88
type MessageIds = typeof messageId;
99
type Options = readonly [];
1010

11+
const NON_RECORD_TYPES = [
12+
'Array',
13+
'Set',
14+
'Map',
15+
'WeakSet',
16+
'WeakMap',
17+
'Date',
18+
'Error',
19+
'RegExp',
20+
'ArrayBuffer',
21+
'DataView',
22+
'Promise',
23+
'Function',
24+
];
25+
1126
export default createRule<Options, MessageIds>({
1227
name: path.parse(__filename).name,
1328
meta: {
1429
type: 'problem',
1530
docs: {
1631
description: `signalState should accept a record or dictionary as an input argument.`,
1732
ngrxModule: 'signals',
33+
requiresTypeChecking: true,
1834
},
1935
schema: [],
2036
messages: {
21-
[messageId]: `Wrap the array in an record or dictionary.`,
37+
[messageId]:
38+
'The property type `{{ property }}` is forbidden as the initial state argument, wrap the property in a record or dictionary.',
2239
},
2340
},
2441
defaultOptions: [],
@@ -32,7 +49,53 @@ export default createRule<Options, MessageIds>({
3249
context.report({
3350
node: argument,
3451
messageId,
52+
data: { property: 'Array' },
3553
});
54+
} else if (argument) {
55+
const services = ESLintUtils.getParserServices(context);
56+
const typeChecker = services.program.getTypeChecker();
57+
const type = services.getTypeAtLocation(argument);
58+
59+
if (typeChecker.isArrayType(type) || typeChecker.isTupleType(type)) {
60+
context.report({
61+
node: argument,
62+
messageId,
63+
data: { property: 'Array' },
64+
});
65+
return;
66+
}
67+
68+
const symbol = type.getSymbol();
69+
if (symbol && NON_RECORD_TYPES.includes(symbol.getName())) {
70+
context.report({
71+
node: argument,
72+
messageId,
73+
data: { property: symbol.getName() },
74+
});
75+
return;
76+
}
77+
78+
const callSignatures = type.getCallSignatures();
79+
if (callSignatures.length > 0) {
80+
context.report({
81+
node: argument,
82+
messageId,
83+
data: { property: 'Function' },
84+
});
85+
return;
86+
}
87+
88+
const typeString = typeChecker.typeToString(type);
89+
const matchedType = NON_RECORD_TYPES.find((t) =>
90+
typeString.startsWith(`${t}<`)
91+
);
92+
if (matchedType) {
93+
context.report({
94+
node: argument,
95+
messageId,
96+
data: { property: matchedType },
97+
});
98+
}
3699
}
37100
},
38101
};

0 commit comments

Comments
 (0)