@dsivd/prestations-ng 19.0.7 → 19.0.8
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/INTRODUCTION_ANGULAR_SIGNALS.md +503 -0
- package/dsivd-prestations-ng-19.0.8.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 +1 -2
- package/dsivd-prestations-ng-19.0.7.tgz +0 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classification of a signal member, shared by the TypeScript-API resolution of
|
|
3
|
+
* `signal-names.mjs` and the ESLint-AST checks of the rules, which read the same names out
|
|
4
|
+
* of two different trees.
|
|
5
|
+
*
|
|
6
|
+
* The value of a signal is never to be mutated, whatever its kind — a mutation notifies
|
|
7
|
+
* none of its dependents. `VIEW_QUERY` is the single exception: it hands out a DOM element
|
|
8
|
+
* or a component instance, and mutating that is the reason for asking for it.
|
|
9
|
+
*
|
|
10
|
+
* The kinds beyond `WRITABLE` differ only in the advice that applies, since none of them
|
|
11
|
+
* has a `.set()`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const WRITABLE = "writable";
|
|
15
|
+
export const COMPUTED = "computed";
|
|
16
|
+
export const INPUT = "input";
|
|
17
|
+
export const FROM_OBSERVABLE = "fromObservable";
|
|
18
|
+
export const VIEW_QUERY = "viewQuery";
|
|
19
|
+
// A read-only signal whose factory its declaration does not name: it may be a view query.
|
|
20
|
+
export const AMBIGUOUS = "ambiguous";
|
|
21
|
+
|
|
22
|
+
// `output()` is absent: an output is never called, it is emitted.
|
|
23
|
+
const KIND_BY_FACTORY = new Map([
|
|
24
|
+
["signal", WRITABLE],
|
|
25
|
+
["model", WRITABLE],
|
|
26
|
+
["linkedSignal", WRITABLE],
|
|
27
|
+
["computed", COMPUTED],
|
|
28
|
+
["input", INPUT],
|
|
29
|
+
["toSignal", FROM_OBSERVABLE],
|
|
30
|
+
["viewChild", VIEW_QUERY],
|
|
31
|
+
["viewChildren", VIEW_QUERY],
|
|
32
|
+
["contentChild", VIEW_QUERY],
|
|
33
|
+
["contentChildren", VIEW_QUERY],
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Declaration files qualify the types with the namespace they were imported under —
|
|
38
|
+
* `readonly icon: _angular_core.ModelSignal<IconProp>` — hence the comparison on the last
|
|
39
|
+
* segment only.
|
|
40
|
+
*
|
|
41
|
+
* `Signal<T>` is where a published declaration loses the information: a `computed()`, a
|
|
42
|
+
* `toSignal()` and a view query all end up spelled that way. It has to stay `AMBIGUOUS`,
|
|
43
|
+
* because this library alone publishes 54 view queries — `AbstractPageComponent` and
|
|
44
|
+
* `FoehnInputComponent` among them — and reporting them would break every project that
|
|
45
|
+
* extends one and touches the element it queried.
|
|
46
|
+
*/
|
|
47
|
+
const KIND_BY_TYPE = new Map([
|
|
48
|
+
["WritableSignal", WRITABLE],
|
|
49
|
+
["ModelSignal", WRITABLE],
|
|
50
|
+
["InputSignal", INPUT],
|
|
51
|
+
["InputSignalWithTransform", INPUT],
|
|
52
|
+
["Signal", AMBIGUOUS],
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
export const factoryKind = (name) => KIND_BY_FACTORY.get(name);
|
|
56
|
+
|
|
57
|
+
export const typeKind = (name) => KIND_BY_TYPE.get(name);
|
package/eslint/signal-names.mjs
CHANGED
|
@@ -2,8 +2,10 @@ import { readFileSync, statSync } from "node:fs";
|
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import ts from "typescript";
|
|
4
4
|
|
|
5
|
+
import { factoryKind, typeKind } from "./signal-kinds.mjs";
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
|
-
* Collects the signal
|
|
8
|
+
* Collects the signal members of a component, walking up its inheritance chain.
|
|
7
9
|
*
|
|
8
10
|
* Base classes are resolved automatically: the `extends` clause is matched against the
|
|
9
11
|
* imports of the file, the module is resolved the way TypeScript would (relative paths,
|
|
@@ -11,31 +13,6 @@ import ts from "typescript";
|
|
|
11
13
|
* to be declared by hand.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
|
-
// `output()` is absent: an output is never called, it is emitted.
|
|
15
|
-
const SIGNAL_FACTORIES = new Set([
|
|
16
|
-
"input",
|
|
17
|
-
"model",
|
|
18
|
-
"computed",
|
|
19
|
-
"signal",
|
|
20
|
-
"linkedSignal",
|
|
21
|
-
"toSignal",
|
|
22
|
-
"viewChild",
|
|
23
|
-
"viewChildren",
|
|
24
|
-
"contentChild",
|
|
25
|
-
"contentChildren",
|
|
26
|
-
]);
|
|
27
|
-
|
|
28
|
-
// Types of a signal member. Declaration files qualify them with the namespace they were
|
|
29
|
-
// imported under: `readonly icon: _angular_core.ModelSignal<IconProp>`, hence the
|
|
30
|
-
// comparison on the last segment only.
|
|
31
|
-
const SIGNAL_TYPES = new Set([
|
|
32
|
-
"Signal",
|
|
33
|
-
"WritableSignal",
|
|
34
|
-
"InputSignal",
|
|
35
|
-
"InputSignalWithTransform",
|
|
36
|
-
"ModelSignal",
|
|
37
|
-
]);
|
|
38
|
-
|
|
39
16
|
const parsedFiles = new Map();
|
|
40
17
|
const compilerOptionsByDirectory = new Map();
|
|
41
18
|
|
|
@@ -96,31 +73,33 @@ const resolveModule = (specifier, containingFile) =>
|
|
|
96
73
|
const lastSegmentOf = (entityName) =>
|
|
97
74
|
ts.isQualifiedName(entityName) ? entityName.right.text : entityName.text;
|
|
98
75
|
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
76
|
+
const kindOfType = (typeNode) =>
|
|
77
|
+
typeNode && ts.isTypeReferenceNode(typeNode)
|
|
78
|
+
? typeKind(lastSegmentOf(typeNode.typeName))
|
|
79
|
+
: undefined;
|
|
103
80
|
|
|
104
|
-
const
|
|
81
|
+
const kindOfInitializer = (expression) => {
|
|
105
82
|
if (!expression || !ts.isCallExpression(expression)) {
|
|
106
|
-
return
|
|
83
|
+
return undefined;
|
|
107
84
|
}
|
|
108
85
|
// `input.required<T>()` and `viewChild.required(…)` unwrap to their factory.
|
|
109
86
|
let callee = expression.expression;
|
|
110
87
|
while (ts.isPropertyAccessExpression(callee)) {
|
|
111
88
|
callee = callee.expression;
|
|
112
89
|
}
|
|
113
|
-
return ts.isIdentifier(callee)
|
|
90
|
+
return ts.isIdentifier(callee) ? factoryKind(callee.text) : undefined;
|
|
114
91
|
};
|
|
115
92
|
|
|
116
|
-
|
|
93
|
+
// Kind of signal a class member holds, `undefined` when it holds none. The initializer
|
|
94
|
+
// comes first: it names the factory, where the type may only say `Signal<T>`.
|
|
95
|
+
const kindOfMember = (member) => {
|
|
117
96
|
if (ts.isGetAccessor(member)) {
|
|
118
|
-
return
|
|
97
|
+
return kindOfType(member.type);
|
|
119
98
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
);
|
|
99
|
+
if (!ts.isPropertyDeclaration(member)) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
return kindOfInitializer(member.initializer) ?? kindOfType(member.type);
|
|
124
103
|
};
|
|
125
104
|
|
|
126
105
|
const findClass = (sourceFile, className) => {
|
|
@@ -174,7 +153,7 @@ const reExportedModulesOf = (sourceFile) =>
|
|
|
174
153
|
.filter((statement) => ts.isExportDeclaration(statement) && statement.moduleSpecifier)
|
|
175
154
|
.map((statement) => statement.moduleSpecifier.text);
|
|
176
155
|
|
|
177
|
-
const collect = (path, className,
|
|
156
|
+
const collect = (path, className, kinds, visited) => {
|
|
178
157
|
const key = `${path}::${className ?? "*"}`;
|
|
179
158
|
if (visited.has(key)) {
|
|
180
159
|
return;
|
|
@@ -195,7 +174,7 @@ const collect = (path, className, names, visited) => {
|
|
|
195
174
|
for (const specifier of reExportedModulesOf(sourceFile)) {
|
|
196
175
|
const target = resolveModule(specifier, path);
|
|
197
176
|
if (target) {
|
|
198
|
-
collect(target, className,
|
|
177
|
+
collect(target, className, kinds, visited);
|
|
199
178
|
}
|
|
200
179
|
}
|
|
201
180
|
return;
|
|
@@ -203,8 +182,13 @@ const collect = (path, className, names, visited) => {
|
|
|
203
182
|
|
|
204
183
|
for (const classDeclaration of classDeclarations) {
|
|
205
184
|
for (const member of classDeclaration.members) {
|
|
206
|
-
if (member.name
|
|
207
|
-
|
|
185
|
+
if (!member.name || !ts.isIdentifier(member.name)) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const kind = kindOfMember(member);
|
|
189
|
+
// A subclass overriding a member wins: it is collected first.
|
|
190
|
+
if (kind && !kinds.has(member.name.text)) {
|
|
191
|
+
kinds.set(member.name.text, kind);
|
|
208
192
|
}
|
|
209
193
|
}
|
|
210
194
|
|
|
@@ -214,19 +198,29 @@ const collect = (path, className, names, visited) => {
|
|
|
214
198
|
}
|
|
215
199
|
// Declaration bundles are flattened: the base class often sits in the same file.
|
|
216
200
|
if (findClass(sourceFile, baseName)) {
|
|
217
|
-
collect(path, baseName,
|
|
201
|
+
collect(path, baseName, kinds, visited);
|
|
218
202
|
continue;
|
|
219
203
|
}
|
|
220
204
|
const imported = importOf(sourceFile, baseName);
|
|
221
205
|
const target = imported && resolveModule(imported.specifier, path);
|
|
222
206
|
if (target) {
|
|
223
|
-
collect(target, imported.exportedName,
|
|
207
|
+
collect(target, imported.exportedName, kinds, visited);
|
|
224
208
|
}
|
|
225
209
|
}
|
|
226
210
|
};
|
|
227
211
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
212
|
+
/**
|
|
213
|
+
* Signal members of a component, mapped to their kind. `undefined` when the file
|
|
214
|
+
* cannot be read: unknown signals must be distinguished from a component that has none.
|
|
215
|
+
*/
|
|
216
|
+
export const collectSignalKinds = (componentPath) => {
|
|
217
|
+
if (!parseFile(componentPath)) {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
const kinds = new Map();
|
|
221
|
+
collect(componentPath, undefined, kinds, new Set());
|
|
222
|
+
return kinds;
|
|
232
223
|
};
|
|
224
|
+
|
|
225
|
+
export const collectSignalNames = (componentPath) =>
|
|
226
|
+
new Set(collectSignalKinds(componentPath)?.keys() ?? []);
|