@dsivd/prestations-ng 19.0.5-beta.4 → 19.0.5-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ANGULAR_SIGNALS.md +174 -16
- package/UPGRADING_V19.md +8 -1
- package/dsivd-prestations-ng-19.0.5-beta.6.tgz +0 -0
- package/eslint/index.mjs +7 -0
- package/eslint/rules/__tests__/no-direct-signal-mutation.test.mjs +98 -0
- package/eslint/rules/index.mjs +1 -0
- package/eslint/rules/no-direct-signal-mutation.mjs +175 -0
- package/fesm2022/dsivd-prestations-ng.mjs +11 -11
- package/fesm2022/dsivd-prestations-ng.mjs.map +1 -1
- package/package.json +5 -1
- package/src/eslint/rules/__tests__/no-direct-signal-mutation.test.mjs +4 -4
- package/src/eslint/rules/no-direct-signal-mutation.mjs +136 -13
- package/types/dsivd-prestations-ng.d.ts +5 -6
- package/dsivd-prestations-ng-19.0.5-beta.4.tgz +0 -0
package/ANGULAR_SIGNALS.md
CHANGED
|
@@ -47,11 +47,16 @@ increment(): void {
|
|
|
47
47
|
}
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
| Method | Description
|
|
51
|
-
| ------------- |
|
|
52
|
-
| `.set(value)` | Replace the value
|
|
53
|
-
| `.update(fn)` | Derive next value from current
|
|
54
|
-
|
|
50
|
+
| Method | Description |
|
|
51
|
+
| ------------- | ------------------------------ |
|
|
52
|
+
| `.set(value)` | Replace the value |
|
|
53
|
+
| `.update(fn)` | Derive next value from current |
|
|
54
|
+
|
|
55
|
+
> ⚠️ `.mutate()` is **not** part of the public API. It was part of the
|
|
56
|
+
> original signals proposal but was removed from `WritableSignal` before
|
|
57
|
+
> the stable release — mutating an object without a `.set()`/`.update()`
|
|
58
|
+
> call is invisible to the signal graph. Always produce a new reference
|
|
59
|
+
> via `.update()` (see [pitfall #8](angular-signals#common_pitfalls)).
|
|
55
60
|
|
|
56
61
|
---
|
|
57
62
|
|
|
@@ -302,8 +307,10 @@ readonly selectedOption = linkedSignal<string[], string>({
|
|
|
302
307
|
});
|
|
303
308
|
```
|
|
304
309
|
|
|
305
|
-
`previous` contains `{ value:
|
|
306
|
-
|
|
310
|
+
`previous` contains `{ source: S; value: D }` — the previous value of the
|
|
311
|
+
`source` signal **and** the previous value of the linked signal itself, or
|
|
312
|
+
`undefined` on first run. Explicit generic type arguments (`<S, D>`) are
|
|
313
|
+
required as soon as you use `previous`.
|
|
307
314
|
|
|
308
315
|
---
|
|
309
316
|
|
|
@@ -393,12 +400,15 @@ Usage in a parent template:
|
|
|
393
400
|
<!-- two-way -->
|
|
394
401
|
<my-component [(isVisible)]="open" />
|
|
395
402
|
|
|
396
|
-
<!-- one-way —
|
|
403
|
+
<!-- one-way binding — compiles fine, but changes made by the -->
|
|
404
|
+
<!-- component are not propagated back to `open` -->
|
|
397
405
|
<my-component [isVisible]="open" />
|
|
398
406
|
```
|
|
399
407
|
|
|
400
|
-
> ⚠️
|
|
401
|
-
>
|
|
408
|
+
> ⚠️ A one-way `[binding]` on a `model()` input is valid and compiles —
|
|
409
|
+
> it's up to the consumer to decide whether they use the input, the
|
|
410
|
+
> output, or both. It's just pointless if the component never needs to
|
|
411
|
+
> write back: use `input()` instead to communicate intent clearly.
|
|
402
412
|
|
|
403
413
|
> ⚠️ `model()` holds object values by reference. Mutating a nested property
|
|
404
414
|
> of a `model()`'s value (e.g. `this.address().locality.name = 'x'`) does
|
|
@@ -828,15 +838,24 @@ effect(() => {
|
|
|
828
838
|
});
|
|
829
839
|
```
|
|
830
840
|
|
|
831
|
-
### 8. Mutating a
|
|
841
|
+
### 8. Mutating a signal's value instead of reassigning
|
|
832
842
|
|
|
833
843
|
This is the signals-era version of "mutating `@Input()` used to work."
|
|
834
844
|
Pre-signals, Zone.js-driven change detection re-read template expressions
|
|
835
|
-
on virtually every async event, so mutating a nested property
|
|
836
|
-
CD "catch it eventually" appeared to work. Signals
|
|
837
|
-
`.set()`/`.update()` —
|
|
838
|
-
|
|
839
|
-
depending on it
|
|
845
|
+
on virtually every async event, so mutating a nested property or an array
|
|
846
|
+
in place and letting CD "catch it eventually" appeared to work. Signals
|
|
847
|
+
only notify subscribers on `.set()`/`.update()` — any in-place mutation of
|
|
848
|
+
the value returned by `signal()`/`model()`/`input()` is invisible to the
|
|
849
|
+
signal, the template, and any `effect()`/`computed()` depending on it,
|
|
850
|
+
because the object/array reference never changes and signals compare by
|
|
851
|
+
reference by default.
|
|
852
|
+
|
|
853
|
+
The fix is always the same shape: read the current value, produce a
|
|
854
|
+
**new top-level reference** with the change applied, and pass that to
|
|
855
|
+
`.set()` or `.update()`. Never call a mutating method or assign a nested
|
|
856
|
+
property directly on `signalOrModel()`.
|
|
857
|
+
|
|
858
|
+
#### 8a. Nested object property
|
|
840
859
|
|
|
841
860
|
```ts
|
|
842
861
|
// 🔴 Silent no-op from the signal's perspective
|
|
@@ -848,3 +867,142 @@ this.address.update((current) => ({
|
|
|
848
867
|
locality: { ...current.locality, name: 'newName' },
|
|
849
868
|
}));
|
|
850
869
|
```
|
|
870
|
+
|
|
871
|
+
#### 8b. Two-way binding directly on an array element inside `@for`
|
|
872
|
+
|
|
873
|
+
Binding `[(model)]` straight to an indexed read of a signal
|
|
874
|
+
(`model()[i]`) is a variant of the same mistake: the "write" side of the
|
|
875
|
+
banana-in-a-box syntax tries to assign into `model()[i]`, which mutates
|
|
876
|
+
the array in place instead of going through `.set()`/`.update()`, so
|
|
877
|
+
nothing propagates and Angular may also warn about an unsupported
|
|
878
|
+
two-way expression.
|
|
879
|
+
|
|
880
|
+
```html
|
|
881
|
+
<!-- 🔴 Tries to write through model()[i] — mutates in place, breaks reactivity -->
|
|
882
|
+
@for (item of model(); track item.id; let i = $index) {
|
|
883
|
+
<app-address-row [(model)]="model()[i]" />
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
<!-- 🟢 One-way in, explicit handler out -->
|
|
887
|
+
@for (item of model(); track item.id; let i = $index) {
|
|
888
|
+
<app-address-row [model]="model()[i]" (modelChange)="updateItem(i, $event)" />
|
|
889
|
+
}
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
```ts
|
|
893
|
+
updateItem(index: number, newValue: NomAdresse): void {
|
|
894
|
+
this.model.update((current) =>
|
|
895
|
+
current.map((item, i) => (i === index ? newValue : item)),
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
#### 8c. Array mutating methods (`push`, `splice`, `pop`, `shift`, `unshift`, `sort`, `reverse`)
|
|
901
|
+
|
|
902
|
+
All of these mutate the array in place and return either the mutated
|
|
903
|
+
array, the removed element(s), or nothing — never a new reference. Since
|
|
904
|
+
they operate on `this.model()` (the _value_, not the signal), Angular has
|
|
905
|
+
no way to know the array changed.
|
|
906
|
+
|
|
907
|
+
```ts
|
|
908
|
+
// 🔴 Adds an item, but the signal reference is untouched — no re-render
|
|
909
|
+
this.model().push({ name: null, address: null });
|
|
910
|
+
|
|
911
|
+
// 🟢 New array reference via spread
|
|
912
|
+
this.model.update((current) => [...current, { name: null, address: null }]);
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
```ts
|
|
916
|
+
// 🔴 Removes an item in place — invisible to the signal
|
|
917
|
+
this.model().splice(index, 1);
|
|
918
|
+
|
|
919
|
+
// 🟢 filter() naturally returns a new array
|
|
920
|
+
this.model.update((current) => current.filter((_, i) => i !== index));
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
```ts
|
|
924
|
+
// 🔴 Same problem with pop() / shift() / unshift()
|
|
925
|
+
this.model().pop();
|
|
926
|
+
this.model().shift();
|
|
927
|
+
this.model().unshift(newItem);
|
|
928
|
+
|
|
929
|
+
// 🟢 Equivalent immutable forms
|
|
930
|
+
this.model.update((current) => current.slice(0, -1)); // pop
|
|
931
|
+
this.model.update((current) => current.slice(1)); // shift
|
|
932
|
+
this.model.update((current) => [newItem, ...current]); // unshift
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
```ts
|
|
936
|
+
// 🔴 sort() / reverse() mutate AND return the same array reference —
|
|
937
|
+
// even risky to combine with spread if you forget it: [...current].sort()
|
|
938
|
+
// mutates the *new* array in place, which is fine, but sorting
|
|
939
|
+
// `this.model()` directly still mutates the one held by the signal
|
|
940
|
+
this.model().sort((a, b) => a.name.localeCompare(b.name));
|
|
941
|
+
|
|
942
|
+
// 🟢 Copy first, then sort/reverse the copy
|
|
943
|
+
this.model.update((current) =>
|
|
944
|
+
[...current].sort((a, b) => a.name.localeCompare(b.name)),
|
|
945
|
+
);
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
#### 8d. Direct index assignment
|
|
949
|
+
|
|
950
|
+
```ts
|
|
951
|
+
// 🔴 Mutates the array element in place
|
|
952
|
+
this.model()[index] = newValue;
|
|
953
|
+
|
|
954
|
+
// 🟢 map() to swap out just that index
|
|
955
|
+
this.model.update((current) =>
|
|
956
|
+
current.map((item, i) => (i === index ? newValue : item)),
|
|
957
|
+
);
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
#### 8e. `Object.assign()` on the current value
|
|
961
|
+
|
|
962
|
+
`Object.assign(target, ...)` mutates and returns `target`. Passing
|
|
963
|
+
`this.model()` as the target mutates the value in place, even though the
|
|
964
|
+
call "looks" like it's producing something.
|
|
965
|
+
|
|
966
|
+
```ts
|
|
967
|
+
// 🔴 target is this.model() itself — mutated in place
|
|
968
|
+
Object.assign(this.model(), { name: 'newName' });
|
|
969
|
+
|
|
970
|
+
// 🟢 target is a fresh object — this is what Object.assign() is for
|
|
971
|
+
this.model.update((current) => Object.assign({}, current, { name: 'newName' }));
|
|
972
|
+
// or, more idiomatically:
|
|
973
|
+
this.model.update((current) => ({ ...current, name: 'newName' }));
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
#### 8f. `Map` / `Set` values
|
|
977
|
+
|
|
978
|
+
`Map` and `Set` are reference types too, and their mutating methods
|
|
979
|
+
(`.set()` on a `Map`, `.add()`/`.delete()` on a `Set`) don't return a new
|
|
980
|
+
`Map`/`Set` — they mutate and return `this`, which is easy to miss since
|
|
981
|
+
`Map.prototype.set` shares a name with `WritableSignal.set`.
|
|
982
|
+
|
|
983
|
+
```ts
|
|
984
|
+
// 🔴 This is Map.prototype.set, not the signal's — mutates the same Map instance
|
|
985
|
+
this.cache().set(key, value);
|
|
986
|
+
|
|
987
|
+
// 🟢 Build a new Map from the old one
|
|
988
|
+
this.cache.update((current) => new Map(current).set(key, value));
|
|
989
|
+
```
|
|
990
|
+
|
|
991
|
+
```ts
|
|
992
|
+
// 🔴 Set.prototype.delete mutates in place
|
|
993
|
+
this.selectedIds().delete(id);
|
|
994
|
+
|
|
995
|
+
// 🟢 Copy into a new Set, then mutate the copy
|
|
996
|
+
this.selectedIds.update((current) => {
|
|
997
|
+
const next = new Set(current);
|
|
998
|
+
next.delete(id);
|
|
999
|
+
return next;
|
|
1000
|
+
});
|
|
1001
|
+
```
|
|
1002
|
+
|
|
1003
|
+
> 💡 General rule of thumb: if a method's name suggests it changes
|
|
1004
|
+
> something "in place" and doesn't hand you back a brand-new
|
|
1005
|
+
> object/array/Map/Set, assume it mutates and wrap it in a copy first.
|
|
1006
|
+
> When unsure, check the return type — a genuinely immutable operation
|
|
1007
|
+
> (`map`, `filter`, `concat`, `slice`, spread) always returns a fresh
|
|
1008
|
+
> reference you can pass straight to `.set()`/`.update()`.
|
package/UPGRADING_V19.md
CHANGED
|
@@ -1092,7 +1092,7 @@ import rxjs from '@smarttools/eslint-plugin-rxjs';
|
|
|
1092
1092
|
import simpleImportSort from 'eslint-plugin-simple-import-sort';
|
|
1093
1093
|
import prettier from 'eslint-config-prettier';
|
|
1094
1094
|
import importX from 'eslint-plugin-import-x';
|
|
1095
|
-
|
|
1095
|
+
import prestationsNgEslint from '@dsivd/prestations-ng/eslint';
|
|
1096
1096
|
|
|
1097
1097
|
export default tseslint.config(
|
|
1098
1098
|
{
|
|
@@ -1123,6 +1123,7 @@ export default tseslint.config(
|
|
|
1123
1123
|
rxjs,
|
|
1124
1124
|
'simple-import-sort': simpleImportSort,
|
|
1125
1125
|
'import-x': importX,
|
|
1126
|
+
'@dsivd/prestations-ng': prestationsNgEslint,
|
|
1126
1127
|
},
|
|
1127
1128
|
processor: angular.processInlineTemplates,
|
|
1128
1129
|
rules: {
|
|
@@ -1143,6 +1144,9 @@ export default tseslint.config(
|
|
|
1143
1144
|
style: 'camelCase',
|
|
1144
1145
|
},
|
|
1145
1146
|
],
|
|
1147
|
+
'@angular-eslint/no-uncalled-signals': ['error'],
|
|
1148
|
+
'@angular-eslint/prefer-signal-model': ['error'],
|
|
1149
|
+
'@angular-eslint/prefer-signals': ['error'],
|
|
1146
1150
|
|
|
1147
1151
|
// ── @typescript-eslint ───────────────────────────────────────────
|
|
1148
1152
|
'@typescript-eslint/explicit-function-return-type': [
|
|
@@ -1229,6 +1233,9 @@ export default tseslint.config(
|
|
|
1229
1233
|
// ── simple-import-sort ───────────────────────────────────────────
|
|
1230
1234
|
'simple-import-sort/imports': 'error',
|
|
1231
1235
|
'simple-import-sort/exports': 'error',
|
|
1236
|
+
|
|
1237
|
+
// ── prestations-ng ──────────────────────────────────────────────
|
|
1238
|
+
'@dsivd/prestations-ng/no-direct-signal-mutation': 'error',
|
|
1232
1239
|
},
|
|
1233
1240
|
},
|
|
1234
1241
|
|
|
Binary file
|
package/eslint/index.mjs
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, it } from 'vitest';
|
|
2
|
+
import { RuleTester } from 'eslint';
|
|
3
|
+
import rule from '../no-direct-signal-mutation.mjs';
|
|
4
|
+
|
|
5
|
+
describe('no-direct-signal-mutation', () => {
|
|
6
|
+
const ruleTester = new RuleTester({
|
|
7
|
+
languageOptions: {
|
|
8
|
+
ecmaVersion: 2020,
|
|
9
|
+
sourceType: 'module',
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should validate signal mutation rule', () => {
|
|
14
|
+
ruleTester.run('no-direct-signal-mutation', rule, {
|
|
15
|
+
valid: [
|
|
16
|
+
// Using .update()
|
|
17
|
+
'model.update((current) => ({ ...current, property: value }))',
|
|
18
|
+
'this.model.update((current) => ({ ...current, property: value }))',
|
|
19
|
+
'getSignal().update((current) => ({ ...current, property: value }))',
|
|
20
|
+
|
|
21
|
+
// Using .set()
|
|
22
|
+
'model.set({ property: value })',
|
|
23
|
+
'this.model.set({ property: value })',
|
|
24
|
+
'getSignal().set({ property: value })',
|
|
25
|
+
|
|
26
|
+
// Regular property assignments
|
|
27
|
+
'this.property = value',
|
|
28
|
+
'obj.property = value',
|
|
29
|
+
'this.obj.property = value',
|
|
30
|
+
|
|
31
|
+
// Reading from function calls (no assignment)
|
|
32
|
+
'model().property',
|
|
33
|
+
'this.model().property',
|
|
34
|
+
'getSignal().property',
|
|
35
|
+
|
|
36
|
+
// Method calls on function results
|
|
37
|
+
'model().someMethod()',
|
|
38
|
+
'this.model().someMethod()',
|
|
39
|
+
'getSignal().doSomething()',
|
|
40
|
+
|
|
41
|
+
// Assignments to non-function results
|
|
42
|
+
'let x = value',
|
|
43
|
+
'const obj = { prop: value }',
|
|
44
|
+
'array[0] = value',
|
|
45
|
+
'pendingFilesByFormKey.get(formKey).files = []',
|
|
46
|
+
'this.pendingFilesByFormKey.get(formKey).url = baseUrl',
|
|
47
|
+
'const autocompleteComponent = viewChild("auto"); autocompleteComponent().inputElement().nativeElement.hidden = true',
|
|
48
|
+
'component.inputElement().nativeElement.value = ""',
|
|
49
|
+
|
|
50
|
+
// Method chaining without assignment
|
|
51
|
+
'model().update((v) => v).subscribe()',
|
|
52
|
+
|
|
53
|
+
// One-way bindings are OK
|
|
54
|
+
'[model]="model().property"',
|
|
55
|
+
'[ngModel]="model().comment"',
|
|
56
|
+
],
|
|
57
|
+
|
|
58
|
+
invalid: [
|
|
59
|
+
{
|
|
60
|
+
code: "model().property = value",
|
|
61
|
+
errors: [{ messageId: 'directMutation' }],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
code: "this.model().property = value",
|
|
65
|
+
errors: [{ messageId: 'directMutation' }],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
code: "getSignal().property = value",
|
|
69
|
+
errors: [{ messageId: 'directMutation' }],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
code: "model().nested.property = value",
|
|
73
|
+
errors: [{ messageId: 'directMutation' }],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
code: "this.model().deeply.nested.property = value",
|
|
77
|
+
errors: [{ messageId: 'directMutation' }],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
code: "model().property += value",
|
|
81
|
+
errors: [{ messageId: 'directMutation' }],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
code: "model().property -= value",
|
|
85
|
+
errors: [{ messageId: 'directMutation' }],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
code: "model().property *= value",
|
|
89
|
+
errors: [{ messageId: 'directMutation' }],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
code: "model().array[0] = value",
|
|
93
|
+
errors: [{ messageId: 'directMutation' }],
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as noDirectSignalMutation } from './no-direct-signal-mutation.mjs';
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule to detect direct mutations of signal/getter results.
|
|
3
|
+
* Warns against patterns like: model().property = value
|
|
4
|
+
* Suggests using .update() or .set() instead.
|
|
5
|
+
*/
|
|
6
|
+
export default {
|
|
7
|
+
meta: {
|
|
8
|
+
type: "problem",
|
|
9
|
+
docs: {
|
|
10
|
+
description:
|
|
11
|
+
"Warn on direct property mutations of function call results (signals, getters, etc.)",
|
|
12
|
+
category: "Best Practices",
|
|
13
|
+
recommended: true,
|
|
14
|
+
},
|
|
15
|
+
messages: {
|
|
16
|
+
directMutation:
|
|
17
|
+
"Direct mutation of function call result detected. Use .update() or .set() instead of direct assignment.",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
create(context) {
|
|
22
|
+
const sourceCode = context.sourceCode;
|
|
23
|
+
const viewQueryFactoryNames = new Set([
|
|
24
|
+
"viewChild",
|
|
25
|
+
"viewChildren",
|
|
26
|
+
"contentChild",
|
|
27
|
+
"contentChildren",
|
|
28
|
+
]);
|
|
29
|
+
const viewQueryAccessorNames = collectViewQueryAccessorNames(sourceCode.ast);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
AssignmentExpression(node) {
|
|
33
|
+
if (node.left.type !== "MemberExpression") {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const rootCallExpression = getFirstCallExpression(node.left.object);
|
|
38
|
+
if (rootCallExpression && isSignalLikeRootCall(rootCallExpression)) {
|
|
39
|
+
context.report({
|
|
40
|
+
node,
|
|
41
|
+
messageId: "directMutation",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function isSignalLikeRootCall(node) {
|
|
48
|
+
if (node.arguments.length !== 0) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// model().prop = ... (local signal-like accessor)
|
|
53
|
+
if (node.callee.type === "Identifier") {
|
|
54
|
+
return !viewQueryAccessorNames.has(node.callee.name);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// this.model().prop = ... (component/service signal accessor)
|
|
58
|
+
// component.model().prop = ... should not be assumed to be a signal read.
|
|
59
|
+
if (node.callee.type === "MemberExpression") {
|
|
60
|
+
if (node.callee.object?.type !== "ThisExpression") {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return !isViewQueryAccessorCall(node.callee);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isViewQueryAccessorCall(callee) {
|
|
70
|
+
const calledName = getCalledName(callee);
|
|
71
|
+
return calledName ? viewQueryAccessorNames.has(calledName) : false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getCalledName(callee) {
|
|
75
|
+
if (!callee) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
if (callee.type === "Identifier") {
|
|
79
|
+
return callee.name;
|
|
80
|
+
}
|
|
81
|
+
if (
|
|
82
|
+
callee.type === "MemberExpression" &&
|
|
83
|
+
!callee.computed &&
|
|
84
|
+
callee.property.type === "Identifier"
|
|
85
|
+
) {
|
|
86
|
+
return callee.property.name;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Finds the first/root function call used as the base of a mutation chain.
|
|
93
|
+
* Examples:
|
|
94
|
+
* - model().property = x => model()
|
|
95
|
+
* - this.model().deep.prop = x => this.model()
|
|
96
|
+
* - this.query().el().x = y => this.query()
|
|
97
|
+
*/
|
|
98
|
+
function getFirstCallExpression(node) {
|
|
99
|
+
if (!node) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
if (node.type === "MemberExpression") {
|
|
103
|
+
return getFirstCallExpression(node.object);
|
|
104
|
+
}
|
|
105
|
+
if (node.type === "CallExpression") {
|
|
106
|
+
if (node.callee.type === "MemberExpression") {
|
|
107
|
+
const nestedCall = getFirstCallExpression(node.callee.object);
|
|
108
|
+
return nestedCall || node;
|
|
109
|
+
}
|
|
110
|
+
return node;
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function collectViewQueryAccessorNames(ast) {
|
|
116
|
+
const names = new Set();
|
|
117
|
+
|
|
118
|
+
walkAst(ast, (node) => {
|
|
119
|
+
if (
|
|
120
|
+
node.type === "VariableDeclarator" &&
|
|
121
|
+
node.id?.type === "Identifier" &&
|
|
122
|
+
isViewQueryFactoryCall(node.init)
|
|
123
|
+
) {
|
|
124
|
+
names.add(node.id.name);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (
|
|
129
|
+
(node.type === "PropertyDefinition" || node.type === "ClassProperty") &&
|
|
130
|
+
node.key?.type === "Identifier" &&
|
|
131
|
+
isViewQueryFactoryCall(node.value)
|
|
132
|
+
) {
|
|
133
|
+
names.add(node.key.name);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return names;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isViewQueryFactoryCall(node) {
|
|
141
|
+
return (
|
|
142
|
+
node?.type === "CallExpression" &&
|
|
143
|
+
node.callee?.type === "Identifier" &&
|
|
144
|
+
viewQueryFactoryNames.has(node.callee.name)
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function walkAst(node, visitor, visited = new WeakSet()) {
|
|
149
|
+
if (!node || typeof node !== "object") {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (visited.has(node)) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
visited.add(node);
|
|
156
|
+
|
|
157
|
+
visitor(node);
|
|
158
|
+
|
|
159
|
+
for (const key of Object.keys(node)) {
|
|
160
|
+
// ESLint AST nodes include cyclic parent references.
|
|
161
|
+
if (key === "parent") {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const child = node[key];
|
|
165
|
+
if (Array.isArray(child)) {
|
|
166
|
+
for (const element of child) {
|
|
167
|
+
walkAst(element, visitor, visited);
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
walkAst(child, visitor, visited);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
};
|