@dsivd/prestations-ng 19.0.7 → 19.0.8-beta.1
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/CHANGELOG.md +27 -1
- package/ESLINT_PLUGIN.md +131 -17
- package/dsivd-prestations-ng-19.0.8-beta.1.tgz +0 -0
- package/eslint/component-path.mjs +15 -0
- package/eslint/rules/no-direct-signal-mutation.mjs +370 -142
- package/eslint/rules/no-uninvoked-signal-in-template.mjs +1 -12
- package/eslint/signal-kinds.mjs +57 -0
- package/eslint/signal-names.mjs +43 -49
- package/fesm2022/dsivd-prestations-ng.mjs +487 -487
- package/fesm2022/dsivd-prestations-ng.mjs.map +1 -1
- package/package.json +1 -1
- package/src/eslint/component-path.mjs +15 -0
- package/src/eslint/rules/__tests__/no-direct-signal-mutation.test.mjs +365 -128
- package/src/eslint/rules/no-direct-signal-mutation.mjs +370 -142
- package/src/eslint/rules/no-uninvoked-signal-in-template.mjs +1 -12
- package/src/eslint/signal-kinds.mjs +57 -0
- package/src/eslint/signal-names.mjs +43 -49
- package/types/dsivd-prestations-ng.d.ts +0 -1
- package/dsivd-prestations-ng-19.0.7.tgz +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,33 @@
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [19.0.8]
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- ESLint rule [`no-direct-signal-mutation`](ESLINT_PLUGIN.md#no-direct-signal-mutation) now
|
|
14
|
+
covers much more than the plain assignment it started with. 🚨 Expect new errors on
|
|
15
|
+
`npm update`; each can be turned off from your own `rules` block.
|
|
16
|
+
- **in-place mutators** are reported as well, since they notify nothing either: those of
|
|
17
|
+
`Array` (`splice`, `push`, `pop`, `shift`, `unshift`, `sort`, `reverse`, `fill`,
|
|
18
|
+
`copyWithin`) and of `Map` / `Set` (`set`, `add`, `delete`, `clear`)
|
|
19
|
+
- so are `delete model().users[0]` and `Object.assign(model(), …)`, and
|
|
20
|
+
`items()["push"](x)` counts as `items().push(x)`
|
|
21
|
+
- the value is **followed through the bindings that can be resolved exactly**: a
|
|
22
|
+
variable written once, the element parameter of an iteration callback, a `@for` item
|
|
23
|
+
and a `@let`. `@for (user of model().users)` then `user.done = true` is reported
|
|
24
|
+
- 🚨 **no signal is mutable**, whatever its kind: `signal()`, `model()`,
|
|
25
|
+
`linkedSignal()`, `computed()`, `input()` and `toSignal()` are all reported, each with
|
|
26
|
+
the fix that applies to it. A **view query** is the single exception —
|
|
27
|
+
`viewChild()`, `contentChildren()` and their `.required` variants hand out a DOM
|
|
28
|
+
element or a component instance, which is there to be mutated
|
|
29
|
+
- only the signals the class really declares are inspected, resolved through the whole
|
|
30
|
+
inheritance chain, so a plain method (`this.getConfig().items.push(x)`) is never
|
|
31
|
+
reported
|
|
32
|
+
- a signal inherited from a published package and known by its type only (`Signal<T>`)
|
|
33
|
+
is left alone: a declaration file no longer says whether it was a `computed()` or a
|
|
34
|
+
view query
|
|
35
|
+
|
|
9
36
|
## [19.0.7]
|
|
10
37
|
|
|
11
38
|
### Added
|
|
@@ -44,7 +71,6 @@
|
|
|
44
71
|
- [recaptcha.service.ts](projects/prestations-ng/src/sdk-recaptcha/recaptcha.service.ts)
|
|
45
72
|
- fixed recaptcha called infinitely when backend says that it must be revalidated
|
|
46
73
|
|
|
47
|
-
|
|
48
74
|
## [19.0.0] - should be aligned with prestations-be 19.0.x
|
|
49
75
|
|
|
50
76
|
### Added
|
package/ESLINT_PLUGIN.md
CHANGED
|
@@ -50,8 +50,8 @@ from the config. Inline templates written in a `@Component` are covered too, thr
|
|
|
50
50
|
|
|
51
51
|
| Config | Target | Contents |
|
|
52
52
|
| ----------------------------- | ----------- | -------------------------------------------------------------- |
|
|
53
|
-
| `configs.tsRecommended` | `**/*.ts` | the baselines, the house style, and `tsRules`
|
|
54
|
-
| `configs.templateRecommended` | `**/*.html` | the baselines, the house style, and `templateRules`
|
|
53
|
+
| `configs.tsRecommended` | `**/*.ts` | the baselines, the house style, and `tsRules` |
|
|
54
|
+
| `configs.templateRecommended` | `**/*.html` | the baselines, the house style, and `templateRules` |
|
|
55
55
|
| `configs.tsRules` | `**/*.ts` | `no-direct-signal-mutation` |
|
|
56
56
|
| `configs.templateRules` | `**/*.html` | `no-direct-signal-mutation`, `no-uninvoked-signal-in-template` |
|
|
57
57
|
|
|
@@ -96,8 +96,9 @@ your own `rules` block. It is applied after the shared config, so it wins:
|
|
|
96
96
|
|
|
97
97
|
### `no-direct-signal-mutation`
|
|
98
98
|
|
|
99
|
-
Reports
|
|
100
|
-
|
|
99
|
+
Reports a mutation of the value held by a signal, in either of its two forms: an
|
|
100
|
+
assignment into one of its members, or a call to an in-place mutator. Mutating that value
|
|
101
|
+
does not notify the signal's dependents, so the view is not refreshed.
|
|
101
102
|
|
|
102
103
|
Applies to **TypeScript files and templates**, including the case where the mutation hides
|
|
103
104
|
in a two-way binding.
|
|
@@ -105,14 +106,20 @@ in a two-way binding.
|
|
|
105
106
|
```ts
|
|
106
107
|
// ✗ mutates the object inside the signal, no notification
|
|
107
108
|
this.model().property = value;
|
|
109
|
+
this.model().items.splice(index, 1);
|
|
108
110
|
|
|
109
111
|
// ✓
|
|
110
112
|
this.model.update((current) => ({ ...current, property: value }));
|
|
113
|
+
this.model.update((current) => ({
|
|
114
|
+
...current,
|
|
115
|
+
items: current.items.filter((_, i) => i !== index),
|
|
116
|
+
}));
|
|
111
117
|
```
|
|
112
118
|
|
|
113
119
|
```html
|
|
114
120
|
<!-- ✗ -->
|
|
115
121
|
<button (click)="model().property = value">…</button>
|
|
122
|
+
<button (click)="model().items.splice(index, 1)">…</button>
|
|
116
123
|
<input [(ngModel)]="model().property" />
|
|
117
124
|
|
|
118
125
|
<!-- ✓ -->
|
|
@@ -122,23 +129,130 @@ this.model.update((current) => ({ ...current, property: value }));
|
|
|
122
129
|
All the assignment operators accepted by the Angular expression parser are covered: `=`,
|
|
123
130
|
`+=`, `-=`, `*=`, `/=`, `%=`, `**=`, `&&=`, `||=`, `??=`.
|
|
124
131
|
|
|
125
|
-
|
|
132
|
+
The in-place mutators covered are those of `Array` — `push`, `pop`, `shift`, `unshift`,
|
|
133
|
+
`splice`, `sort`, `reverse`, `fill`, `copyWithin` — and of `Map` / `Set` — `set`, `add`,
|
|
134
|
+
`delete`, `clear`. Their non-mutating counterparts, `toSorted`, `toReversed`, `filter`,
|
|
135
|
+
`concat`, `with`, are never reported: they return a new value. `items()["push"](x)` is the
|
|
136
|
+
same call as `items().push(x)` and is reported alike.
|
|
126
137
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
138
|
+
Two other ways of writing into the value are covered, in TypeScript — the Angular template
|
|
139
|
+
parser accepts neither:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
delete this.model().users[0];
|
|
143
|
+
Object.assign(this.model(), { property: value });
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
#### Names bound to the value
|
|
147
|
+
|
|
148
|
+
A mutation is rarely written on the signal read itself. The rule follows the value through
|
|
149
|
+
the bindings it can resolve exactly, and only those:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
// ✗ a variable written once
|
|
153
|
+
const users = this.model().users;
|
|
154
|
+
users.push(user);
|
|
155
|
+
|
|
156
|
+
// ✗ the element parameter of an iteration callback
|
|
157
|
+
this.model().users.forEach((user) => (user.done = true));
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```angular-html
|
|
161
|
+
<!-- ✗ a @for item, bound only inside its own block -->
|
|
162
|
+
@for (user of model().users; track user.id) {
|
|
163
|
+
<input [(ngModel)]="user.done" />
|
|
164
|
+
<button (click)="user.tags.push(tag)">…</button>
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
<!-- ✗ a @let -->
|
|
168
|
+
@let users = model().users;
|
|
169
|
+
<button (click)="users.push(user)">…</button>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
In TypeScript these come from ESLint's scope analysis, in templates from the block that
|
|
173
|
+
introduces the name — nothing is inferred from a naming convention. A variable that is
|
|
174
|
+
**reassigned** is dropped: the name may hold something else by the time it is mutated. Same
|
|
175
|
+
for `reduce`, whose first parameter is the accumulator and not an element.
|
|
176
|
+
|
|
177
|
+
**What it cannot follow** is a value crossing a function boundary — handed to a method,
|
|
178
|
+
stored in a class field, returned and mutated elsewhere:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
// not reported: the rule cannot know what `sortInto` does
|
|
182
|
+
this.sortInto(this.model().users);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
That needs interprocedural analysis, which ESLint does not provide; a full type checker
|
|
186
|
+
would be required. It is the one remaining gap in detection.
|
|
187
|
+
|
|
188
|
+
#### Only real signals are inspected
|
|
189
|
+
|
|
190
|
+
The rule does not guess from the shape of a call. It reads the declaring class — the linted
|
|
191
|
+
file itself, or the component a template belongs to — and resolves its signal members
|
|
192
|
+
through the whole inheritance chain, the way TypeScript would: relative paths,
|
|
193
|
+
`node_modules`, `paths` mappings, and the flattened `.d.ts` of a published library. Nothing
|
|
194
|
+
has to be declared by hand.
|
|
195
|
+
|
|
196
|
+
**No signal's value is to be mutated, whatever its kind** — a mutation notifies none of its
|
|
197
|
+
dependents. The kinds differ only in the advice the message carries, since none but the
|
|
198
|
+
first has a `.set()`:
|
|
199
|
+
|
|
200
|
+
| Declared as | On a mutation of its value |
|
|
201
|
+
| --------------------------------------- | ----------------------------------------------------------- |
|
|
202
|
+
| `signal()`, `model()`, `linkedSignal()` | use `.set()` / `.update()` |
|
|
203
|
+
| `computed()` | the value is derived: mutate the signal it is computed from |
|
|
204
|
+
| `input()` | the value belongs to the parent: ask through an `output()` |
|
|
205
|
+
| `toSignal()` | the value comes from the observable: change it upstream |
|
|
206
|
+
| `viewChild()`, `contentChildren()`, … | **the one exception**, left alone: see below |
|
|
207
|
+
|
|
208
|
+
A view query is the exception because it hands out a DOM element or a component instance,
|
|
209
|
+
and mutating that is the reason for asking for it: `rows().nativeElement.hidden = true` is
|
|
210
|
+
the normal way to use one. `.required` variants included.
|
|
211
|
+
|
|
212
|
+
Anything that is not a declared signal is left alone as well:
|
|
213
|
+
|
|
214
|
+
| Expression | Why |
|
|
215
|
+
| ------------------------------------ | ------------------------------------------------------------ |
|
|
216
|
+
| `this.getConfig().items.push(item)` | `getConfig()` is a plain method, not a signal |
|
|
217
|
+
| `map.get(key).files.push(file)` | the call takes arguments, so it is not a signal read |
|
|
218
|
+
| `component.model().items.push(item)` | reached through a foreign receiver, not the component itself |
|
|
219
|
+
|
|
220
|
+
Because the rule knows a real signal from an ordinary method, `set` / `add` / `delete` are
|
|
221
|
+
safe to report: `byId().set(k, v)` mutates the `Map` inside the signal, while `byId.set(m)`
|
|
222
|
+
replaces the signal's value and is correct.
|
|
223
|
+
|
|
224
|
+
**One accepted false positive.** A `computed()` may legitimately hand out a live mutable
|
|
225
|
+
object, and no type information is available to tell that apart from a derived value:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
readonly items = computed(() => this.form.get('items') as FormArray);
|
|
229
|
+
this.items().push(control); // reported, though this is the FormArray API
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**One accepted gap, and it is forced.** A published declaration file spells a `computed()`,
|
|
233
|
+
a `toSignal()` and a view query alike, `Signal<T>` — the factory name is gone. A signal
|
|
234
|
+
**inherited from a package** and known by its type only is therefore left alone:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
// in the published .d.ts, indistinguishable from one another
|
|
238
|
+
readonly fieldId: Signal<string>; // was a computed()
|
|
239
|
+
readonly inputElement: Signal<ElementRef>; // was a viewChild()
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Reporting that group would break the lint of every project extending a component of this
|
|
243
|
+
library: `prestations-ng` publishes 54 view queries, `AbstractPageComponent` and
|
|
244
|
+
`FoehnInputComponent` among them, and touching the element they queried is normal code. So
|
|
245
|
+
a mutation of an inherited `computed()` goes unreported — the narrower harm of the two.
|
|
246
|
+
Signals declared in your own project, base classes included, are unaffected: their factory
|
|
247
|
+
call is right there to be read.
|
|
133
248
|
|
|
134
249
|
The rule has no autofix: the correct rewrite depends on what you meant.
|
|
135
250
|
|
|
136
|
-
**
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
which is read-only as well.
|
|
251
|
+
**When the declaring class cannot be read** — an unsaved buffer, a template with no twin
|
|
252
|
+
`.ts` — its signals are unknown, and the rule falls back to treating any argument-less
|
|
253
|
+
accessor on the component as a signal. It may then report a plain method. That direction is
|
|
254
|
+
deliberate: a false positive costs one `eslint-disable-next-line`, a missed mutation is a
|
|
255
|
+
silent bug.
|
|
142
256
|
|
|
143
257
|
### `no-uninvoked-signal-in-template`
|
|
144
258
|
|
|
Binary file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Path of the component a linted template belongs to, `undefined` when the file is not a
|
|
2
|
+
// template.
|
|
3
|
+
//
|
|
4
|
+
// An inline template is a virtual block whose path is derived from the `.ts`, and whose
|
|
5
|
+
// name ends with `.html` too: `foo.component.ts/1_inline-template-….component.html`.
|
|
6
|
+
// It has to be matched BEFORE the external template, otherwise it resolves to garbage.
|
|
7
|
+
export function resolveComponentPath(filename) {
|
|
8
|
+
const inlineMatch = filename.match(/^(.*\.ts)(?:[/\\]|$)/);
|
|
9
|
+
if (inlineMatch) {
|
|
10
|
+
return inlineMatch[1];
|
|
11
|
+
}
|
|
12
|
+
return filename.endsWith(".html")
|
|
13
|
+
? `${filename.slice(0, -".html".length)}.ts`
|
|
14
|
+
: undefined;
|
|
15
|
+
}
|