@cplieger/deadset-ts 8.0.0-dev.3 → 8.0.0-dev.4
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/README.md +1 -1
- package/dist/src/analysis.js +34 -5
- package/dist/src/augmentations.js +1 -1
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contextual-uses.js +56 -9
- package/dist/src/dependencies.js +22 -14
- package/dist/src/discover.js +14 -82
- package/dist/src/exempt.js +8 -0
- package/dist/src/findings/completion.js +4 -2
- package/dist/src/kinds.js +1 -1
- package/dist/src/query.js +1 -0
- package/dist/src/reporters/annotations.js +15 -8
- package/dist/src/roots.js +69 -41
- package/dist/src/serialization-contract.js +337 -9
- package/dist/src/verbs/analyze.js +0 -1
- package/dist/src/workflow-steps.js +32 -15
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +64 -5
- package/src/augmentations.ts +1 -1
- package/src/conformance-record.ts +1 -1
- package/src/contextual-uses.ts +64 -12
- package/src/dependencies.ts +27 -14
- package/src/discover.ts +14 -94
- package/src/exempt.ts +16 -0
- package/src/findings/boundary.ts +2 -0
- package/src/findings/completion.ts +4 -2
- package/src/kinds.ts +1 -1
- package/src/query.ts +11 -0
- package/src/reporters/annotations.ts +21 -10
- package/src/reporters/reporter.ts +1 -3
- package/src/roots.ts +73 -43
- package/src/serialization-contract.ts +455 -11
- package/src/sweep.ts +2 -0
- package/src/verbs/analyze.ts +0 -1
- package/src/workflow-steps.ts +35 -17
package/dist/src/roots.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { isArrowFunction, isAssertionExpression, isExpressionWithTypeArguments, isFunctionLikeDeclaration, isIdentifier, isMethodDeclaration, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAssignment, isReturnStatement, isSatisfiesExpression, isPropertyAccessExpression, isQualifiedName, isTypeQueryNode, isTypeReferenceNode, isPropertyDeclaration, isVariableDeclaration, isVariableStatement, SyntaxKind, } from "@typescript/native/ast";
|
|
11
11
|
import { aliasChains } from "./alias-chain.js";
|
|
12
|
+
import { isGlobalBlock } from "./augmentations.js";
|
|
12
13
|
import { filesNamedBy, outsideChain, relativeSpecifiers, } from "./configuration-files.js";
|
|
13
14
|
import { conventionEntries } from "./conventions.js";
|
|
14
15
|
import { entryPoints } from "./entry-points.js";
|
|
@@ -443,9 +444,11 @@ const EXPORTING_CONTAINERS = new Set(["file", "namespace"]);
|
|
|
443
444
|
* Every declaration a consumer of the library can name: what the published files export,
|
|
444
445
|
* and below each the members a consumer writes, a namespace's exported ones and any other
|
|
445
446
|
* declaration's public and protected ones. A declaration a published one's type names is
|
|
446
|
-
* published too, and so is an exported declaration a release tag marks
|
|
447
|
-
*
|
|
448
|
-
*
|
|
447
|
+
* published too, and so is an exported declaration a release tag marks, and every
|
|
448
|
+
* declaration a `declare global` block of a published file, or of a file holding a published
|
|
449
|
+
* declaration, holds: the consumer's compiler loads that file's typings and their global
|
|
450
|
+
* scope with it. A type name the checker leaves unanswered publishes every declaration
|
|
451
|
+
* spelled as it is, as `guessed`. A published file is not itself published.
|
|
449
452
|
*/
|
|
450
453
|
function publishedAPI(project, published, exportsOf, held, byPath, tagged) {
|
|
451
454
|
const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
|
|
@@ -471,55 +474,80 @@ function publishedAPI(project, published, exportsOf, held, byPath, tagged) {
|
|
|
471
474
|
reach(id);
|
|
472
475
|
}
|
|
473
476
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
477
|
+
const files = new Set(held.symbols.filter((one) => one.kind === "file").map((one) => one.id));
|
|
478
|
+
const blocks = held.symbols.filter((one) => isGlobalBlock(one, held, files));
|
|
479
|
+
const fileOf = (id) => {
|
|
480
|
+
let at = byId.get(id);
|
|
481
|
+
while (at !== undefined && at.kind !== "file") {
|
|
482
|
+
at = byId.get(at.parent);
|
|
483
|
+
}
|
|
484
|
+
return at?.id;
|
|
485
|
+
};
|
|
486
|
+
const publishedFiles = published.flatMap((file) => held.declarations.get(nodeKey(file, file)) ?? []);
|
|
487
|
+
const globalsReached = () => {
|
|
488
|
+
const typed = new Set([...publishedFiles, ...[...reached].flatMap((id) => fileOf(id) ?? [])]);
|
|
489
|
+
const before = pending.length;
|
|
490
|
+
for (const block of blocks.filter((one) => typed.has(one.parent))) {
|
|
491
|
+
reach(block.id);
|
|
492
|
+
for (const member of children.get(block.id) ?? []) {
|
|
493
|
+
if (member.kind !== "type-parameter") {
|
|
484
494
|
reach(member.id);
|
|
485
495
|
}
|
|
486
496
|
}
|
|
487
497
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const member = byId.get(target.id);
|
|
504
|
-
if (member?.visibility === "public" || member?.visibility === "protected") {
|
|
505
|
-
reach(target.id);
|
|
498
|
+
return pending.length > before;
|
|
499
|
+
};
|
|
500
|
+
do {
|
|
501
|
+
while (pending.length > 0) {
|
|
502
|
+
const round = [];
|
|
503
|
+
for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
|
|
504
|
+
round.push(next);
|
|
505
|
+
const exporting = EXPORTING_CONTAINERS.has(byId.get(next)?.kind ?? "");
|
|
506
|
+
for (const member of children.get(next) ?? []) {
|
|
507
|
+
const reachable = exporting
|
|
508
|
+
? member.exported
|
|
509
|
+
: member.visibility !== "private" && member.visibility !== "private-name";
|
|
510
|
+
if (member.kind !== "type-parameter" && reachable) {
|
|
511
|
+
reach(member.id);
|
|
512
|
+
}
|
|
506
513
|
}
|
|
507
514
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
515
|
+
const named = round.flatMap((id) => {
|
|
516
|
+
const symbol = byId.get(id);
|
|
517
|
+
return symbol === undefined || symbol.kind === "file" ? [] : nodes.typeNamesOf(symbol);
|
|
518
|
+
});
|
|
519
|
+
const inferred = inferredTypeSymbols(project, round.flatMap((id) => {
|
|
520
|
+
const symbol = byId.get(id);
|
|
521
|
+
return symbol === undefined ? [] : nodes.nodesOf(symbol);
|
|
522
|
+
}));
|
|
523
|
+
for (const symbol of inferred.types) {
|
|
524
|
+
for (const target of chains.chainOf(symbol)) {
|
|
525
|
+
reach(target.id);
|
|
514
526
|
}
|
|
515
527
|
}
|
|
516
|
-
|
|
528
|
+
for (const symbol of inferred.members) {
|
|
517
529
|
for (const target of chains.chainOf(symbol)) {
|
|
518
|
-
|
|
530
|
+
const member = byId.get(target.id);
|
|
531
|
+
if (member?.visibility === "public" || member?.visibility === "protected") {
|
|
532
|
+
reach(target.id);
|
|
533
|
+
}
|
|
519
534
|
}
|
|
520
535
|
}
|
|
521
|
-
|
|
522
|
-
|
|
536
|
+
project.symbolsAt(named.map((node) => project.handle(node))).forEach((symbol, index) => {
|
|
537
|
+
if (symbol === UNANSWERED) {
|
|
538
|
+
for (const id of held.byName.get(named[index]?.getText() ?? "") ?? []) {
|
|
539
|
+
guessed.add(id);
|
|
540
|
+
reach(id);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
else if (symbol !== undefined) {
|
|
544
|
+
for (const target of chains.chainOf(symbol)) {
|
|
545
|
+
reach(target.id);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
} while (globalsReached());
|
|
523
551
|
return { ids: [...reached], guessed: [...guessed] };
|
|
524
552
|
}
|
|
525
553
|
/** The type names declarations of the inventory write, each file walked once, when first asked. */
|
|
@@ -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, 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";
|
|
9
|
+
import { isArrayLiteralExpression, isArrowFunction, isAsExpression, isAwaitExpression, isCallExpression, isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isElementAccessExpression, isExpressionStatement, isFunctionDeclaration, isFunctionExpression, isFunctionLikeDeclaration, isGetAccessorDeclaration, isIdentifier, isInterfaceDeclaration, isMethodDeclaration, isNonNullExpression, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isParenthesizedExpression, isForInStatement, isPropertyAccessExpression, isPropertyDeclaration, isReturnStatement, isSatisfiesExpression, isSetAccessorDeclaration, isSourceFile, isSpreadAssignment, isSpreadElement, isStringLiteral, isTypeAssertion, 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";
|
|
@@ -14,7 +14,7 @@ import { classMembers, memberComponent, typesAt } from "./class-members.js";
|
|
|
14
14
|
import { names, namesADeclaration, resolveEntries, spelledEntry, } from "./configured-declarations.js";
|
|
15
15
|
import { nodeKey } from "./inventory.js";
|
|
16
16
|
import { renderPosition } from "./position.js";
|
|
17
|
-
import { must } from "./query.js";
|
|
17
|
+
import { must, Unanswerable } from "./query.js";
|
|
18
18
|
import { erasedStores } from "./erased-stores.js";
|
|
19
19
|
import { checkedTypes, CONVERSION_MEMBERS, valueReach } from "./value-reach.js";
|
|
20
20
|
/** The serializer every run reads, whatever the configuration names. */
|
|
@@ -25,6 +25,8 @@ const CONVERSIONS = new Set(["toJSON", "toString"]);
|
|
|
25
25
|
const SERIALIZED_CONVERSION = "toJSON";
|
|
26
26
|
/** The parameter types that keep nothing of the value they are given. */
|
|
27
27
|
const ERASED = TypeFlags.Any | TypeFlags.Unknown;
|
|
28
|
+
/** The standard library's promise types, whose value is their one type argument. */
|
|
29
|
+
const PROMISES = new Set(["Promise", "PromiseLike"]);
|
|
28
30
|
/** How deep a callee's result is searched for a type parameter: `Promise<Box<T>>` is two levels. */
|
|
29
31
|
const MENTION_DEPTH = 3;
|
|
30
32
|
/** The wider of two reaches, where either may be absent. */
|
|
@@ -135,9 +137,16 @@ function throughMethod(call) {
|
|
|
135
137
|
function walkProject(project, held) {
|
|
136
138
|
const sites = [];
|
|
137
139
|
const parameters = [];
|
|
140
|
+
const initialized = new Set();
|
|
138
141
|
for (const file of project.ownSourceFiles()) {
|
|
139
142
|
const visit = (node, self) => {
|
|
140
143
|
let inner = self;
|
|
144
|
+
if (isPropertyDeclaration(node) && node.initializer !== undefined) {
|
|
145
|
+
const id = held.declarations.get(nodeKey(file, node));
|
|
146
|
+
if (id !== undefined) {
|
|
147
|
+
initialized.add(id);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
141
150
|
if (isClassDeclaration(node) || isClassExpression(node)) {
|
|
142
151
|
inner = held.declarations.get(nodeKey(file, node));
|
|
143
152
|
}
|
|
@@ -171,7 +180,7 @@ function walkProject(project, held) {
|
|
|
171
180
|
visit(child, undefined);
|
|
172
181
|
});
|
|
173
182
|
}
|
|
174
|
-
return { sites, parameters };
|
|
183
|
+
return { sites, parameters, initialized };
|
|
175
184
|
}
|
|
176
185
|
/** The calls of `Object` that read every own property of their argument by name. */
|
|
177
186
|
const ENUMERATIONS = new Set([
|
|
@@ -182,15 +191,15 @@ const ENUMERATIONS = new Set([
|
|
|
182
191
|
"assign",
|
|
183
192
|
]);
|
|
184
193
|
/**
|
|
185
|
-
* Whether a parameter type keeps no member name of the value it is given
|
|
186
|
-
* or an object type of a string index signature
|
|
194
|
+
* Whether a parameter type keeps no member name of the value it is given beyond the
|
|
195
|
+
* properties it names: `unknown`, `any`, or an object type of a string index signature,
|
|
196
|
+
* under which every other member of the value is held.
|
|
187
197
|
*/
|
|
188
198
|
function keepsNoName(project, type) {
|
|
189
199
|
if ((type.flags & ERASED) !== 0) {
|
|
190
200
|
return true;
|
|
191
201
|
}
|
|
192
|
-
if ((type.flags & TypeFlags.Object) === 0
|
|
193
|
-
must(project.queries.propertiesOf(type)).length > 0) {
|
|
202
|
+
if ((type.flags & TypeFlags.Object) === 0) {
|
|
194
203
|
return false;
|
|
195
204
|
}
|
|
196
205
|
return must(project.queries.indexInfosOf(type)).some((info) => (info.keyType.flags & TypeFlags.String) !== 0);
|
|
@@ -711,6 +720,34 @@ function typeParametersOf(project, type, depth) {
|
|
|
711
720
|
...must(queries.typeArguments(type)),
|
|
712
721
|
].flatMap((one) => typeParametersOf(project, one, depth + 1));
|
|
713
722
|
}
|
|
723
|
+
/**
|
|
724
|
+
* The type parameters one type names, by identifier: through constituents, type arguments,
|
|
725
|
+
* an alias's arguments and the parameters and results of its call signatures, so a callback
|
|
726
|
+
* parameter's result names the parameter its callee's result does.
|
|
727
|
+
*/
|
|
728
|
+
function namedTypeParameters(project, type, depth) {
|
|
729
|
+
const { queries } = project;
|
|
730
|
+
const aliased = type.aliasTypeArguments;
|
|
731
|
+
if (type.isTypeParameter()) {
|
|
732
|
+
return [type.id];
|
|
733
|
+
}
|
|
734
|
+
if (depth >= MENTION_DEPTH) {
|
|
735
|
+
return aliased ?? [];
|
|
736
|
+
}
|
|
737
|
+
return [
|
|
738
|
+
...(aliased ?? []),
|
|
739
|
+
...[
|
|
740
|
+
...(type.isUnionType() || type.isIntersectionType() ? must(queries.constituents(type)) : []),
|
|
741
|
+
...must(queries.typeArguments(type)),
|
|
742
|
+
...must(queries.signaturesOf(type, SignatureKind.Call)).flatMap((signature) => [
|
|
743
|
+
...signature.parameters.flatMap((_parameter, at) => [
|
|
744
|
+
must(queries.typeAtPosition(signature, at)),
|
|
745
|
+
]),
|
|
746
|
+
...[must(queries.returnTypeOf(signature))].filter((one) => one !== undefined),
|
|
747
|
+
]),
|
|
748
|
+
].flatMap((one) => namedTypeParameters(project, one, depth + 1)),
|
|
749
|
+
];
|
|
750
|
+
}
|
|
714
751
|
/**
|
|
715
752
|
* The types an instantiated type puts where a declared type names `parameter`: matched
|
|
716
753
|
* through type arguments, and in a union every constituent the declared union does not
|
|
@@ -838,6 +875,10 @@ export function serializationContract(input) {
|
|
|
838
875
|
const holdsBack = (flags, name, to) => (flags & SymbolFlags.Property) !== 0 ||
|
|
839
876
|
((flags & CONVERSION_MEMBERS) !== 0 &&
|
|
840
877
|
(name === SERIALIZED_CONVERSION || (to.reach === "conversions" && CONVERSIONS.has(name))));
|
|
878
|
+
// The receiver reads only what the program wrote, so a data member holds while written.
|
|
879
|
+
const evidence = (id, flags, detail, site) => (flags & SymbolFlags.Property) !== 0 && !walked.initialized.has(id)
|
|
880
|
+
? { id, detail, site, whileWritten: true }
|
|
881
|
+
: { id, detail, site };
|
|
841
882
|
const found = [];
|
|
842
883
|
for (const flow of flows) {
|
|
843
884
|
const site = renderPosition(flow.site.getSourceFile(), targetRoot, flow.site.getStart());
|
|
@@ -846,16 +887,303 @@ export function serializationContract(input) {
|
|
|
846
887
|
const symbol = byId.get(member.id);
|
|
847
888
|
const name = symbol === undefined ? "" : (memberComponent(symbol, byId) ?? "");
|
|
848
889
|
if (holdsBack(member.flags, name, flow.to)) {
|
|
849
|
-
found.push(
|
|
890
|
+
found.push(evidence(member.id, member.flags, detail, site));
|
|
850
891
|
}
|
|
851
892
|
}
|
|
852
893
|
for (const member of flow.members) {
|
|
853
894
|
if (holdsBack(member.flags, member.name, flow.to)) {
|
|
854
895
|
for (const id of chains.declarationsOf(member)) {
|
|
855
|
-
found.push(
|
|
896
|
+
found.push(evidence(id, member.flags, detail, site));
|
|
856
897
|
}
|
|
857
898
|
}
|
|
858
899
|
}
|
|
859
900
|
}
|
|
860
901
|
return found;
|
|
861
902
|
}
|
|
903
|
+
/**
|
|
904
|
+
* The data members the decodes and copies of `files` fill, as {@link Decoded} names them.
|
|
905
|
+
* A generic declared outside the analysis, or only ambiently, fills the type that a type
|
|
906
|
+
* parameter of its own no parameter names binds, where the awaited result carries that
|
|
907
|
+
* type through its data members rather than only naming it, as a store's methods do: no
|
|
908
|
+
* argument hands the value over, so it fills it where the analysis cannot see, and may
|
|
909
|
+
* read it. Where the checker leaves a question unanswered, every data member is `unanswered`.
|
|
910
|
+
*/
|
|
911
|
+
export function decodedMembers(project, held, files, consumers) {
|
|
912
|
+
try {
|
|
913
|
+
return { ...decodedOf(project, held, files, consumers), unanswered: [] };
|
|
914
|
+
}
|
|
915
|
+
catch (error) {
|
|
916
|
+
if (!(error instanceof Unanswerable)) {
|
|
917
|
+
throw error;
|
|
918
|
+
}
|
|
919
|
+
return {
|
|
920
|
+
filled: [],
|
|
921
|
+
parsed: [],
|
|
922
|
+
copied: [],
|
|
923
|
+
unanswered: held.symbols
|
|
924
|
+
.filter((symbol) => symbol.kind === "class-member" || symbol.kind === "type-member")
|
|
925
|
+
.map((symbol) => symbol.id),
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
function decodedOf(project, held, files, consumers) {
|
|
930
|
+
const { queries } = project;
|
|
931
|
+
const own = project.ownPaths();
|
|
932
|
+
const inAnalysis = (path) => own.has(path) || consumers.some((root) => path === root || path.startsWith(`${root}/`));
|
|
933
|
+
const calls = [];
|
|
934
|
+
const untyped = [];
|
|
935
|
+
const spreads = [];
|
|
936
|
+
const noteRead = (node) => {
|
|
937
|
+
const at = readAs(node);
|
|
938
|
+
if (at.tops.length > 0 || at.asserted.length > 0) {
|
|
939
|
+
untyped.push(node);
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
const visit = (node) => {
|
|
943
|
+
if (isCallExpression(node)) {
|
|
944
|
+
calls.push(node);
|
|
945
|
+
noteRead(node);
|
|
946
|
+
}
|
|
947
|
+
else if (isPropertyAccessExpression(node) || isElementAccessExpression(node)) {
|
|
948
|
+
noteRead(node);
|
|
949
|
+
}
|
|
950
|
+
else if (isSpreadAssignment(node)) {
|
|
951
|
+
spreads.push(node);
|
|
952
|
+
}
|
|
953
|
+
node.forEachChild(visit);
|
|
954
|
+
};
|
|
955
|
+
for (const file of files) {
|
|
956
|
+
file.forEachChild(visit);
|
|
957
|
+
}
|
|
958
|
+
const filled = [];
|
|
959
|
+
const resolved = queries.resolvedSignatures(calls).map(must);
|
|
960
|
+
for (const signature of resolved) {
|
|
961
|
+
const declared = signature?.target === undefined ? undefined : must(queries.genericOf(signature));
|
|
962
|
+
const declaration = declared?.declaration;
|
|
963
|
+
const generic = declared?.typeParameters ?? [];
|
|
964
|
+
if (signature === undefined || declared === undefined || generic.length === 0) {
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
const node = declaration === undefined ? undefined : project.declarationAt(declaration)?.node;
|
|
968
|
+
if (declaration === undefined ||
|
|
969
|
+
(inAnalysis(declaration.path) &&
|
|
970
|
+
(node === undefined || (node.flags & NodeFlags.Ambient) === 0))) {
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
const result = must(queries.returnTypeOf(declared));
|
|
974
|
+
const given = must(queries.returnTypeOf(signature));
|
|
975
|
+
if (result === undefined || given === undefined) {
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
const passed = new Set(declared.parameters.flatMap((_parameter, at) => {
|
|
979
|
+
const type = must(queries.parameterType(declared, at));
|
|
980
|
+
return type === undefined ? [] : namedTypeParameters(project, type, 0);
|
|
981
|
+
}));
|
|
982
|
+
const types = typeParametersOf(project, result, 0)
|
|
983
|
+
.filter((parameter) => generic.includes(parameter.id) && !passed.has(parameter.id))
|
|
984
|
+
.flatMap((parameter) => bound(project, result, given, parameter, 0));
|
|
985
|
+
if (types.length > 0) {
|
|
986
|
+
filled.push({ carried: awaited(project, given), types });
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
const read = readTypes(project, untyped);
|
|
990
|
+
const copies = copiedNames(project, spreads, calls);
|
|
991
|
+
if (filled.length === 0 &&
|
|
992
|
+
read.parsed.length === 0 &&
|
|
993
|
+
read.made.length === 0 &&
|
|
994
|
+
copies.length === 0) {
|
|
995
|
+
return { filled: [], parsed: [], copied: [] };
|
|
996
|
+
}
|
|
997
|
+
const reach = valueReach(project, held);
|
|
998
|
+
const chains = aliasChains(project, held);
|
|
999
|
+
const members = (type) => {
|
|
1000
|
+
const reached = reach.reachedFrom(type);
|
|
1001
|
+
const ofClasses = classMembers(project, held, reached.classes, "instance");
|
|
1002
|
+
return [
|
|
1003
|
+
...[...reached.classes].flatMap((id) => (ofClasses.get(id) ?? []).flatMap((member) => (member.flags & SymbolFlags.Property) !== 0 ? [member.id] : [])),
|
|
1004
|
+
...reached.members.flatMap((member) => (member.flags & SymbolFlags.Property) !== 0 ? chains.declarationsOf(member) : []),
|
|
1005
|
+
];
|
|
1006
|
+
};
|
|
1007
|
+
return {
|
|
1008
|
+
filled: [
|
|
1009
|
+
...new Set([
|
|
1010
|
+
...filled.flatMap(({ carried, types }) => {
|
|
1011
|
+
const carries = new Set(members(carried));
|
|
1012
|
+
return types.flatMap((type) => members(type).filter((id) => carries.has(id)));
|
|
1013
|
+
}),
|
|
1014
|
+
...read.made.flatMap(members),
|
|
1015
|
+
]),
|
|
1016
|
+
],
|
|
1017
|
+
parsed: [...new Set(read.parsed.flatMap(members))],
|
|
1018
|
+
copied: [
|
|
1019
|
+
...new Set(copies.flatMap(({ into, names }) => must(queries.constituents(into)).flatMap((part) => must(queries.propertiesOf(part)).flatMap((property) => (property.flags & SymbolFlags.Property) !== 0 && names.has(property.name)
|
|
1020
|
+
? chains.declarationsOf(property)
|
|
1021
|
+
: [])))),
|
|
1022
|
+
],
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Where one value is read as a type: each `as` and angle-bracket assertion around it,
|
|
1027
|
+
* through parentheses, `await` and `!`, and the outermost expression unless a statement
|
|
1028
|
+
* of its own or the object or callee of a further access holds it. A `satisfies` check
|
|
1029
|
+
* is stepped through and types nothing, its expression's type being its operand's. A
|
|
1030
|
+
* callback's result is read as well where the call taking the callback is, which may
|
|
1031
|
+
* hand it on inside its result, as `map` does.
|
|
1032
|
+
*/
|
|
1033
|
+
function readAs(value, depth = 0) {
|
|
1034
|
+
const asserted = [];
|
|
1035
|
+
let at = value;
|
|
1036
|
+
for (;;) {
|
|
1037
|
+
const parent = at.parent;
|
|
1038
|
+
if (isAsExpression(parent) || isTypeAssertion(parent)) {
|
|
1039
|
+
asserted.push(parent.type);
|
|
1040
|
+
}
|
|
1041
|
+
else if (!isParenthesizedExpression(parent) &&
|
|
1042
|
+
!isAwaitExpression(parent) &&
|
|
1043
|
+
!isNonNullExpression(parent) &&
|
|
1044
|
+
!isSatisfiesExpression(parent)) {
|
|
1045
|
+
break;
|
|
1046
|
+
}
|
|
1047
|
+
at = parent;
|
|
1048
|
+
}
|
|
1049
|
+
const parent = at.parent;
|
|
1050
|
+
const unused = isExpressionStatement(parent) ||
|
|
1051
|
+
((isPropertyAccessExpression(parent) ||
|
|
1052
|
+
isElementAccessExpression(parent) ||
|
|
1053
|
+
isCallExpression(parent)) &&
|
|
1054
|
+
parent.expression === at);
|
|
1055
|
+
const call = depth < MENTION_DEPTH ? callTaking(at) : undefined;
|
|
1056
|
+
const outer = call === undefined ? undefined : readAs(call, depth + 1);
|
|
1057
|
+
return {
|
|
1058
|
+
asserted: [...asserted, ...(outer?.asserted ?? [])],
|
|
1059
|
+
tops: [...(unused ? [] : [at]), ...(outer?.tops ?? [])],
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
/** The call one value is returned to as a callback's result, an argument of that call. */
|
|
1063
|
+
function callTaking(value) {
|
|
1064
|
+
const parent = value.parent;
|
|
1065
|
+
let callback;
|
|
1066
|
+
if (isArrowFunction(parent) && parent.body === value) {
|
|
1067
|
+
callback = parent;
|
|
1068
|
+
}
|
|
1069
|
+
else if (isReturnStatement(parent)) {
|
|
1070
|
+
let at = parent.parent;
|
|
1071
|
+
while (!isFunctionLikeDeclaration(at) && !isSourceFile(at)) {
|
|
1072
|
+
at = at.parent;
|
|
1073
|
+
}
|
|
1074
|
+
callback = isArrowFunction(at) || isFunctionExpression(at) ? at : undefined;
|
|
1075
|
+
}
|
|
1076
|
+
let at = callback?.parent;
|
|
1077
|
+
while (at !== undefined && isParenthesizedExpression(at)) {
|
|
1078
|
+
at = at.parent;
|
|
1079
|
+
}
|
|
1080
|
+
return at !== undefined && isCallExpression(at) && at.expression !== callback ? at : undefined;
|
|
1081
|
+
}
|
|
1082
|
+
/** The global the standard library declares one name of `JSON` or `Object` as. */
|
|
1083
|
+
function standardGlobal(callee, global, name) {
|
|
1084
|
+
return isPropertyAccessExpression(callee) &&
|
|
1085
|
+
isIdentifier(callee.expression) &&
|
|
1086
|
+
callee.expression.text === global &&
|
|
1087
|
+
callee.name.text === name
|
|
1088
|
+
? callee.expression
|
|
1089
|
+
: undefined;
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* The types each value of `values` typed `any` or `unknown` is read as ({@link readAs}):
|
|
1093
|
+
* `parsed` for a `JSON.parse` call of the standard library, `made` for any other call or
|
|
1094
|
+
* property, whose maker the analysis cannot see. `JSON` must be the standard library's
|
|
1095
|
+
* global.
|
|
1096
|
+
*/
|
|
1097
|
+
function readTypes(project, values) {
|
|
1098
|
+
if (values.length === 0) {
|
|
1099
|
+
return { parsed: [], made: [] };
|
|
1100
|
+
}
|
|
1101
|
+
const own = project.ownPaths();
|
|
1102
|
+
const parses = values.map((value) => isCallExpression(value) ? standardGlobal(value.expression, "JSON", "parse") : undefined);
|
|
1103
|
+
const globals = parses.flatMap((global) => (global === undefined ? [] : [global]));
|
|
1104
|
+
const standard = new Set(project
|
|
1105
|
+
.symbolsAt(globals.map((global) => project.handle(global)))
|
|
1106
|
+
.map(must)
|
|
1107
|
+
.flatMap((symbol, at) => symbol?.declarations.every((handle) => !own.has(handle.path)) === true ? [globals[at]] : []));
|
|
1108
|
+
// An `await` around the value reads the value it resolves to.
|
|
1109
|
+
const valueTypes = typesAt(project, values.map((value) => (isAwaitExpression(value.parent) ? value.parent : value)));
|
|
1110
|
+
const sites = values.flatMap((value, at) => {
|
|
1111
|
+
const parse = parses[at];
|
|
1112
|
+
const parsed = parse !== undefined && standard.has(parse);
|
|
1113
|
+
const type = valueTypes[at];
|
|
1114
|
+
const untyped = type !== undefined && (type.flags & (TypeFlags.Any | TypeFlags.Unknown)) !== 0;
|
|
1115
|
+
return parsed || untyped ? [{ ...readAs(value), parsed }] : [];
|
|
1116
|
+
});
|
|
1117
|
+
// Each type node and each outermost expression, with whether a `JSON.parse` call is read as it.
|
|
1118
|
+
const typed = sites.flatMap((site) => site.asserted.map((node) => ({ node, parsed: site.parsed })));
|
|
1119
|
+
const placed = sites.flatMap((site) => site.tops.map((node) => ({ node, parsed: site.parsed })));
|
|
1120
|
+
const read = [
|
|
1121
|
+
...typesAt(project, typed.map((one) => one.node)).map((type, at) => ({ type, parsed: typed[at]?.parsed })),
|
|
1122
|
+
...project.queries
|
|
1123
|
+
.contextualTypes(placed.map((one) => one.node))
|
|
1124
|
+
.map(must)
|
|
1125
|
+
.map((type, at) => ({ type, parsed: placed[at]?.parsed })),
|
|
1126
|
+
];
|
|
1127
|
+
const typesWhere = (parsed) => read.flatMap((one) => (one.type !== undefined && one.parsed === parsed ? [one.type] : []));
|
|
1128
|
+
return { parsed: typesWhere(true), made: typesWhere(false) };
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* The copies the spreads and calls of `files` make: for each spread in an object literal,
|
|
1132
|
+
* the type the literal is read as, and for each `Object.assign` call of the standard
|
|
1133
|
+
* library, its first argument's type, with the member names of the copied values' types.
|
|
1134
|
+
*/
|
|
1135
|
+
function copiedNames(project, spreads, calls) {
|
|
1136
|
+
const own = project.ownPaths();
|
|
1137
|
+
const assigns = calls.flatMap((call) => {
|
|
1138
|
+
const global = standardGlobal(call.expression, "Object", "assign");
|
|
1139
|
+
const [target, ...sources] = call.arguments;
|
|
1140
|
+
return global === undefined || target === undefined || sources.length === 0
|
|
1141
|
+
? []
|
|
1142
|
+
: [{ global, target, sources: sources.filter((source) => !isSpreadElement(source)) }];
|
|
1143
|
+
});
|
|
1144
|
+
const standard = project
|
|
1145
|
+
.symbolsAt(assigns.map((one) => project.handle(one.global)))
|
|
1146
|
+
.map(must)
|
|
1147
|
+
.map((symbol) => symbol?.declarations.every((handle) => !own.has(handle.path)) === true);
|
|
1148
|
+
const assigned = assigns.filter((_one, at) => standard[at] === true);
|
|
1149
|
+
const literals = spreads.flatMap((spread) => isObjectLiteralExpression(spread.parent)
|
|
1150
|
+
? [{ literal: spread.parent, source: spread.expression }]
|
|
1151
|
+
: []);
|
|
1152
|
+
const contexts = project.queries.contextualTypes(literals.map((one) => one.literal)).map(must);
|
|
1153
|
+
const targets = typesAt(project, assigned.map((one) => one.target));
|
|
1154
|
+
const spreadTypes = typesAt(project, literals.map((one) => one.source));
|
|
1155
|
+
const sourceTypes = assigned.map((one) => typesAt(project, one.sources));
|
|
1156
|
+
const namesOf = (types) => new Set(types.flatMap((type) => type === undefined
|
|
1157
|
+
? []
|
|
1158
|
+
: must(project.queries.propertiesOf(type)).map((property) => property.name)));
|
|
1159
|
+
const found = [];
|
|
1160
|
+
const add = (into, names) => {
|
|
1161
|
+
if (into !== undefined && names.size > 0) {
|
|
1162
|
+
found.push({ into, names });
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
contexts.forEach((into, at) => {
|
|
1166
|
+
add(into, namesOf([spreadTypes[at]]));
|
|
1167
|
+
});
|
|
1168
|
+
targets.forEach((into, at) => {
|
|
1169
|
+
add(into, namesOf(sourceTypes[at] ?? []));
|
|
1170
|
+
});
|
|
1171
|
+
return found;
|
|
1172
|
+
}
|
|
1173
|
+
/** The value a promise of the standard library resolves to, through nested promises. */
|
|
1174
|
+
function awaited(project, type) {
|
|
1175
|
+
const { queries } = project;
|
|
1176
|
+
let at = type;
|
|
1177
|
+
for (let depth = 0; depth < MENTION_DEPTH; depth += 1) {
|
|
1178
|
+
const symbol = must(queries.symbolOfType(at));
|
|
1179
|
+
const [inner] = must(queries.typeArguments(at));
|
|
1180
|
+
const standard = symbol !== undefined &&
|
|
1181
|
+
PROMISES.has(symbol.name) &&
|
|
1182
|
+
symbol.declarations.every((handle) => project.program.getSourceFileMetadata(handle.path)?.isDefaultLibrary === true);
|
|
1183
|
+
if (!standard || inner === undefined) {
|
|
1184
|
+
break;
|
|
1185
|
+
}
|
|
1186
|
+
at = inner;
|
|
1187
|
+
}
|
|
1188
|
+
return at;
|
|
1189
|
+
}
|
|
@@ -259,7 +259,6 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
|
|
|
259
259
|
try {
|
|
260
260
|
host.writeDocument(documents.report, json(report));
|
|
261
261
|
const options = {
|
|
262
|
-
failOn: config.reporters.failOn,
|
|
263
262
|
readSource: (path) => host.readFile(joinPath(targetRoot, path)),
|
|
264
263
|
template,
|
|
265
264
|
};
|
|
@@ -74,24 +74,32 @@ export function runCommands(text, key = RUN_LINE) {
|
|
|
74
74
|
return found;
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* own, so a `cd` moves the directory of the later commands of its line alone.
|
|
77
|
+
* The commands of the recipe lines of the target root's makefile, each line's in order: a
|
|
78
|
+
* recipe line is one that starts with a tab, joined with the lines a trailing `\`
|
|
79
|
+
* continues it onto.
|
|
81
80
|
*/
|
|
82
|
-
function
|
|
81
|
+
function recipeLines(host, root) {
|
|
83
82
|
const path = MAKEFILES.map((name) => joinPath(root, name)).find((candidate) => host.kindOf(candidate) === "file");
|
|
84
83
|
const text = path === undefined ? undefined : readText(host, path);
|
|
85
84
|
if (text === undefined) {
|
|
86
85
|
return [];
|
|
87
86
|
}
|
|
87
|
+
return text
|
|
88
|
+
.replace(/\\\r?\n/gu, " ")
|
|
89
|
+
.split(/\r?\n/u)
|
|
90
|
+
.filter((line) => line.startsWith("\t"))
|
|
91
|
+
.map((line) => withoutComment(line.slice(1).replace(/^[@+-]+/u, "")).split(COMMAND_SEPARATORS));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Every token the recipe lines of the target root's makefile hold, each with the directory
|
|
95
|
+
* below the root it is read against: make runs each line in a shell of its own, so a `cd`
|
|
96
|
+
* moves the directory of the later commands of its line alone.
|
|
97
|
+
*/
|
|
98
|
+
function recipeTokens(host, root) {
|
|
88
99
|
const found = [];
|
|
89
|
-
for (const line of
|
|
90
|
-
if (!line.startsWith("\t")) {
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
100
|
+
for (const line of recipeLines(host, root)) {
|
|
93
101
|
let dir = root;
|
|
94
|
-
for (const command of
|
|
102
|
+
for (const command of line) {
|
|
95
103
|
const tokens = scriptTokens(command);
|
|
96
104
|
const [first, target, ...rest] = tokens;
|
|
97
105
|
if (first === "cd" && target !== undefined && rest.length === 0 && !COMPUTED.test(target)) {
|
|
@@ -131,13 +139,25 @@ function withoutComment(line) {
|
|
|
131
139
|
return line;
|
|
132
140
|
}
|
|
133
141
|
/**
|
|
134
|
-
* Every
|
|
142
|
+
* Every command the target root's workflow steps, git hooks and makefile run: the `run:`
|
|
135
143
|
* steps of the workflow files in `.github/workflows`, every line of each file in `.husky`
|
|
136
144
|
* and `.githooks`, the `entry:` commands of `.pre-commit-config.yaml`, the `run:` commands
|
|
137
145
|
* of a lefthook file and the makefile's recipe lines.
|
|
138
146
|
*/
|
|
147
|
+
export function workflowCommands(host, targetRoot) {
|
|
148
|
+
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
149
|
+
return [...stepCommands(host, root), ...recipeLines(host, root).flat()];
|
|
150
|
+
}
|
|
151
|
+
/** Every token the commands {@link workflowCommands} reads hold. */
|
|
139
152
|
export function workflowTokens(host, targetRoot) {
|
|
140
153
|
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
154
|
+
return [
|
|
155
|
+
...stepCommands(host, root).flatMap((command) => scriptTokens(command)),
|
|
156
|
+
...recipeTokens(host, root).map(({ token }) => token),
|
|
157
|
+
];
|
|
158
|
+
}
|
|
159
|
+
/** The commands of the workflow steps, git hooks and hook managers at one root. */
|
|
160
|
+
function stepCommands(host, root) {
|
|
141
161
|
const commands = [];
|
|
142
162
|
for (const path of filesIn(host, joinPath(root, WORKFLOWS), (name) => /\.ya?ml$/u.test(name))) {
|
|
143
163
|
commands.push(...runCommands(readText(host, path) ?? ""));
|
|
@@ -150,10 +170,7 @@ export function workflowTokens(host, targetRoot) {
|
|
|
150
170
|
for (const [name, key] of HOOK_MANAGERS) {
|
|
151
171
|
commands.push(...runCommands(readText(host, joinPath(root, name)) ?? "", key));
|
|
152
172
|
}
|
|
153
|
-
return
|
|
154
|
-
...commands.flatMap((command) => scriptTokens(command)),
|
|
155
|
-
...recipeTokens(host, root).map(({ token }) => token),
|
|
156
|
-
];
|
|
173
|
+
return commands;
|
|
157
174
|
}
|
|
158
175
|
/**
|
|
159
176
|
* The own files the target root's workflow steps and git hooks name, as
|