@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
package/src/roots.ts
CHANGED
|
@@ -9,8 +9,17 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
|
+
isArrowFunction,
|
|
13
|
+
isAssertionExpression,
|
|
12
14
|
isExpressionWithTypeArguments,
|
|
13
15
|
isFunctionLikeDeclaration,
|
|
16
|
+
isIdentifier,
|
|
17
|
+
isMethodDeclaration,
|
|
18
|
+
isObjectLiteralExpression,
|
|
19
|
+
isParenthesizedExpression,
|
|
20
|
+
isPropertyAssignment,
|
|
21
|
+
isReturnStatement,
|
|
22
|
+
isSatisfiesExpression,
|
|
14
23
|
isPropertyAccessExpression,
|
|
15
24
|
isQualifiedName,
|
|
16
25
|
isTypeQueryNode,
|
|
@@ -18,7 +27,10 @@ import {
|
|
|
18
27
|
isPropertyDeclaration,
|
|
19
28
|
isVariableDeclaration,
|
|
20
29
|
isVariableStatement,
|
|
30
|
+
SyntaxKind,
|
|
31
|
+
type Expression,
|
|
21
32
|
type Node,
|
|
33
|
+
type ObjectLiteralExpression,
|
|
22
34
|
type SourceFile,
|
|
23
35
|
} from "@typescript/native/ast";
|
|
24
36
|
import { aliasChains } from "./alias-chain.ts";
|
|
@@ -67,6 +79,8 @@ export type RootKind =
|
|
|
67
79
|
| "pattern"
|
|
68
80
|
/** A declaration of a function or statement a type error skipped, and what it may have selected. */
|
|
69
81
|
| "type-error"
|
|
82
|
+
/** An ambient global interface merging into one declared outside the target, and its holders. */
|
|
83
|
+
| "global-augmentation"
|
|
70
84
|
/** A file a tool or the platform enters by its own convention, and what it exports where the tool reads it. */
|
|
71
85
|
| EntryRule;
|
|
72
86
|
|
|
@@ -446,6 +460,12 @@ export function roots<Brand>(
|
|
|
446
460
|
published.push(...entryFiles(entry));
|
|
447
461
|
}
|
|
448
462
|
}
|
|
463
|
+
if (options.publishedAPI) {
|
|
464
|
+
// A library's bundle hands what its entry exports to every program that loads it.
|
|
465
|
+
published.push(
|
|
466
|
+
...points.filter((point) => point.rule === "bundler-entry").map((point) => point.file),
|
|
467
|
+
);
|
|
468
|
+
}
|
|
449
469
|
const publishedNames = new Set(published.map((file) => file.fileName));
|
|
450
470
|
const patterned: { file: SourceFile; pattern: string }[] = [];
|
|
451
471
|
for (const pattern of options.entryFiles) {
|
|
@@ -713,17 +733,26 @@ function publishedAPI<Brand>(
|
|
|
713
733
|
const symbol = byId.get(id);
|
|
714
734
|
return symbol === undefined || symbol.kind === "file" ? [] : nodes.typeNamesOf(symbol);
|
|
715
735
|
});
|
|
716
|
-
|
|
736
|
+
const inferred = inferredTypeSymbols(
|
|
717
737
|
project,
|
|
718
738
|
round.flatMap((id) => {
|
|
719
739
|
const symbol = byId.get(id);
|
|
720
740
|
return symbol === undefined ? [] : nodes.nodesOf(symbol);
|
|
721
741
|
}),
|
|
722
|
-
)
|
|
742
|
+
);
|
|
743
|
+
for (const symbol of inferred.types) {
|
|
723
744
|
for (const target of chains.chainOf(symbol)) {
|
|
724
745
|
reach(target.id);
|
|
725
746
|
}
|
|
726
747
|
}
|
|
748
|
+
for (const symbol of inferred.members) {
|
|
749
|
+
for (const target of chains.chainOf(symbol)) {
|
|
750
|
+
const member = byId.get(target.id);
|
|
751
|
+
if (member?.visibility === "public" || member?.visibility === "protected") {
|
|
752
|
+
reach(target.id);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
727
756
|
project.symbolsAt(named.map((node) => project.handle(node))).forEach((symbol, index) => {
|
|
728
757
|
if (symbol === UNANSWERED) {
|
|
729
758
|
for (const id of held.byName.get(named[index]?.getText() ?? "") ?? []) {
|
|
@@ -783,30 +812,139 @@ function declarationNodes(
|
|
|
783
812
|
const INFERRED_DEPTH = 4;
|
|
784
813
|
|
|
785
814
|
/**
|
|
786
|
-
* The
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
|
|
790
|
-
|
|
815
|
+
* The values one function body returns, each through the type assertions around it: the
|
|
816
|
+
* value a caller receives carries the operand's type whatever type the assertion or the
|
|
817
|
+
* signature names. A nested function's returns are its own.
|
|
818
|
+
*/
|
|
819
|
+
function returnedValues(body: Node): readonly Expression[] {
|
|
820
|
+
const found: Expression[] = [];
|
|
821
|
+
const add = (value: Expression): void => {
|
|
822
|
+
for (let at: Expression | undefined = value; at !== undefined;) {
|
|
823
|
+
found.push(at);
|
|
824
|
+
at =
|
|
825
|
+
isAssertionExpression(at) || isSatisfiesExpression(at) || isParenthesizedExpression(at)
|
|
826
|
+
? at.expression
|
|
827
|
+
: undefined;
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
if (!isFunctionLikeDeclaration(body.parent) || body.parent.body !== body) {
|
|
831
|
+
return found;
|
|
832
|
+
}
|
|
833
|
+
if (isArrowFunction(body.parent) && body.kind !== SyntaxKind.Block) {
|
|
834
|
+
add(body as Expression);
|
|
835
|
+
return found;
|
|
836
|
+
}
|
|
837
|
+
const visit = (node: Node): void => {
|
|
838
|
+
if (isFunctionLikeDeclaration(node)) {
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
if (isReturnStatement(node) && node.expression !== undefined) {
|
|
842
|
+
add(node.expression);
|
|
843
|
+
}
|
|
844
|
+
node.forEachChild(visit);
|
|
845
|
+
};
|
|
846
|
+
body.forEachChild(visit);
|
|
847
|
+
return found;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/** The object literal one returned value is, written there or as the initializer of a variable `body` declares. */
|
|
851
|
+
function returnedLiteral(value: Expression, body: Node): ObjectLiteralExpression | undefined {
|
|
852
|
+
if (isObjectLiteralExpression(value)) {
|
|
853
|
+
return value;
|
|
854
|
+
}
|
|
855
|
+
if (!isIdentifier(value)) {
|
|
856
|
+
return undefined;
|
|
857
|
+
}
|
|
858
|
+
let found: ObjectLiteralExpression | undefined;
|
|
859
|
+
const visit = (node: Node): void => {
|
|
860
|
+
if (
|
|
861
|
+
isVariableDeclaration(node) &&
|
|
862
|
+
isIdentifier(node.name) &&
|
|
863
|
+
node.name.text === value.text &&
|
|
864
|
+
node.initializer !== undefined
|
|
865
|
+
) {
|
|
866
|
+
let at: Expression = node.initializer;
|
|
867
|
+
while (
|
|
868
|
+
isAssertionExpression(at) ||
|
|
869
|
+
isSatisfiesExpression(at) ||
|
|
870
|
+
isParenthesizedExpression(at)
|
|
871
|
+
) {
|
|
872
|
+
at = at.expression;
|
|
873
|
+
}
|
|
874
|
+
found = isObjectLiteralExpression(at) ? at : found;
|
|
875
|
+
}
|
|
876
|
+
if (!isFunctionLikeDeclaration(node)) {
|
|
877
|
+
node.forEachChild(visit);
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
body.forEachChild(visit);
|
|
881
|
+
return found;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* The values one function body returns, and those each method of an object literal it
|
|
886
|
+
* returns returns, to a fixed depth: a caller holding the literal calls its methods, and
|
|
887
|
+
* receives what they return.
|
|
888
|
+
*/
|
|
889
|
+
function returnedThroughLiterals(body: Node, depth = 0): readonly Expression[] {
|
|
890
|
+
const values = returnedValues(body);
|
|
891
|
+
if (depth >= INFERRED_DEPTH) {
|
|
892
|
+
return values;
|
|
893
|
+
}
|
|
894
|
+
const found = [...values];
|
|
895
|
+
for (const value of values) {
|
|
896
|
+
for (const property of returnedLiteral(value, body)?.properties ?? []) {
|
|
897
|
+
const fn = isMethodDeclaration(property)
|
|
898
|
+
? property
|
|
899
|
+
: isPropertyAssignment(property) && isFunctionLikeDeclaration(property.initializer)
|
|
900
|
+
? property.initializer
|
|
901
|
+
: undefined;
|
|
902
|
+
if (fn?.body !== undefined) {
|
|
903
|
+
found.push(...returnedThroughLiterals(fn.body, depth + 1));
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return found;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* The declarations of the types the compiler infers where a declaration writes none, and
|
|
912
|
+
* of the values a function returns: the type of a variable or a property with no
|
|
913
|
+
* annotation, the result of a function or a method with no result annotation, and the type
|
|
914
|
+
* of each value its body returns, each with the types its type arguments and its union or
|
|
915
|
+
* intersection constituents name, to a fixed depth; and as `members` the properties of each
|
|
916
|
+
* returned value's type. A question the checker leaves unanswered names nothing.
|
|
791
917
|
*/
|
|
792
918
|
function inferredTypeSymbols<Brand>(
|
|
793
919
|
project: ProjectView<Brand>,
|
|
794
920
|
declarations: readonly Node[],
|
|
795
|
-
): readonly TSSymbol[] {
|
|
921
|
+
): { readonly types: readonly TSSymbol[]; readonly members: readonly TSSymbol[] } {
|
|
796
922
|
const { queries } = project;
|
|
797
|
-
const valued = declarations.flatMap((node) =>
|
|
923
|
+
const valued: Node[] = declarations.flatMap((node) =>
|
|
798
924
|
(isVariableDeclaration(node) || isPropertyDeclaration(node)) &&
|
|
799
925
|
node.type === undefined &&
|
|
800
926
|
!isFunctionLikeDeclaration(node.initializer ?? node)
|
|
801
927
|
? [node.name]
|
|
802
928
|
: [],
|
|
803
929
|
);
|
|
930
|
+
const returned = declarations.flatMap((node) => {
|
|
931
|
+
const signature = isFunctionLikeDeclaration(node)
|
|
932
|
+
? node
|
|
933
|
+
: isVariableDeclaration(node) &&
|
|
934
|
+
node.initializer !== undefined &&
|
|
935
|
+
isFunctionLikeDeclaration(node.initializer)
|
|
936
|
+
? node.initializer
|
|
937
|
+
: undefined;
|
|
938
|
+
return signature?.body === undefined ? [] : returnedThroughLiterals(signature.body);
|
|
939
|
+
});
|
|
804
940
|
const types: Type[] = [];
|
|
805
|
-
|
|
941
|
+
const returnedTypes: Type[] = [];
|
|
942
|
+
queries.typesAt([...valued, ...returned]).forEach((type, at) => {
|
|
806
943
|
if (type !== UNANSWERED && type !== undefined) {
|
|
807
|
-
types.push(type);
|
|
944
|
+
(at < valued.length ? types : returnedTypes).push(type);
|
|
808
945
|
}
|
|
809
946
|
});
|
|
947
|
+
types.push(...returnedTypes);
|
|
810
948
|
for (const node of declarations) {
|
|
811
949
|
const signature = isFunctionLikeDeclaration(node)
|
|
812
950
|
? node
|
|
@@ -849,7 +987,19 @@ function inferredTypeSymbols<Brand>(
|
|
|
849
987
|
types.forEach((type) => {
|
|
850
988
|
visit(type, 0);
|
|
851
989
|
});
|
|
852
|
-
|
|
990
|
+
// A returned value carries each property of its type, one an anonymous type declares or
|
|
991
|
+
// an intersection merges included, which no type name reaches.
|
|
992
|
+
const members = new Map<number, TSSymbol>();
|
|
993
|
+
for (const type of returnedTypes) {
|
|
994
|
+
const parts = queries.constituents(type);
|
|
995
|
+
for (const one of [type, ...(parts === UNANSWERED ? [] : parts)]) {
|
|
996
|
+
const properties = queries.propertiesOf(one);
|
|
997
|
+
for (const property of properties === UNANSWERED ? [] : properties) {
|
|
998
|
+
members.set(property.id, property);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return { types: [...found.values()], members: [...members.values()] };
|
|
853
1003
|
}
|
|
854
1004
|
|
|
855
1005
|
/**
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
isClassStaticBlockDeclaration,
|
|
18
18
|
isFunctionDeclaration,
|
|
19
19
|
isFunctionExpression,
|
|
20
|
+
isFunctionLikeDeclaration,
|
|
20
21
|
isGetAccessorDeclaration,
|
|
21
22
|
isIdentifier,
|
|
22
23
|
isInterfaceDeclaration,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
isNumericLiteral,
|
|
26
27
|
isObjectLiteralExpression,
|
|
27
28
|
isParenthesizedExpression,
|
|
29
|
+
isForInStatement,
|
|
28
30
|
isPropertyAccessExpression,
|
|
29
31
|
isPropertyDeclaration,
|
|
30
32
|
isSatisfiesExpression,
|
|
@@ -307,9 +309,37 @@ function walkProject<Brand>(project: ProjectView<Brand>, held: Inventory): Walke
|
|
|
307
309
|
return { sites, parameters };
|
|
308
310
|
}
|
|
309
311
|
|
|
312
|
+
/** The calls of `Object` that read every own property of their argument by name. */
|
|
313
|
+
const ENUMERATIONS: ReadonlySet<string> = new Set([
|
|
314
|
+
"keys",
|
|
315
|
+
"entries",
|
|
316
|
+
"values",
|
|
317
|
+
"getOwnPropertyNames",
|
|
318
|
+
"assign",
|
|
319
|
+
]);
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Whether a parameter type keeps no member name of the value it is given: `unknown`, `any`,
|
|
323
|
+
* or an object type of a string index signature and no property.
|
|
324
|
+
*/
|
|
325
|
+
function keepsNoName<Brand>(project: ProjectView<Brand>, type: Type): boolean {
|
|
326
|
+
if ((type.flags & ERASED) !== 0) {
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
if (
|
|
330
|
+
(type.flags & TypeFlags.Object) === 0 ||
|
|
331
|
+
must(project.queries.propertiesOf(type)).length > 0
|
|
332
|
+
) {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
return must(project.queries.indexInfosOf(type)).some(
|
|
336
|
+
(info) => (info.keyType.flags & TypeFlags.String) !== 0,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
310
340
|
/**
|
|
311
|
-
* The parameters that keep
|
|
312
|
-
*
|
|
341
|
+
* The parameters that keep no member name of the value they are given, as `keepsNoName`
|
|
342
|
+
* reads it, and a rest parameter whose elements keep none, each by its symbol.
|
|
313
343
|
*/
|
|
314
344
|
function erasedParameters<Brand>(
|
|
315
345
|
project: ProjectView<Brand>,
|
|
@@ -325,10 +355,10 @@ function erasedParameters<Brand>(
|
|
|
325
355
|
return false;
|
|
326
356
|
}
|
|
327
357
|
if (!parameter.rest) {
|
|
328
|
-
return (type
|
|
358
|
+
return keepsNoName(project, type);
|
|
329
359
|
}
|
|
330
360
|
const [element] = must(project.queries.typeArguments(type));
|
|
331
|
-
return element !== undefined && (element
|
|
361
|
+
return element !== undefined && keepsNoName(project, element);
|
|
332
362
|
});
|
|
333
363
|
const bySymbol = new Map<number, Parameter>();
|
|
334
364
|
project
|
|
@@ -343,6 +373,124 @@ function erasedParameters<Brand>(
|
|
|
343
373
|
return bySymbol;
|
|
344
374
|
}
|
|
345
375
|
|
|
376
|
+
/** Whether one node is a function, whose body runs only where something calls it. */
|
|
377
|
+
function isFunctionNode(node: Node): boolean {
|
|
378
|
+
return (
|
|
379
|
+
isFunctionDeclaration(node) ||
|
|
380
|
+
isFunctionExpression(node) ||
|
|
381
|
+
isArrowFunction(node) ||
|
|
382
|
+
isMethodDeclaration(node) ||
|
|
383
|
+
isGetAccessorDeclaration(node) ||
|
|
384
|
+
isSetAccessorDeclaration(node)
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** The name a function nested in another is called by: its own, or the variable it initializes. */
|
|
389
|
+
function nestedName(fn: Node): string | undefined {
|
|
390
|
+
if (isFunctionDeclaration(fn)) {
|
|
391
|
+
return fn.name?.text;
|
|
392
|
+
}
|
|
393
|
+
let at = fn.parent;
|
|
394
|
+
while (isParenthesizedExpression(at) || isAsExpression(at) || isSatisfiesExpression(at)) {
|
|
395
|
+
at = at.parent;
|
|
396
|
+
}
|
|
397
|
+
return isVariableDeclaration(at) && isIdentifier(at.name) ? at.name.text : undefined;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** The names one function's body writes as identifiers, other than the names its declarations bind. */
|
|
401
|
+
function namesWritten(fn: Node): ReadonlySet<string> {
|
|
402
|
+
const found = new Set<string>();
|
|
403
|
+
const visit = (node: Node): void => {
|
|
404
|
+
const declares =
|
|
405
|
+
(isFunctionDeclaration(node.parent) || isVariableDeclaration(node.parent)) &&
|
|
406
|
+
node.parent.name === node;
|
|
407
|
+
if (isIdentifier(node) && !declares) {
|
|
408
|
+
found.add(node.text);
|
|
409
|
+
}
|
|
410
|
+
node.forEachChild(visit);
|
|
411
|
+
};
|
|
412
|
+
fn.forEachChild(visit);
|
|
413
|
+
return found;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* The erased parameters their own function enumerates: one an `Object.keys`, `entries`,
|
|
418
|
+
* `values`, `getOwnPropertyNames` or `assign` call takes, `Object` the standard library's,
|
|
419
|
+
* or a `for…in` statement walks, which reads every property of the value by its name. A
|
|
420
|
+
* nested function is read as the function's own only where its name is written in it, or
|
|
421
|
+
* it has none, a callback or a function called where it is written.
|
|
422
|
+
*/
|
|
423
|
+
function enumeratedParameters<Brand>(
|
|
424
|
+
project: ProjectView<Brand>,
|
|
425
|
+
erased: ReadonlyMap<number, Parameter>,
|
|
426
|
+
): readonly Parameter[] {
|
|
427
|
+
const asked: {
|
|
428
|
+
readonly node: Node;
|
|
429
|
+
readonly parameter: Parameter;
|
|
430
|
+
readonly object: Node | undefined;
|
|
431
|
+
}[] = [];
|
|
432
|
+
for (const parameter of erased.values()) {
|
|
433
|
+
const fn = parameter.name.parent.parent;
|
|
434
|
+
const spelled = parameter.name.getText();
|
|
435
|
+
const written = namesWritten(fn);
|
|
436
|
+
const visit = (node: Node): void => {
|
|
437
|
+
if (isFunctionNode(node)) {
|
|
438
|
+
const name = nestedName(node);
|
|
439
|
+
if (name !== undefined && !written.has(name)) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const read: Expression[] = [];
|
|
444
|
+
let object: Node | undefined;
|
|
445
|
+
if (isCallExpression(node)) {
|
|
446
|
+
const callee = node.expression;
|
|
447
|
+
if (
|
|
448
|
+
isPropertyAccessExpression(callee) &&
|
|
449
|
+
isIdentifier(callee.expression) &&
|
|
450
|
+
callee.expression.text === "Object" &&
|
|
451
|
+
ENUMERATIONS.has(callee.name.text)
|
|
452
|
+
) {
|
|
453
|
+
object = callee.expression;
|
|
454
|
+
// `Object.assign` reads its sources, every argument after the target.
|
|
455
|
+
read.push(...node.arguments.slice(...(callee.name.text === "assign" ? [1] : [0, 1])));
|
|
456
|
+
}
|
|
457
|
+
} else if (isForInStatement(node)) {
|
|
458
|
+
read.push(node.expression);
|
|
459
|
+
}
|
|
460
|
+
for (const value of read.map(evaluated)) {
|
|
461
|
+
if (isIdentifier(value) && value.text === spelled) {
|
|
462
|
+
asked.push({ node: value, parameter, object });
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
node.forEachChild(visit);
|
|
466
|
+
};
|
|
467
|
+
fn.forEachChild(visit);
|
|
468
|
+
}
|
|
469
|
+
const symbols = project.symbolsAt(asked.map((one) => project.handle(one.node))).map(must);
|
|
470
|
+
const objects = asked.flatMap((one) => (one.object === undefined ? [] : [one.object]));
|
|
471
|
+
const own = project.ownPaths();
|
|
472
|
+
const standard = new Set(
|
|
473
|
+
project
|
|
474
|
+
.symbolsAt(objects.map((node) => project.handle(node)))
|
|
475
|
+
.map(must)
|
|
476
|
+
.flatMap((symbol, at) =>
|
|
477
|
+
symbol?.declarations.every((handle) => !own.has(handle.path)) === true ? [objects[at]] : [],
|
|
478
|
+
),
|
|
479
|
+
);
|
|
480
|
+
return [
|
|
481
|
+
...new Set(
|
|
482
|
+
asked.flatMap((one, at) => {
|
|
483
|
+
const symbol = symbols[at];
|
|
484
|
+
return symbol !== undefined &&
|
|
485
|
+
erased.get(symbol.id) === one.parameter &&
|
|
486
|
+
(one.object === undefined || standard.has(one.object))
|
|
487
|
+
? [one.parameter]
|
|
488
|
+
: [];
|
|
489
|
+
}),
|
|
490
|
+
),
|
|
491
|
+
];
|
|
492
|
+
}
|
|
493
|
+
|
|
346
494
|
/** Each place a call's argument passes one of the erased parameters it can see. */
|
|
347
495
|
function handedOn<Brand>(
|
|
348
496
|
project: ProjectView<Brand>,
|
|
@@ -452,8 +600,8 @@ interface Destinations {
|
|
|
452
600
|
* callee is a serializer an entry names; at the arguments its resolved signature types
|
|
453
601
|
* `unknown` or `any` where its callee is declared outside the analysis or only ambiently
|
|
454
602
|
* with no implementation in it; and, where its callee is a function of the project, at each
|
|
455
|
-
* parameter the function hands on to a destination, holding back the widest
|
|
456
|
-
*
|
|
603
|
+
* parameter the function enumerates or hands on to a destination, holding back the widest
|
|
604
|
+
* of what those hold back, settled by widening until no parameter moves.
|
|
457
605
|
*/
|
|
458
606
|
function destinations<Brand>(
|
|
459
607
|
input: DetectorInput<Brand>,
|
|
@@ -674,6 +822,9 @@ function destinations<Brand>(
|
|
|
674
822
|
return undefined;
|
|
675
823
|
};
|
|
676
824
|
|
|
825
|
+
for (const parameter of enumeratedParameters(project, erased)) {
|
|
826
|
+
forwards.set(keyOf(parameter), "properties");
|
|
827
|
+
}
|
|
677
828
|
const forwarded = handedOn(project, walked.sites, erased, invocation);
|
|
678
829
|
for (let moved = true; moved;) {
|
|
679
830
|
moved = false;
|
|
@@ -776,6 +927,19 @@ function flowsOf<Brand>(
|
|
|
776
927
|
},
|
|
777
928
|
];
|
|
778
929
|
});
|
|
930
|
+
const generic = typeParameterFlows(
|
|
931
|
+
project,
|
|
932
|
+
reach,
|
|
933
|
+
sites,
|
|
934
|
+
arguments_.flatMap((one) => {
|
|
935
|
+
const type = typeOf.get(one.value);
|
|
936
|
+
const parameters = type === undefined ? [] : typeParametersOf(project, type, 0);
|
|
937
|
+
const destination = parameters.length === 0 ? undefined : to.at(one.site.call, one.argument);
|
|
938
|
+
return destination === undefined
|
|
939
|
+
? []
|
|
940
|
+
: parameters.map((parameter) => ({ parameter, to: destination }));
|
|
941
|
+
}),
|
|
942
|
+
);
|
|
779
943
|
const left = stores.outside.flatMap((store) => {
|
|
780
944
|
const reached = reach.reachedFrom(store.type);
|
|
781
945
|
return reached.classes.size === 0 && reached.members.length === 0
|
|
@@ -790,7 +954,145 @@ function flowsOf<Brand>(
|
|
|
790
954
|
},
|
|
791
955
|
];
|
|
792
956
|
});
|
|
793
|
-
return [...passed, ...left];
|
|
957
|
+
return [...passed, ...generic, ...left];
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/** The type parameters one type names, through constituents and type arguments. */
|
|
961
|
+
function typeParametersOf<Brand>(project: ProjectView<Brand>, type: Type, depth: number): Type[] {
|
|
962
|
+
if (type.isTypeParameter()) {
|
|
963
|
+
return [type];
|
|
964
|
+
}
|
|
965
|
+
if (depth >= MENTION_DEPTH) {
|
|
966
|
+
return [];
|
|
967
|
+
}
|
|
968
|
+
const { queries } = project;
|
|
969
|
+
return [
|
|
970
|
+
...(type.isUnionType() || type.isIntersectionType() ? must(queries.constituents(type)) : []),
|
|
971
|
+
...must(queries.typeArguments(type)),
|
|
972
|
+
].flatMap((one) => typeParametersOf(project, one, depth + 1));
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* The types an instantiated type puts where a declared type names `parameter`: matched
|
|
977
|
+
* through type arguments, and in a union every constituent the declared union does not
|
|
978
|
+
* itself name.
|
|
979
|
+
*/
|
|
980
|
+
function bound<Brand>(
|
|
981
|
+
project: ProjectView<Brand>,
|
|
982
|
+
declared: Type,
|
|
983
|
+
instantiated: Type,
|
|
984
|
+
parameter: Type,
|
|
985
|
+
depth: number,
|
|
986
|
+
): Type[] {
|
|
987
|
+
if (declared.id === parameter.id) {
|
|
988
|
+
return [instantiated];
|
|
989
|
+
}
|
|
990
|
+
if (depth >= MENTION_DEPTH) {
|
|
991
|
+
return [];
|
|
992
|
+
}
|
|
993
|
+
const { queries } = project;
|
|
994
|
+
if (declared.isUnionType()) {
|
|
995
|
+
const parts = must(queries.constituents(declared));
|
|
996
|
+
if (parts.some((part) => part.id === parameter.id)) {
|
|
997
|
+
const kept = new Set(parts.map((part) => part.id));
|
|
998
|
+
const given = instantiated.isUnionType()
|
|
999
|
+
? must(queries.constituents(instantiated))
|
|
1000
|
+
: [instantiated];
|
|
1001
|
+
return given.filter((part) => !kept.has(part.id));
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
const declaredArguments = must(queries.typeArguments(declared));
|
|
1005
|
+
const givenArguments = must(queries.typeArguments(instantiated));
|
|
1006
|
+
return declaredArguments.length !== givenArguments.length
|
|
1007
|
+
? []
|
|
1008
|
+
: declaredArguments.flatMap((one, at) => {
|
|
1009
|
+
const given = givenArguments[at];
|
|
1010
|
+
return given === undefined ? [] : bound(project, one, given, parameter, depth + 1);
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* The flows a type parameter of a function of the project carries where a value its type
|
|
1016
|
+
* names reaches a destination: each call of the function passes its type argument there,
|
|
1017
|
+
* written or inferred from the arguments, and a type argument that is a type parameter
|
|
1018
|
+
* of the calling function is followed on to that function's calls, to a fixpoint.
|
|
1019
|
+
*/
|
|
1020
|
+
function typeParameterFlows<Brand>(
|
|
1021
|
+
project: ProjectView<Brand>,
|
|
1022
|
+
reach: ReturnType<typeof valueReach>,
|
|
1023
|
+
sites: readonly Site[],
|
|
1024
|
+
pending: { readonly parameter: Type; readonly to: Destination }[],
|
|
1025
|
+
): readonly Flow[] {
|
|
1026
|
+
const { queries } = project;
|
|
1027
|
+
const byName = new Map<string, Site[]>();
|
|
1028
|
+
for (const site of sites) {
|
|
1029
|
+
const name = calleeName(site.call.expression)?.getText();
|
|
1030
|
+
if (name !== undefined) {
|
|
1031
|
+
byName.set(name, [...(byName.get(name) ?? []), site]);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
const found: Flow[] = [];
|
|
1035
|
+
const seen = new Set<number>();
|
|
1036
|
+
for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
|
|
1037
|
+
const { parameter, to } = next;
|
|
1038
|
+
if (seen.has(parameter.id)) {
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
seen.add(parameter.id);
|
|
1042
|
+
const symbol = must(queries.symbolOfType(parameter));
|
|
1043
|
+
const [handle] = symbol?.declarations ?? [];
|
|
1044
|
+
const declaration = handle === undefined ? undefined : project.declarationAt(handle)?.node;
|
|
1045
|
+
const fn = declaration?.parent;
|
|
1046
|
+
const index =
|
|
1047
|
+
fn === undefined || !isFunctionLikeDeclaration(fn)
|
|
1048
|
+
? -1
|
|
1049
|
+
: (fn.typeParameters ?? []).findIndex((one) => one === declaration);
|
|
1050
|
+
const name =
|
|
1051
|
+
fn === undefined
|
|
1052
|
+
? undefined
|
|
1053
|
+
: isMethodDeclaration(fn) && isIdentifier(fn.name)
|
|
1054
|
+
? fn.name.text
|
|
1055
|
+
: nestedName(fn);
|
|
1056
|
+
if (symbol === undefined || fn === undefined || index < 0 || name === undefined) {
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
for (const site of byName.get(name) ?? []) {
|
|
1060
|
+
const resolved = must(queries.resolvedSignature(site.call));
|
|
1061
|
+
const declared = resolved === undefined ? undefined : must(queries.genericOf(resolved));
|
|
1062
|
+
const called =
|
|
1063
|
+
declared?.declaration === undefined
|
|
1064
|
+
? undefined
|
|
1065
|
+
: project.declarationAt(declared.declaration)?.node;
|
|
1066
|
+
if (resolved === undefined || declared === undefined || called !== fn) {
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
const written = site.call.typeArguments?.[index];
|
|
1070
|
+
const given =
|
|
1071
|
+
written === undefined
|
|
1072
|
+
? site.call.arguments.flatMap((_argument, at) => {
|
|
1073
|
+
const from = must(queries.parameterType(declared, at));
|
|
1074
|
+
const into = must(queries.parameterType(resolved, at));
|
|
1075
|
+
return from === undefined || into === undefined
|
|
1076
|
+
? []
|
|
1077
|
+
: bound(project, from, into, parameter, 0);
|
|
1078
|
+
})
|
|
1079
|
+
: typesAt(project, [written]).filter((type) => type !== undefined);
|
|
1080
|
+
for (const type of given) {
|
|
1081
|
+
const reached = reach.reachedFrom(type);
|
|
1082
|
+
if (reached.classes.size > 0 || reached.members.length > 0) {
|
|
1083
|
+
found.push({
|
|
1084
|
+
site: site.call,
|
|
1085
|
+
classes: reached.classes,
|
|
1086
|
+
members: reached.members,
|
|
1087
|
+
to,
|
|
1088
|
+
detail: `passed to ${to.callee} through the type parameter ${symbol.name}`,
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
pending.push(...typeParametersOf(project, type, 0).map((one) => ({ parameter: one, to })));
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return found;
|
|
794
1096
|
}
|
|
795
1097
|
|
|
796
1098
|
/**
|
package/src/sweep.ts
CHANGED
|
@@ -195,10 +195,14 @@ export function runnerCalls(symbol: InventorySymbol): boolean {
|
|
|
195
195
|
* That run's roots are the graph's own and, of each test file, its top level and every
|
|
196
196
|
* function it exports, which a test runner may call, and every export of a manual mock,
|
|
197
197
|
* which the runner reads; any other export of a test file, and test-support code, which
|
|
198
|
-
* runs where a test imports it, are judged as any file's are.
|
|
199
|
-
* `
|
|
198
|
+
* runs where a test imports it, are judged as any file's are. `held` seeds it as a mark
|
|
199
|
+
* seeds a sweep; with `namespaces` false a file read reaches none of the file's exports.
|
|
200
200
|
*/
|
|
201
|
-
export function reachedWithTheTests(
|
|
201
|
+
export function reachedWithTheTests(
|
|
202
|
+
graph: Graph,
|
|
203
|
+
held: readonly string[],
|
|
204
|
+
namespaces = true,
|
|
205
|
+
): readonly boolean[] {
|
|
202
206
|
const tests = graph.symbols.flatMap((symbol, at) => {
|
|
203
207
|
const exported = symbol.exported && graph.symbols[graph.parent[at] ?? OUTSIDE]?.kind === "file";
|
|
204
208
|
const called = exported && runnerCalls(symbol);
|
|
@@ -208,9 +212,12 @@ export function reachedWithTheTests(graph: Graph, held: readonly string[]): read
|
|
|
208
212
|
? [{ at, kind: "test-runner" as const }]
|
|
209
213
|
: [];
|
|
210
214
|
});
|
|
211
|
-
return reachable(
|
|
212
|
-
|
|
213
|
-
|
|
215
|
+
return reachable(
|
|
216
|
+
{ ...graph, rooted: [...graph.rooted, ...tests] },
|
|
217
|
+
flagged(graph, held),
|
|
218
|
+
{ production: false },
|
|
219
|
+
namespaces,
|
|
220
|
+
);
|
|
214
221
|
}
|
|
215
222
|
|
|
216
223
|
/**
|
|
@@ -240,7 +247,12 @@ function flagged(graph: Graph, ids: readonly string[]): boolean[] {
|
|
|
240
247
|
* included, while a root reaches through the references the mode counts. The held seed
|
|
241
248
|
* runs first, so what it reaches is expanded under the wider rule.
|
|
242
249
|
*/
|
|
243
|
-
function reachable(
|
|
250
|
+
function reachable(
|
|
251
|
+
graph: Graph,
|
|
252
|
+
held: readonly boolean[],
|
|
253
|
+
mode: Mode,
|
|
254
|
+
namespaces = true,
|
|
255
|
+
): readonly boolean[] {
|
|
244
256
|
const reached = graph.symbols.map(() => false);
|
|
245
257
|
const expanded = graph.symbols.map(() => false);
|
|
246
258
|
const queue: number[] = [];
|
|
@@ -269,7 +281,7 @@ function reachable(graph: Graph, held: readonly boolean[], mode: Mode): readonly
|
|
|
269
281
|
if (!holding && mode.production && edge.test) {
|
|
270
282
|
continue;
|
|
271
283
|
}
|
|
272
|
-
enter(edge.to, !edge.evaluation);
|
|
284
|
+
enter(edge.to, namespaces && !edge.evaluation);
|
|
273
285
|
}
|
|
274
286
|
}
|
|
275
287
|
};
|
|
@@ -285,7 +297,7 @@ function reachable(graph: Graph, held: readonly boolean[], mode: Mode): readonly
|
|
|
285
297
|
}
|
|
286
298
|
// A consumer's reference reaches what it names as one of the target's own does.
|
|
287
299
|
for (const edge of callsOf(graph, mode)) {
|
|
288
|
-
enter(edge.to, !edge.evaluation);
|
|
300
|
+
enter(edge.to, namespaces && !edge.evaluation);
|
|
289
301
|
}
|
|
290
302
|
walk(false);
|
|
291
303
|
return reached;
|