@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/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. A type name the
447
- * checker leaves unanswered publishes every declaration spelled as it is, as `guessed`. A
448
- * published file is not itself published.
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
- while (pending.length > 0) {
475
- const round = [];
476
- for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
477
- round.push(next);
478
- const exporting = EXPORTING_CONTAINERS.has(byId.get(next)?.kind ?? "");
479
- for (const member of children.get(next) ?? []) {
480
- const reachable = exporting
481
- ? member.exported
482
- : member.visibility !== "private" && member.visibility !== "private-name";
483
- if (member.kind !== "type-parameter" && reachable) {
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
- const named = round.flatMap((id) => {
489
- const symbol = byId.get(id);
490
- return symbol === undefined || symbol.kind === "file" ? [] : nodes.typeNamesOf(symbol);
491
- });
492
- const inferred = inferredTypeSymbols(project, round.flatMap((id) => {
493
- const symbol = byId.get(id);
494
- return symbol === undefined ? [] : nodes.nodesOf(symbol);
495
- }));
496
- for (const symbol of inferred.types) {
497
- for (const target of chains.chainOf(symbol)) {
498
- reach(target.id);
499
- }
500
- }
501
- for (const symbol of inferred.members) {
502
- for (const target of chains.chainOf(symbol)) {
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
- project.symbolsAt(named.map((node) => project.handle(node))).forEach((symbol, index) => {
510
- if (symbol === UNANSWERED) {
511
- for (const id of held.byName.get(named[index]?.getText() ?? "") ?? []) {
512
- guessed.add(id);
513
- reach(id);
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
- else if (symbol !== undefined) {
528
+ for (const symbol of inferred.members) {
517
529
  for (const target of chains.chainOf(symbol)) {
518
- reach(target.id);
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: `unknown`, `any`,
186
- * or an object type of a string index signature and no property.
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({ id: member.id, detail, site });
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({ id, detail, site });
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
- * Every token the recipe lines of the target root's makefile hold, each with the directory
78
- * below the root it is read against: a recipe line is one that starts with a tab, joined
79
- * with the lines a trailing `\` continues it onto, and make runs each in a shell of its
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 recipeTokens(host, root) {
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 text.replace(/\\\r?\n/gu, " ").split(/\r?\n/u)) {
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 withoutComment(line.slice(1).replace(/^[@+-]+/u, "")).split(COMMAND_SEPARATORS)) {
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 token the target root's workflow steps, git hooks and makefile hold: the `run:`
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
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "8.0.0-dev.3",
3
+ "version": "8.0.0-dev.4",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"