@cplieger/deadset-ts 8.0.0-dev.2 → 8.0.0-dev.3
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/dist/src/analysis.js +27 -5
- package/dist/src/augmentations.js +62 -0
- package/dist/src/build-scripts.js +437 -0
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/convention-rows.js +39 -2
- package/dist/src/conventions.js +15 -0
- package/dist/src/dependencies.js +89 -0
- package/dist/src/entry-points.js +9 -18
- package/dist/src/findings/completion.js +62 -26
- package/dist/src/findings/intra-function.js +4 -2
- package/dist/src/findings/reads-and-writes.js +7 -6
- package/dist/src/findings/unused-declarations.js +4 -1
- package/dist/src/graph.js +5 -3
- package/dist/src/intra-function-parts.js +45 -17
- package/dist/src/kinds.js +7 -7
- package/dist/src/manifest.js +18 -0
- package/dist/src/matrix.js +38 -4
- package/dist/src/report.js +23 -4
- package/dist/src/reporters/sarif.js +12 -8
- package/dist/src/roots.js +138 -11
- package/dist/src/serialization-contract.js +258 -8
- package/dist/src/sweep.js +7 -9
- package/dist/src/test-globals.js +234 -0
- package/dist/src/type-errors.js +11 -3
- package/dist/src/unseen-loads.js +107 -0
- package/dist/src/verbs/analyze.js +24 -2
- package/dist/src/yaml-document.js +355 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +45 -7
- package/src/augmentations.ts +76 -1
- package/src/build-scripts.ts +564 -0
- package/src/conformance-record.ts +1 -1
- package/src/convention-rows.ts +47 -2
- package/src/conventions.ts +16 -0
- package/src/dependencies.ts +96 -0
- package/src/entry-points.ts +10 -25
- package/src/file-facts.ts +3 -0
- package/src/findings/completion.ts +72 -28
- package/src/findings/intra-function.ts +4 -2
- package/src/findings/reads-and-writes.ts +7 -6
- package/src/findings/unused-declarations.ts +6 -1
- package/src/graph.ts +5 -3
- package/src/intra-function-parts.ts +55 -16
- package/src/kinds.ts +7 -7
- package/src/manifest.ts +21 -3
- package/src/matrix.ts +51 -4
- package/src/report.ts +56 -16
- package/src/reporters/sarif.ts +16 -8
- package/src/roots.ts +162 -12
- package/src/serialization-contract.ts +309 -7
- package/src/sweep.ts +21 -9
- package/src/test-globals.ts +275 -0
- package/src/type-errors.ts +14 -3
- package/src/unseen-loads.ts +145 -0
- package/src/verbs/analyze.ts +28 -2
- package/src/yaml-document.ts +377 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* the project handing such a parameter to a destination is one, to a fixpoint, and a
|
|
7
7
|
* function invoked through `call`, `apply` or `bind` is the callee.
|
|
8
8
|
*/
|
|
9
|
-
import { isArrayLiteralExpression, isArrowFunction, isAsExpression, isCallExpression, isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isIdentifier, isInterfaceDeclaration, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyDeclaration, isSatisfiesExpression, isSetAccessorDeclaration, isSpreadElement, isStringLiteral, isTypeLiteralNode, isVariableDeclaration, ModifierFlags, NodeFlags, SyntaxKind, } from "@typescript/native/ast";
|
|
9
|
+
import { isArrayLiteralExpression, isArrowFunction, isAsExpression, isCallExpression, isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isFunctionDeclaration, isFunctionExpression, isFunctionLikeDeclaration, isGetAccessorDeclaration, isIdentifier, isInterfaceDeclaration, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isParenthesizedExpression, isForInStatement, isPropertyAccessExpression, isPropertyDeclaration, isSatisfiesExpression, isSetAccessorDeclaration, isSpreadElement, isStringLiteral, isTypeLiteralNode, isVariableDeclaration, ModifierFlags, NodeFlags, SyntaxKind, } from "@typescript/native/ast";
|
|
10
10
|
import { SignatureKind, SymbolFlags, TypeFlags, } from "@typescript/native/sync";
|
|
11
11
|
import { aliasChains } from "./alias-chain.js";
|
|
12
12
|
import { calleeName, resolvedTargets, valuesPassed } from "./calls.js";
|
|
@@ -173,9 +173,31 @@ function walkProject(project, held) {
|
|
|
173
173
|
}
|
|
174
174
|
return { sites, parameters };
|
|
175
175
|
}
|
|
176
|
+
/** The calls of `Object` that read every own property of their argument by name. */
|
|
177
|
+
const ENUMERATIONS = new Set([
|
|
178
|
+
"keys",
|
|
179
|
+
"entries",
|
|
180
|
+
"values",
|
|
181
|
+
"getOwnPropertyNames",
|
|
182
|
+
"assign",
|
|
183
|
+
]);
|
|
176
184
|
/**
|
|
177
|
-
*
|
|
178
|
-
*
|
|
185
|
+
* Whether a parameter type keeps no member name of the value it is given: `unknown`, `any`,
|
|
186
|
+
* or an object type of a string index signature and no property.
|
|
187
|
+
*/
|
|
188
|
+
function keepsNoName(project, type) {
|
|
189
|
+
if ((type.flags & ERASED) !== 0) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
if ((type.flags & TypeFlags.Object) === 0 ||
|
|
193
|
+
must(project.queries.propertiesOf(type)).length > 0) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
return must(project.queries.indexInfosOf(type)).some((info) => (info.keyType.flags & TypeFlags.String) !== 0);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* The parameters that keep no member name of the value they are given, as `keepsNoName`
|
|
200
|
+
* reads it, and a rest parameter whose elements keep none, each by its symbol.
|
|
179
201
|
*/
|
|
180
202
|
function erasedParameters(project, parameters) {
|
|
181
203
|
const types = typesAt(project, parameters.map((parameter) => parameter.name));
|
|
@@ -185,10 +207,10 @@ function erasedParameters(project, parameters) {
|
|
|
185
207
|
return false;
|
|
186
208
|
}
|
|
187
209
|
if (!parameter.rest) {
|
|
188
|
-
return (type
|
|
210
|
+
return keepsNoName(project, type);
|
|
189
211
|
}
|
|
190
212
|
const [element] = must(project.queries.typeArguments(type));
|
|
191
|
-
return element !== undefined && (element
|
|
213
|
+
return element !== undefined && keepsNoName(project, element);
|
|
192
214
|
});
|
|
193
215
|
const bySymbol = new Map();
|
|
194
216
|
project
|
|
@@ -202,6 +224,103 @@ function erasedParameters(project, parameters) {
|
|
|
202
224
|
});
|
|
203
225
|
return bySymbol;
|
|
204
226
|
}
|
|
227
|
+
/** Whether one node is a function, whose body runs only where something calls it. */
|
|
228
|
+
function isFunctionNode(node) {
|
|
229
|
+
return (isFunctionDeclaration(node) ||
|
|
230
|
+
isFunctionExpression(node) ||
|
|
231
|
+
isArrowFunction(node) ||
|
|
232
|
+
isMethodDeclaration(node) ||
|
|
233
|
+
isGetAccessorDeclaration(node) ||
|
|
234
|
+
isSetAccessorDeclaration(node));
|
|
235
|
+
}
|
|
236
|
+
/** The name a function nested in another is called by: its own, or the variable it initializes. */
|
|
237
|
+
function nestedName(fn) {
|
|
238
|
+
if (isFunctionDeclaration(fn)) {
|
|
239
|
+
return fn.name?.text;
|
|
240
|
+
}
|
|
241
|
+
let at = fn.parent;
|
|
242
|
+
while (isParenthesizedExpression(at) || isAsExpression(at) || isSatisfiesExpression(at)) {
|
|
243
|
+
at = at.parent;
|
|
244
|
+
}
|
|
245
|
+
return isVariableDeclaration(at) && isIdentifier(at.name) ? at.name.text : undefined;
|
|
246
|
+
}
|
|
247
|
+
/** The names one function's body writes as identifiers, other than the names its declarations bind. */
|
|
248
|
+
function namesWritten(fn) {
|
|
249
|
+
const found = new Set();
|
|
250
|
+
const visit = (node) => {
|
|
251
|
+
const declares = (isFunctionDeclaration(node.parent) || isVariableDeclaration(node.parent)) &&
|
|
252
|
+
node.parent.name === node;
|
|
253
|
+
if (isIdentifier(node) && !declares) {
|
|
254
|
+
found.add(node.text);
|
|
255
|
+
}
|
|
256
|
+
node.forEachChild(visit);
|
|
257
|
+
};
|
|
258
|
+
fn.forEachChild(visit);
|
|
259
|
+
return found;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* The erased parameters their own function enumerates: one an `Object.keys`, `entries`,
|
|
263
|
+
* `values`, `getOwnPropertyNames` or `assign` call takes, `Object` the standard library's,
|
|
264
|
+
* or a `for…in` statement walks, which reads every property of the value by its name. A
|
|
265
|
+
* nested function is read as the function's own only where its name is written in it, or
|
|
266
|
+
* it has none, a callback or a function called where it is written.
|
|
267
|
+
*/
|
|
268
|
+
function enumeratedParameters(project, erased) {
|
|
269
|
+
const asked = [];
|
|
270
|
+
for (const parameter of erased.values()) {
|
|
271
|
+
const fn = parameter.name.parent.parent;
|
|
272
|
+
const spelled = parameter.name.getText();
|
|
273
|
+
const written = namesWritten(fn);
|
|
274
|
+
const visit = (node) => {
|
|
275
|
+
if (isFunctionNode(node)) {
|
|
276
|
+
const name = nestedName(node);
|
|
277
|
+
if (name !== undefined && !written.has(name)) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const read = [];
|
|
282
|
+
let object;
|
|
283
|
+
if (isCallExpression(node)) {
|
|
284
|
+
const callee = node.expression;
|
|
285
|
+
if (isPropertyAccessExpression(callee) &&
|
|
286
|
+
isIdentifier(callee.expression) &&
|
|
287
|
+
callee.expression.text === "Object" &&
|
|
288
|
+
ENUMERATIONS.has(callee.name.text)) {
|
|
289
|
+
object = callee.expression;
|
|
290
|
+
// `Object.assign` reads its sources, every argument after the target.
|
|
291
|
+
read.push(...node.arguments.slice(...(callee.name.text === "assign" ? [1] : [0, 1])));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
else if (isForInStatement(node)) {
|
|
295
|
+
read.push(node.expression);
|
|
296
|
+
}
|
|
297
|
+
for (const value of read.map(evaluated)) {
|
|
298
|
+
if (isIdentifier(value) && value.text === spelled) {
|
|
299
|
+
asked.push({ node: value, parameter, object });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
node.forEachChild(visit);
|
|
303
|
+
};
|
|
304
|
+
fn.forEachChild(visit);
|
|
305
|
+
}
|
|
306
|
+
const symbols = project.symbolsAt(asked.map((one) => project.handle(one.node))).map(must);
|
|
307
|
+
const objects = asked.flatMap((one) => (one.object === undefined ? [] : [one.object]));
|
|
308
|
+
const own = project.ownPaths();
|
|
309
|
+
const standard = new Set(project
|
|
310
|
+
.symbolsAt(objects.map((node) => project.handle(node)))
|
|
311
|
+
.map(must)
|
|
312
|
+
.flatMap((symbol, at) => symbol?.declarations.every((handle) => !own.has(handle.path)) === true ? [objects[at]] : []));
|
|
313
|
+
return [
|
|
314
|
+
...new Set(asked.flatMap((one, at) => {
|
|
315
|
+
const symbol = symbols[at];
|
|
316
|
+
return symbol !== undefined &&
|
|
317
|
+
erased.get(symbol.id) === one.parameter &&
|
|
318
|
+
(one.object === undefined || standard.has(one.object))
|
|
319
|
+
? [one.parameter]
|
|
320
|
+
: [];
|
|
321
|
+
})),
|
|
322
|
+
];
|
|
323
|
+
}
|
|
205
324
|
/** Each place a call's argument passes one of the erased parameters it can see. */
|
|
206
325
|
function handedOn(project, sites, erased, invocationOf) {
|
|
207
326
|
const spellings = new Set([...erased.values()].map((parameter) => parameter.name.getText()));
|
|
@@ -285,8 +404,8 @@ function implementedOwners(project, inAnalysis) {
|
|
|
285
404
|
* callee is a serializer an entry names; at the arguments its resolved signature types
|
|
286
405
|
* `unknown` or `any` where its callee is declared outside the analysis or only ambiently
|
|
287
406
|
* with no implementation in it; and, where its callee is a function of the project, at each
|
|
288
|
-
* parameter the function hands on to a destination, holding back the widest
|
|
289
|
-
*
|
|
407
|
+
* parameter the function enumerates or hands on to a destination, holding back the widest
|
|
408
|
+
* of what those hold back, settled by widening until no parameter moves.
|
|
290
409
|
*/
|
|
291
410
|
function destinations(input, chains, walked) {
|
|
292
411
|
const { project, held } = input;
|
|
@@ -478,6 +597,9 @@ function destinations(input, chains, walked) {
|
|
|
478
597
|
}
|
|
479
598
|
return undefined;
|
|
480
599
|
};
|
|
600
|
+
for (const parameter of enumeratedParameters(project, erased)) {
|
|
601
|
+
forwards.set(keyOf(parameter), "properties");
|
|
602
|
+
}
|
|
481
603
|
const forwarded = handedOn(project, walked.sites, erased, invocation);
|
|
482
604
|
for (let moved = true; moved;) {
|
|
483
605
|
moved = false;
|
|
@@ -551,6 +673,14 @@ function flowsOf(project, held, sites, to, stores) {
|
|
|
551
673
|
},
|
|
552
674
|
];
|
|
553
675
|
});
|
|
676
|
+
const generic = typeParameterFlows(project, reach, sites, arguments_.flatMap((one) => {
|
|
677
|
+
const type = typeOf.get(one.value);
|
|
678
|
+
const parameters = type === undefined ? [] : typeParametersOf(project, type, 0);
|
|
679
|
+
const destination = parameters.length === 0 ? undefined : to.at(one.site.call, one.argument);
|
|
680
|
+
return destination === undefined
|
|
681
|
+
? []
|
|
682
|
+
: parameters.map((parameter) => ({ parameter, to: destination }));
|
|
683
|
+
}));
|
|
554
684
|
const left = stores.outside.flatMap((store) => {
|
|
555
685
|
const reached = reach.reachedFrom(store.type);
|
|
556
686
|
return reached.classes.size === 0 && reached.members.length === 0
|
|
@@ -565,7 +695,127 @@ function flowsOf(project, held, sites, to, stores) {
|
|
|
565
695
|
},
|
|
566
696
|
];
|
|
567
697
|
});
|
|
568
|
-
return [...passed, ...left];
|
|
698
|
+
return [...passed, ...generic, ...left];
|
|
699
|
+
}
|
|
700
|
+
/** The type parameters one type names, through constituents and type arguments. */
|
|
701
|
+
function typeParametersOf(project, type, depth) {
|
|
702
|
+
if (type.isTypeParameter()) {
|
|
703
|
+
return [type];
|
|
704
|
+
}
|
|
705
|
+
if (depth >= MENTION_DEPTH) {
|
|
706
|
+
return [];
|
|
707
|
+
}
|
|
708
|
+
const { queries } = project;
|
|
709
|
+
return [
|
|
710
|
+
...(type.isUnionType() || type.isIntersectionType() ? must(queries.constituents(type)) : []),
|
|
711
|
+
...must(queries.typeArguments(type)),
|
|
712
|
+
].flatMap((one) => typeParametersOf(project, one, depth + 1));
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* The types an instantiated type puts where a declared type names `parameter`: matched
|
|
716
|
+
* through type arguments, and in a union every constituent the declared union does not
|
|
717
|
+
* itself name.
|
|
718
|
+
*/
|
|
719
|
+
function bound(project, declared, instantiated, parameter, depth) {
|
|
720
|
+
if (declared.id === parameter.id) {
|
|
721
|
+
return [instantiated];
|
|
722
|
+
}
|
|
723
|
+
if (depth >= MENTION_DEPTH) {
|
|
724
|
+
return [];
|
|
725
|
+
}
|
|
726
|
+
const { queries } = project;
|
|
727
|
+
if (declared.isUnionType()) {
|
|
728
|
+
const parts = must(queries.constituents(declared));
|
|
729
|
+
if (parts.some((part) => part.id === parameter.id)) {
|
|
730
|
+
const kept = new Set(parts.map((part) => part.id));
|
|
731
|
+
const given = instantiated.isUnionType()
|
|
732
|
+
? must(queries.constituents(instantiated))
|
|
733
|
+
: [instantiated];
|
|
734
|
+
return given.filter((part) => !kept.has(part.id));
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
const declaredArguments = must(queries.typeArguments(declared));
|
|
738
|
+
const givenArguments = must(queries.typeArguments(instantiated));
|
|
739
|
+
return declaredArguments.length !== givenArguments.length
|
|
740
|
+
? []
|
|
741
|
+
: declaredArguments.flatMap((one, at) => {
|
|
742
|
+
const given = givenArguments[at];
|
|
743
|
+
return given === undefined ? [] : bound(project, one, given, parameter, depth + 1);
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* The flows a type parameter of a function of the project carries where a value its type
|
|
748
|
+
* names reaches a destination: each call of the function passes its type argument there,
|
|
749
|
+
* written or inferred from the arguments, and a type argument that is a type parameter
|
|
750
|
+
* of the calling function is followed on to that function's calls, to a fixpoint.
|
|
751
|
+
*/
|
|
752
|
+
function typeParameterFlows(project, reach, sites, pending) {
|
|
753
|
+
const { queries } = project;
|
|
754
|
+
const byName = new Map();
|
|
755
|
+
for (const site of sites) {
|
|
756
|
+
const name = calleeName(site.call.expression)?.getText();
|
|
757
|
+
if (name !== undefined) {
|
|
758
|
+
byName.set(name, [...(byName.get(name) ?? []), site]);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
const found = [];
|
|
762
|
+
const seen = new Set();
|
|
763
|
+
for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
|
|
764
|
+
const { parameter, to } = next;
|
|
765
|
+
if (seen.has(parameter.id)) {
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
seen.add(parameter.id);
|
|
769
|
+
const symbol = must(queries.symbolOfType(parameter));
|
|
770
|
+
const [handle] = symbol?.declarations ?? [];
|
|
771
|
+
const declaration = handle === undefined ? undefined : project.declarationAt(handle)?.node;
|
|
772
|
+
const fn = declaration?.parent;
|
|
773
|
+
const index = fn === undefined || !isFunctionLikeDeclaration(fn)
|
|
774
|
+
? -1
|
|
775
|
+
: (fn.typeParameters ?? []).findIndex((one) => one === declaration);
|
|
776
|
+
const name = fn === undefined
|
|
777
|
+
? undefined
|
|
778
|
+
: isMethodDeclaration(fn) && isIdentifier(fn.name)
|
|
779
|
+
? fn.name.text
|
|
780
|
+
: nestedName(fn);
|
|
781
|
+
if (symbol === undefined || fn === undefined || index < 0 || name === undefined) {
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
for (const site of byName.get(name) ?? []) {
|
|
785
|
+
const resolved = must(queries.resolvedSignature(site.call));
|
|
786
|
+
const declared = resolved === undefined ? undefined : must(queries.genericOf(resolved));
|
|
787
|
+
const called = declared?.declaration === undefined
|
|
788
|
+
? undefined
|
|
789
|
+
: project.declarationAt(declared.declaration)?.node;
|
|
790
|
+
if (resolved === undefined || declared === undefined || called !== fn) {
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
const written = site.call.typeArguments?.[index];
|
|
794
|
+
const given = written === undefined
|
|
795
|
+
? site.call.arguments.flatMap((_argument, at) => {
|
|
796
|
+
const from = must(queries.parameterType(declared, at));
|
|
797
|
+
const into = must(queries.parameterType(resolved, at));
|
|
798
|
+
return from === undefined || into === undefined
|
|
799
|
+
? []
|
|
800
|
+
: bound(project, from, into, parameter, 0);
|
|
801
|
+
})
|
|
802
|
+
: typesAt(project, [written]).filter((type) => type !== undefined);
|
|
803
|
+
for (const type of given) {
|
|
804
|
+
const reached = reach.reachedFrom(type);
|
|
805
|
+
if (reached.classes.size > 0 || reached.members.length > 0) {
|
|
806
|
+
found.push({
|
|
807
|
+
site: site.call,
|
|
808
|
+
classes: reached.classes,
|
|
809
|
+
members: reached.members,
|
|
810
|
+
to,
|
|
811
|
+
detail: `passed to ${to.callee} through the type parameter ${symbol.name}`,
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
pending.push(...typeParametersOf(project, type, 0).map((one) => ({ parameter: one, to })));
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return found;
|
|
569
819
|
}
|
|
570
820
|
/**
|
|
571
821
|
* The serialization-contract detector: each data member and `toJSON` of each class,
|
package/dist/src/sweep.js
CHANGED
|
@@ -84,10 +84,10 @@ export function runnerCalls(symbol) {
|
|
|
84
84
|
* That run's roots are the graph's own and, of each test file, its top level and every
|
|
85
85
|
* function it exports, which a test runner may call, and every export of a manual mock,
|
|
86
86
|
* which the runner reads; any other export of a test file, and test-support code, which
|
|
87
|
-
* runs where a test imports it, are judged as any file's are.
|
|
88
|
-
* `
|
|
87
|
+
* runs where a test imports it, are judged as any file's are. `held` seeds it as a mark
|
|
88
|
+
* seeds a sweep; with `namespaces` false a file read reaches none of the file's exports.
|
|
89
89
|
*/
|
|
90
|
-
export function reachedWithTheTests(graph, held) {
|
|
90
|
+
export function reachedWithTheTests(graph, held, namespaces = true) {
|
|
91
91
|
const tests = graph.symbols.flatMap((symbol, at) => {
|
|
92
92
|
const exported = symbol.exported && graph.symbols[graph.parent[at] ?? OUTSIDE]?.kind === "file";
|
|
93
93
|
const called = exported && runnerCalls(symbol);
|
|
@@ -97,9 +97,7 @@ export function reachedWithTheTests(graph, held) {
|
|
|
97
97
|
? [{ at, kind: "test-runner" }]
|
|
98
98
|
: [];
|
|
99
99
|
});
|
|
100
|
-
return reachable({ ...graph, rooted: [...graph.rooted, ...tests] }, flagged(graph, held), {
|
|
101
|
-
production: false,
|
|
102
|
-
});
|
|
100
|
+
return reachable({ ...graph, rooted: [...graph.rooted, ...tests] }, flagged(graph, held), { production: false }, namespaces);
|
|
103
101
|
}
|
|
104
102
|
/**
|
|
105
103
|
* The references a loaded consumer makes that the mode counts. A production mode counts
|
|
@@ -126,7 +124,7 @@ function flagged(graph, ids) {
|
|
|
126
124
|
* included, while a root reaches through the references the mode counts. The held seed
|
|
127
125
|
* runs first, so what it reaches is expanded under the wider rule.
|
|
128
126
|
*/
|
|
129
|
-
function reachable(graph, held, mode) {
|
|
127
|
+
function reachable(graph, held, mode, namespaces = true) {
|
|
130
128
|
const reached = graph.symbols.map(() => false);
|
|
131
129
|
const expanded = graph.symbols.map(() => false);
|
|
132
130
|
const queue = [];
|
|
@@ -154,7 +152,7 @@ function reachable(graph, held, mode) {
|
|
|
154
152
|
if (!holding && mode.production && edge.test) {
|
|
155
153
|
continue;
|
|
156
154
|
}
|
|
157
|
-
enter(edge.to, !edge.evaluation);
|
|
155
|
+
enter(edge.to, namespaces && !edge.evaluation);
|
|
158
156
|
}
|
|
159
157
|
}
|
|
160
158
|
};
|
|
@@ -169,7 +167,7 @@ function reachable(graph, held, mode) {
|
|
|
169
167
|
}
|
|
170
168
|
// A consumer's reference reaches what it names as one of the target's own does.
|
|
171
169
|
for (const edge of callsOf(graph, mode)) {
|
|
172
|
-
enter(edge.to, !edge.evaluation);
|
|
170
|
+
enter(edge.to, namespaces && !edge.evaluation);
|
|
173
171
|
}
|
|
174
172
|
walk(false);
|
|
175
173
|
return reached;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The globals a test runner injects into every file it runs, which no import names: a
|
|
3
|
+
* Vitest configuration whose `test.globals` is `true` gives each test and each file it
|
|
4
|
+
* loads `describe`, `it`, `expect` and `vi` (https://vitest.dev/config/globals). A program
|
|
5
|
+
* analyzed without the runner's declarations of them reads every use as an error, so a
|
|
6
|
+
* configuration under such a runner is read with them; and a file that calls one is
|
|
7
|
+
* written for the runner, as one that imports it is.
|
|
8
|
+
*/
|
|
9
|
+
import { isAsExpression, isBinaryExpression, isCallExpression, isExportAssignment, isExpressionStatement, isIdentifier, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, SyntaxKind, } from "@typescript/native/ast";
|
|
10
|
+
import { dirnamePath, joinPath } from "./paths.js";
|
|
11
|
+
import { UNANSWERED } from "./query.js";
|
|
12
|
+
import { writtenPath } from "./workspace-programs.js";
|
|
13
|
+
/** The test runner whose configuration injects the globals, and the `types` entry declaring them. */
|
|
14
|
+
export const GLOBALS_RUNNER = "vitest";
|
|
15
|
+
const GLOBALS_TYPES = "vitest/globals";
|
|
16
|
+
/** The configuration files the runner reads its settings from, by name. */
|
|
17
|
+
const RUNNER_CONFIG = /^(?:vitest|vite)\.config\.(?:js|mjs|cjs|ts|mts|cts)$/u;
|
|
18
|
+
/** The globals `vitest/globals` declares that a test or a helper calls or reads a member of. */
|
|
19
|
+
const GLOBAL_NAMES = new Set([
|
|
20
|
+
"afterAll",
|
|
21
|
+
"afterEach",
|
|
22
|
+
"assert",
|
|
23
|
+
"assertType",
|
|
24
|
+
"beforeAll",
|
|
25
|
+
"beforeEach",
|
|
26
|
+
"bench",
|
|
27
|
+
"describe",
|
|
28
|
+
"expect",
|
|
29
|
+
"expectTypeOf",
|
|
30
|
+
"it",
|
|
31
|
+
"onTestFailed",
|
|
32
|
+
"onTestFinished",
|
|
33
|
+
"suite",
|
|
34
|
+
"test",
|
|
35
|
+
"vi",
|
|
36
|
+
"vitest",
|
|
37
|
+
]);
|
|
38
|
+
/** The expression one wrapper that changes no value holds, and a call's first argument. */
|
|
39
|
+
function configObject(node) {
|
|
40
|
+
let at = node;
|
|
41
|
+
while (at !== undefined) {
|
|
42
|
+
if (isParenthesizedExpression(at) || isAsExpression(at) || isSatisfiesExpression(at)) {
|
|
43
|
+
at = at.expression;
|
|
44
|
+
}
|
|
45
|
+
else if (isCallExpression(at)) {
|
|
46
|
+
at = at.arguments[0];
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
return at;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
/** The value one object literal writes for one member as `name: value`. */
|
|
55
|
+
function member(object, name) {
|
|
56
|
+
if (object === undefined || !isObjectLiteralExpression(object)) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
for (const property of object.properties) {
|
|
60
|
+
if (isPropertyAssignment(property) &&
|
|
61
|
+
isIdentifier(property.name) &&
|
|
62
|
+
property.name.text === name) {
|
|
63
|
+
return configObject(property.initializer);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
/** Whether one runner configuration's default export sets `test.globals` to `true`. */
|
|
69
|
+
function setsGlobals(file) {
|
|
70
|
+
for (const statement of file.statements) {
|
|
71
|
+
let exported;
|
|
72
|
+
if (isExportAssignment(statement)) {
|
|
73
|
+
exported = statement.expression;
|
|
74
|
+
}
|
|
75
|
+
else if (isExpressionStatement(statement) && isBinaryExpression(statement.expression)) {
|
|
76
|
+
const { left, operatorToken, right } = statement.expression;
|
|
77
|
+
if (operatorToken.kind === SyntaxKind.EqualsToken &&
|
|
78
|
+
isPropertyAccessExpression(left) &&
|
|
79
|
+
isIdentifier(left.expression) &&
|
|
80
|
+
left.expression.text === "module" &&
|
|
81
|
+
isIdentifier(left.name) &&
|
|
82
|
+
left.name.text === "exports") {
|
|
83
|
+
exported = right;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const object = exported === undefined ? undefined : configObject(exported);
|
|
87
|
+
const globals = member(member(object, "test"), "globals");
|
|
88
|
+
if (globals?.kind === SyntaxKind.TrueKeyword) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
/** Whether the runner's configuration in one manifest's directory injects the globals. */
|
|
95
|
+
function injectsGlobals(host, parse, dir) {
|
|
96
|
+
let names;
|
|
97
|
+
try {
|
|
98
|
+
names = host
|
|
99
|
+
.readDirectory(dir)
|
|
100
|
+
.filter((entry) => !entry.directory && RUNNER_CONFIG.test(entry.name))
|
|
101
|
+
.map((entry) => entry.name);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
return names.some((name) => {
|
|
107
|
+
const path = joinPath(dir, name);
|
|
108
|
+
try {
|
|
109
|
+
return setsGlobals(parse(path, host.readFile(path)));
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/** Whether a file declaring the globals is installed where the configuration resolves packages from. */
|
|
117
|
+
function globalsInstalled(host, configFile) {
|
|
118
|
+
for (let dir = dirnamePath(configFile);; dir = dirnamePath(dir)) {
|
|
119
|
+
if (host.kindOf(joinPath(dir, "node_modules", GLOBALS_RUNNER, "globals.d.ts")) === "file") {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
if (dirnamePath(dir) === dir) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** The directory of the manifest nearest one configuration file, at or above its own. */
|
|
128
|
+
function manifestDir(host, configFile) {
|
|
129
|
+
for (let dir = dirnamePath(configFile);; dir = dirnamePath(dir)) {
|
|
130
|
+
if (host.kindOf(joinPath(dir, "package.json")) === "file") {
|
|
131
|
+
return dir;
|
|
132
|
+
}
|
|
133
|
+
if (dirnamePath(dir) === dir) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function typesOf(value) {
|
|
139
|
+
return Array.isArray(value) && value.every((one) => typeof one === "string") ? value : undefined;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* How the snapshot reads each configuration of `configFiles` whose nearest manifest's
|
|
143
|
+
* runner configuration injects the globals and that can resolve their declarations: as
|
|
144
|
+
* `programs` reads it, else as itself, with the globals' declarations added to its `types`.
|
|
145
|
+
* A configuration the compiler cannot read is left for its project to refuse.
|
|
146
|
+
*/
|
|
147
|
+
export function withTestGlobals(engine, host, parse, configFiles, programs) {
|
|
148
|
+
const found = new Map(programs);
|
|
149
|
+
const injects = new Map();
|
|
150
|
+
for (const configFile of configFiles) {
|
|
151
|
+
const dir = manifestDir(host, configFile);
|
|
152
|
+
if (dir === undefined || !globalsInstalled(host, configFile)) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (!injects.has(dir)) {
|
|
156
|
+
injects.set(dir, injectsGlobals(host, parse, dir));
|
|
157
|
+
}
|
|
158
|
+
if (injects.get(dir) !== true) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
let base;
|
|
162
|
+
try {
|
|
163
|
+
base = typesOf(engine.parseConfigFile(configFile).options.types) ?? [];
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const held = found.get(configFile);
|
|
169
|
+
if (held?.kind === "program") {
|
|
170
|
+
const listed = typesOf(held.params.compilerOptions.types) ?? base;
|
|
171
|
+
if (!listed.includes(GLOBALS_TYPES)) {
|
|
172
|
+
found.set(configFile, {
|
|
173
|
+
kind: "program",
|
|
174
|
+
params: {
|
|
175
|
+
...held.params,
|
|
176
|
+
compilerOptions: { ...held.params.compilerOptions, types: [...listed, GLOBALS_TYPES] },
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
let written = { extends: configFile };
|
|
183
|
+
if (held !== undefined) {
|
|
184
|
+
try {
|
|
185
|
+
written = JSON.parse(held.text);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const options = (written["compilerOptions"] ?? {});
|
|
192
|
+
const listed = typesOf(options["types"]) ?? base;
|
|
193
|
+
if (listed.includes(GLOBALS_TYPES)) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
found.set(configFile, {
|
|
197
|
+
kind: "configuration",
|
|
198
|
+
file: held?.file ?? writtenPath(host, configFile),
|
|
199
|
+
text: `${JSON.stringify({ ...written, compilerOptions: { ...options, types: [...listed, GLOBALS_TYPES] } }, null, 2)}\n`,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return found;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Whether one file names a global the runner injects, which is how a file written for the
|
|
206
|
+
* runner's globals reads: a call of one of the names or a read of a member of it that
|
|
207
|
+
* resolves to a declaration outside the project's own files, the runner's declaration
|
|
208
|
+
* file. A name the file binds, imported, declared or a parameter, resolves to its own.
|
|
209
|
+
*/
|
|
210
|
+
export function namesTestGlobals(project, file) {
|
|
211
|
+
const named = [];
|
|
212
|
+
const visit = (node) => {
|
|
213
|
+
const callee = isCallExpression(node)
|
|
214
|
+
? node.expression
|
|
215
|
+
: isPropertyAccessExpression(node)
|
|
216
|
+
? node.expression
|
|
217
|
+
: undefined;
|
|
218
|
+
if (callee !== undefined && isIdentifier(callee) && GLOBAL_NAMES.has(callee.text)) {
|
|
219
|
+
named.push(callee);
|
|
220
|
+
}
|
|
221
|
+
node.forEachChild(visit);
|
|
222
|
+
};
|
|
223
|
+
file.forEachChild(visit);
|
|
224
|
+
if (named.length === 0) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
const own = project.ownPaths();
|
|
228
|
+
return project
|
|
229
|
+
.symbolsAt(named.map((node) => project.handle(node)))
|
|
230
|
+
.some((symbol) => symbol === UNANSWERED ||
|
|
231
|
+
(symbol !== undefined &&
|
|
232
|
+
symbol.declarations.length > 0 &&
|
|
233
|
+
symbol.declarations.every((handle) => !own.has(handle.path))));
|
|
234
|
+
}
|
package/dist/src/type-errors.js
CHANGED
|
@@ -13,6 +13,12 @@ import { isAnswered } from "./query.js";
|
|
|
13
13
|
import { findWorkspace } from "./workspace.js";
|
|
14
14
|
/** The compiler's codes for an import whose module it cannot find. */
|
|
15
15
|
const MODULE_NOT_FOUND = new Set([2307, 2792]);
|
|
16
|
+
/**
|
|
17
|
+
* The compiler's codes for where the configuration places a file, which leave every type
|
|
18
|
+
* the file's statements carry as it is: an output that would overwrite an input (5055,
|
|
19
|
+
* 5056), a file outside `rootDir` (6059), one a composite project does not list (6307).
|
|
20
|
+
*/
|
|
21
|
+
const CONFIGURATION_LEVEL = new Set([5055, 5056, 6059, 6307]);
|
|
16
22
|
const MANIFEST = "package.json";
|
|
17
23
|
const MANIFEST_SECTIONS = [
|
|
18
24
|
"dependencies",
|
|
@@ -204,8 +210,8 @@ function messageOf(text, targetRoot) {
|
|
|
204
210
|
}
|
|
205
211
|
/**
|
|
206
212
|
* Reads the errors one project's own files carry. A diagnostic marking unnecessary code
|
|
207
|
-
* is no error here, and one in a file that
|
|
208
|
-
* package's to answer for.
|
|
213
|
+
* or about where the configuration places a file is no error here, and one in a file that
|
|
214
|
+
* is not the project's own is that file's package's to answer for.
|
|
209
215
|
*/
|
|
210
216
|
export function readErrors(project, errors, where) {
|
|
211
217
|
let workspace = null;
|
|
@@ -223,7 +229,9 @@ export function readErrors(project, errors, where) {
|
|
|
223
229
|
const skips = [];
|
|
224
230
|
for (const { diagnostic, at } of errors) {
|
|
225
231
|
const file = diagnostic.fileName === undefined ? undefined : files.get(diagnostic.fileName);
|
|
226
|
-
if (file === undefined ||
|
|
232
|
+
if (file === undefined ||
|
|
233
|
+
diagnostic.reportsUnnecessary === true ||
|
|
234
|
+
CONFIGURATION_LEVEL.has(diagnostic.code)) {
|
|
227
235
|
continue;
|
|
228
236
|
}
|
|
229
237
|
const path = relativePath(context.targetRoot, file.fileName) ?? file.fileName;
|