@cplieger/deadset-ts 7.2.0-dev.2 → 7.2.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.
Files changed (65) hide show
  1. package/dist/bin/mapper-connection.js +7 -4
  2. package/dist/src/analysis.js +10 -6
  3. package/dist/src/component-files.js +96 -31
  4. package/dist/src/configuration-files.js +5 -3
  5. package/dist/src/configured-declarations.js +24 -2
  6. package/dist/src/conformance-record.js +1 -1
  7. package/dist/src/contextual-uses.js +26 -26
  8. package/dist/src/dependencies.js +2 -2
  9. package/dist/src/destructuring.js +77 -2
  10. package/dist/src/discover.js +68 -86
  11. package/dist/src/edges.js +1 -1
  12. package/dist/src/intra-function-parts.js +17 -6
  13. package/dist/src/inventory.js +11 -0
  14. package/dist/src/json-document.js +5 -5
  15. package/dist/src/namespace-objects.js +28 -18
  16. package/dist/src/overrides.js +7 -2
  17. package/dist/src/paths.js +9 -0
  18. package/dist/src/references.js +25 -12
  19. package/dist/src/reflective-lookup.js +8 -6
  20. package/dist/src/reporters/template.js +22 -13
  21. package/dist/src/serialization-contract.js +43 -12
  22. package/dist/src/session.js +19 -1
  23. package/dist/src/spelled-names.js +58 -0
  24. package/dist/src/suppress-file.js +2 -2
  25. package/dist/src/suppress-inline.js +3 -1
  26. package/dist/src/template-field.js +9 -6
  27. package/dist/src/type-errors.js +82 -28
  28. package/dist/src/value-reach.js +17 -5
  29. package/dist/src/verbs/analyze.js +3 -1
  30. package/dist/src/version-range.js +10 -5
  31. package/dist/src/workspace-resolution.js +19 -3
  32. package/dist/src/workspace.js +1 -1
  33. package/jsr.json +1 -1
  34. package/package.json +2 -2
  35. package/src/analysis.ts +32 -6
  36. package/src/component-files.ts +116 -31
  37. package/src/configuration-files.ts +15 -6
  38. package/src/configured-declarations.ts +33 -4
  39. package/src/conformance-record.ts +1 -1
  40. package/src/contextual-uses.ts +27 -27
  41. package/src/dependencies.ts +2 -2
  42. package/src/destructuring.ts +93 -2
  43. package/src/discover.ts +92 -87
  44. package/src/edges.ts +1 -1
  45. package/src/intra-function-parts.ts +21 -5
  46. package/src/inventory.ts +12 -0
  47. package/src/json-document.ts +7 -7
  48. package/src/namespace-objects.ts +34 -21
  49. package/src/overrides.ts +10 -2
  50. package/src/paths.ts +10 -0
  51. package/src/references.ts +28 -16
  52. package/src/reflective-lookup.ts +9 -7
  53. package/src/reporters/template.ts +21 -13
  54. package/src/serialization-contract.ts +53 -13
  55. package/src/session.ts +22 -1
  56. package/src/spelled-names.ts +102 -0
  57. package/src/suppress-file.ts +2 -2
  58. package/src/suppress-inline.ts +3 -1
  59. package/src/template-field.ts +9 -6
  60. package/src/type-errors.ts +94 -35
  61. package/src/value-reach.ts +19 -6
  62. package/src/verbs/analyze.ts +6 -1
  63. package/src/version-range.ts +10 -5
  64. package/src/workspace-resolution.ts +24 -4
  65. package/src/workspace.ts +1 -1
@@ -793,7 +793,7 @@ class Execution {
793
793
  case "lit":
794
794
  return arg.value;
795
795
  case "pipe":
796
- return this.pipeline(arg.pipe, dot, false);
796
+ return this.pipeline(arg.pipe, dot);
797
797
  case "field":
798
798
  return this.field(arg.recv === undefined ? dot : this.arg(arg.recv, dot), arg.names);
799
799
  case "func":
@@ -837,19 +837,18 @@ class Execution {
837
837
  }
838
838
  return call(head.name, operands());
839
839
  }
840
- pipeline(pipe, dot, declares) {
840
+ pipeline(pipe, dot) {
841
841
  let value;
842
842
  for (const command of pipe.commands) {
843
843
  value = this.command(command, dot, value);
844
844
  }
845
845
  const result = value ?? null;
846
- const [only] = pipe.vars;
847
- if (only !== undefined && (declares || pipe.assign)) {
846
+ for (const name of pipe.vars) {
848
847
  if (pipe.assign) {
849
- this.assign(only, result);
848
+ this.assign(name, result);
850
849
  }
851
850
  else {
852
- this.scopes.at(-1)?.set(only, result);
851
+ this.scopes.at(-1)?.set(name, result);
853
852
  }
854
853
  }
855
854
  return result;
@@ -861,7 +860,7 @@ class Execution {
861
860
  this.write(node.text);
862
861
  break;
863
862
  case "action": {
864
- const value = this.pipeline(node.pipe, dot, true);
863
+ const value = this.pipeline(node.pipe, dot);
865
864
  if (node.pipe.vars.length === 0) {
866
865
  this.write(formatted(value, false));
867
866
  }
@@ -874,7 +873,7 @@ class Execution {
874
873
  case "with": {
875
874
  this.scopes.push(new Map());
876
875
  try {
877
- const value = this.pipeline(node.pipe, dot, true);
876
+ const value = this.pipeline(node.pipe, dot);
878
877
  const taken = truthy(value);
879
878
  this.run(taken ? node.list : node.otherwise, taken && node.k === "with" ? value : dot);
880
879
  }
@@ -892,7 +891,9 @@ class Execution {
892
891
  range(node, dot) {
893
892
  this.scopes.push(new Map());
894
893
  try {
895
- const value = this.pipeline({ ...node.pipe, vars: [] }, dot, false);
894
+ // Go's `evalPipeline` gives every variable the range declares or assigns the whole value first:
895
+ // https://github.com/golang/go/blob/go1.27.2/src/text/template/exec.go#L525
896
+ const value = this.pipeline(node.pipe, dot);
896
897
  let entries;
897
898
  if (value === null) {
898
899
  entries = [];
@@ -913,14 +914,22 @@ class Execution {
913
914
  return;
914
915
  }
915
916
  const [first, second] = node.pipe.vars;
917
+ const scope = this.scopes.at(-1);
918
+ const bind = (name, held) => {
919
+ if (node.pipe.assign) {
920
+ this.assign(name, held);
921
+ }
922
+ else {
923
+ scope?.set(name, held);
924
+ }
925
+ };
916
926
  for (const [key, element] of entries) {
917
- const scope = this.scopes.at(-1);
918
927
  if (second !== undefined && first !== undefined) {
919
- scope?.set(first, key);
920
- scope?.set(second, element);
928
+ bind(first, key);
929
+ bind(second, element);
921
930
  }
922
931
  else if (first !== undefined) {
923
- scope?.set(first, element);
932
+ bind(first, element);
924
933
  }
925
934
  try {
926
935
  this.run(node.list, element);
@@ -16,7 +16,7 @@ import { nodeKey } from "./inventory.js";
16
16
  import { renderPosition } from "./position.js";
17
17
  import { must } from "./query.js";
18
18
  import { erasedStores } from "./erased-stores.js";
19
- import { checkedTypes, valueReach } from "./value-reach.js";
19
+ import { checkedTypes, CONVERSION_MEMBERS, valueReach } from "./value-reach.js";
20
20
  /** The serializer every run reads, whatever the configuration names. */
21
21
  const JSON_STRINGIFY = { shape: "global", global: "JSON.stringify" };
22
22
  /** The methods a serializer and a formatter resolve by name on a value that left the analysis. */
@@ -313,13 +313,14 @@ function destinations(input, chains, walked) {
313
313
  const inAnalysis = (path) => own.has(path) || input.consumers.some((root) => path === root || path.startsWith(`${root}/`));
314
314
  const implemented = implementedOwners(project, inAnalysis);
315
315
  /**
316
- * Whether every declaration of a callee is outside the analysis or ambient. An ambient
317
- * declaration has no body, so the code that runs is outside the analysis, unless it is a
318
- * member of an interface or an object type a class or a typed object literal of the
319
- * analysis implements; `settled` false skips that question, which asks the checker.
316
+ * Whether every one of `declarations`, all of the callee's by default, is outside the
317
+ * analysis or ambient. An ambient declaration has no body, so the code that runs is
318
+ * outside the analysis, unless it is a member of an interface or an object type a class
319
+ * or a typed object literal of the analysis implements; `settled` false skips that
320
+ * question, which asks the checker.
320
321
  */
321
- const outside = (target, settled) => target.declarations.length > 0 &&
322
- target.declarations.every((handle) => {
322
+ const outside = (target, settled, declarations = target.declarations) => declarations.length > 0 &&
323
+ declarations.every((handle) => {
323
324
  if (!inAnalysis(handle.path)) {
324
325
  return true;
325
326
  }
@@ -333,6 +334,12 @@ function destinations(input, chains, walked) {
333
334
  const owner = must(queries.parentOf(target));
334
335
  return owner === undefined || !implemented().has(owner.id);
335
336
  });
337
+ /**
338
+ * Whether some one declaration of a callee is outside the analysis or ambient, without
339
+ * asking the checker. A project's declaration merged into a library's symbol leaves the
340
+ * library's beside it, so a call can still resolve to the library's.
341
+ */
342
+ const mayBeOutside = (target) => target.declarations.some((handle) => outside(target, false, [handle]));
336
343
  const invocation = (call) => {
337
344
  const form = forms.get(call);
338
345
  const method = form === undefined ? undefined : callees.get(form.method);
@@ -340,7 +347,7 @@ function destinations(input, chains, walked) {
340
347
  // program's own `call` and `Reflect.apply` invoke nothing here.
341
348
  if (form !== undefined &&
342
349
  method !== undefined &&
343
- outside(method, false) &&
350
+ mayBeOutside(method) &&
344
351
  FUNCTION_INTERFACES.has(must(project.queries.parentOf(method))?.name ?? "")) {
345
352
  return form.invocation;
346
353
  }
@@ -350,6 +357,30 @@ function destinations(input, chains, walked) {
350
357
  const name = calleeName(invocation(call).callee);
351
358
  return name === undefined ? undefined : callees.get(name);
352
359
  };
360
+ const signatures = new Map();
361
+ const resolvedOf = (call) => {
362
+ if (!signatures.has(call)) {
363
+ signatures.set(call, must(queries.resolvedSignature(call)));
364
+ }
365
+ return signatures.get(call);
366
+ };
367
+ /**
368
+ * The declarations of a callee that the declaration of a direct call's resolved
369
+ * signature lies in, or all of them where it lies in none: a declaration merged into
370
+ * the callee's symbol that the call does not resolve to does not decide where it runs.
371
+ */
372
+ const calledDeclarations = (call, target) => {
373
+ const declaration = invocation(call).indirect ? undefined : resolvedOf(call)?.declaration;
374
+ const signature = declaration === undefined ? undefined : project.declarationAt(declaration)?.node;
375
+ if (declaration === undefined || signature === undefined) {
376
+ return target.declarations;
377
+ }
378
+ const containing = target.declarations.filter((handle) => {
379
+ const node = handle.path === declaration.path ? project.declarationAt(handle)?.node : undefined;
380
+ return node !== undefined && node.pos <= signature.pos && signature.end <= node.end;
381
+ });
382
+ return containing.length === 0 ? target.declarations : containing;
383
+ };
353
384
  const erasedArguments = new Map();
354
385
  const erasedAt = (call, argument) => {
355
386
  let indexes = erasedArguments.get(call);
@@ -362,7 +393,7 @@ function destinations(input, chains, walked) {
362
393
  ? type === undefined
363
394
  ? []
364
395
  : must(queries.signaturesOf(type, SignatureKind.Call))
365
- : [must(queries.resolvedSignature(call))].filter((one) => one !== undefined);
396
+ : [resolvedOf(call)].filter((one) => one !== undefined);
366
397
  indexes = new Set(invoked.arguments.flatMap((_unused, index) => signatures.some((signature) => {
367
398
  const parameter = must(queries.parameterType(signature, index));
368
399
  return parameter !== undefined && (parameter.flags & ERASED) !== 0;
@@ -384,7 +415,7 @@ function destinations(input, chains, walked) {
384
415
  if (entry !== undefined) {
385
416
  return { reach: "properties", callee: spelledEntry(entry.entry, held) };
386
417
  }
387
- if (outside(target, true)) {
418
+ if (outside(target, true, calledDeclarations(call, target))) {
388
419
  return erasedAt(call, argument)
389
420
  ? {
390
421
  reach: "conversions",
@@ -419,7 +450,7 @@ function destinations(input, chains, walked) {
419
450
  if (target === undefined) {
420
451
  return false;
421
452
  }
422
- if (named.some((one) => names(one, target, chains)) || outside(target, false)) {
453
+ if (named.some((one) => names(one, target, chains)) || mayBeOutside(target)) {
423
454
  return true;
424
455
  }
425
456
  return chains
@@ -509,7 +540,7 @@ export function serializationContract(input) {
509
540
  // A destination holds back a data member and `toJSON`, and `toString` as well where
510
541
  // the value left the analysis.
511
542
  const holdsBack = (flags, name, to) => (flags & SymbolFlags.Property) !== 0 ||
512
- ((flags & SymbolFlags.Method) !== 0 &&
543
+ ((flags & CONVERSION_MEMBERS) !== 0 &&
513
544
  (name === SERIALIZED_CONVERSION || (to.reach === "conversions" && CONVERSIONS.has(name))));
514
545
  const found = [];
515
546
  for (const flow of flows) {
@@ -189,12 +189,30 @@ export function ownedAs(project, own) {
189
189
  },
190
190
  };
191
191
  }
192
- /** The three diagnostic sets one project carries, read before any analysis. */
192
+ /**
193
+ * The program's diagnostics placed in no file. The per-file sets never carry one; the
194
+ * client answers its `fileName` as `null`, and the global set repeats the program set:
195
+ * https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/api/session.go#L3726-L3752
196
+ */
197
+ function unplacedOf(program) {
198
+ const seen = new Set();
199
+ return [...program.getGlobalDiagnostics(), ...program.getProgramDiagnostics()]
200
+ .filter((diagnostic) => diagnostic.fileName == null)
201
+ .filter((diagnostic) => {
202
+ const key = `${String(diagnostic.code)}\u0000${diagnostic.text}`;
203
+ const fresh = !seen.has(key);
204
+ seen.add(key);
205
+ return fresh;
206
+ })
207
+ .map((diagnostic) => ({ ...diagnostic, fileName: undefined }));
208
+ }
209
+ /** The diagnostic sets one project carries, read before any analysis. */
193
210
  export function diagnosticsOf(project) {
194
211
  return {
195
212
  syntactic: project.program.getSyntacticDiagnostics(),
196
213
  semantic: project.program.getSemanticDiagnostics(),
197
214
  configParsing: project.program.getConfigFileParsingDiagnostics(),
215
+ unplaced: unplacedOf(project.program),
198
216
  locate: (fileName, offset) => {
199
217
  const file = project.program.getSourceFile(fileName);
200
218
  if (file === undefined || offset < 0 || offset > file.text.length) {
@@ -0,0 +1,58 @@
1
+ import { isBindingElement, isBreakStatement, isClassDeclaration, isClassExpression, isContinueStatement, isEnumDeclaration, isEnumMember, isExportDeclaration, isExportSpecifier, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isImportClause, isImportEqualsDeclaration, isImportSpecifier, isInterfaceDeclaration, isJsxAttribute, isLabeledStatement, isMetaProperty, isMethodDeclaration, isMethodSignatureDeclaration, isModuleDeclaration, isNamespaceExport, isNamespaceExportDeclaration, isNamespaceImport, isParameterDeclaration, isPropertyAccessExpression, isPropertyAssignment, isPropertyDeclaration, isPropertySignatureDeclaration, isQualifiedName, isSetAccessorDeclaration, isTypeAliasDeclaration, isTypeParameterDeclaration, isVariableDeclaration, } from "@typescript/native/ast";
2
+ /**
3
+ * Whether one identifier spells a name without referring to a binding of it: a member's
4
+ * name after a dot, a property or member an object literal, a class, a type or an enum
5
+ * declares, the name a declaration binds, the property a binding element destructures,
6
+ * an import or export specifier's name in another module or under a rename, a JSX
7
+ * attribute's name, a statement label, or the name of a meta-property. A shorthand
8
+ * property and an unrenamed local export are no such spelling: each reads the binding.
9
+ */
10
+ export function spellsNoBinding(node) {
11
+ const parent = node.parent;
12
+ if (isPropertyAccessExpression(parent) || isMetaProperty(parent)) {
13
+ return parent.name === node;
14
+ }
15
+ if (isQualifiedName(parent)) {
16
+ return parent.right === node;
17
+ }
18
+ if (isBindingElement(parent)) {
19
+ return parent.propertyName === node || parent.name === node;
20
+ }
21
+ if (isImportSpecifier(parent)) {
22
+ return true;
23
+ }
24
+ if (isExportSpecifier(parent)) {
25
+ return ((isExportDeclaration(parent.parent.parent) &&
26
+ parent.parent.parent.moduleSpecifier !== undefined) ||
27
+ (parent.propertyName !== undefined && parent.name === node));
28
+ }
29
+ if (isPropertyAssignment(parent) ||
30
+ isPropertyDeclaration(parent) ||
31
+ isMethodDeclaration(parent) ||
32
+ isPropertySignatureDeclaration(parent) ||
33
+ isMethodSignatureDeclaration(parent) ||
34
+ isGetAccessorDeclaration(parent) ||
35
+ isSetAccessorDeclaration(parent) ||
36
+ isEnumMember(parent) ||
37
+ isJsxAttribute(parent) ||
38
+ isVariableDeclaration(parent) ||
39
+ isParameterDeclaration(parent) ||
40
+ isFunctionDeclaration(parent) ||
41
+ isFunctionExpression(parent) ||
42
+ isClassDeclaration(parent) ||
43
+ isClassExpression(parent) ||
44
+ isInterfaceDeclaration(parent) ||
45
+ isTypeAliasDeclaration(parent) ||
46
+ isEnumDeclaration(parent) ||
47
+ isModuleDeclaration(parent) ||
48
+ isTypeParameterDeclaration(parent) ||
49
+ isImportClause(parent) ||
50
+ isNamespaceImport(parent) ||
51
+ isImportEqualsDeclaration(parent) ||
52
+ isNamespaceExport(parent) ||
53
+ isNamespaceExportDeclaration(parent)) {
54
+ return parent.name === node;
55
+ }
56
+ return ((isLabeledStatement(parent) || isBreakStatement(parent) || isContinueStatement(parent)) &&
57
+ parent.label === node);
58
+ }
@@ -51,7 +51,7 @@ function isObject(value) {
51
51
  */
52
52
  function readRecord(value, site, byReference, shape) {
53
53
  const refuse = (text, want) => {
54
- throw new SuppressionError(shape.mechanism, `${shape.file} ${siteText(site)}`, text, want);
54
+ throw new SuppressionError(shape.mechanism, siteText(site), text, want);
55
55
  };
56
56
  if (!isObject(value)) {
57
57
  return refuse(JSON.stringify(value), shape.recordWant);
@@ -159,7 +159,7 @@ function readDocument(host, targetRoot, symbols, shape) {
159
159
  const records = [];
160
160
  const refusals = [];
161
161
  held.forEach((value, index) => {
162
- const offset = walk.opened[index];
162
+ const offset = walk.starts[index];
163
163
  const site = offset === undefined ? documentSite(shape) : positionAt(text, shape.file, offset);
164
164
  const read = readRecord(value, site, byReference, shape);
165
165
  records.push(...read.records);
@@ -100,7 +100,9 @@ function lineComments(file) {
100
100
  const spans = [];
101
101
  const visit = (node) => {
102
102
  if (LITERALS.has(node.kind)) {
103
- spans.push({ start: tokenStart(text, node.pos), end: node.end });
103
+ // JSX text carries no trivia: its first code unit is at `pos`, even a `//`.
104
+ const start = node.kind === SyntaxKind.JsxText ? node.pos : tokenStart(text, node.pos);
105
+ spans.push({ start, end: node.end });
104
106
  }
105
107
  node.forEachChild(visit);
106
108
  };
@@ -14,7 +14,7 @@
14
14
  import { readComponent } from "./component-files.js";
15
15
  import { ConfigError } from "./config.js";
16
16
  import { memberNames } from "./member-names.js";
17
- import { isAbsolutePath, joinPath, normalizePath, relativePath } from "./paths.js";
17
+ import { extensionOf, isAbsolutePath, joinPath, normalizePath, relativePath } from "./paths.js";
18
18
  import { isComponentFile } from "./position.js";
19
19
  /** The kinds of declaration an action can name. */
20
20
  const READ_BY_A_TEMPLATE = new Set([
@@ -87,7 +87,10 @@ function stringEnd(text, from) {
87
87
  }
88
88
  return at + 1;
89
89
  }
90
- /** The end of the `${` substitution whose expression starts at `from`, past its closing brace. */
90
+ /**
91
+ * The end of the `${` substitution whose expression starts at `from`: past its closing brace,
92
+ * or one past the text's end where none closes it, so the expression is the text to its end.
93
+ */
91
94
  function substitutionEnd(text, from) {
92
95
  let depth = 1;
93
96
  let at = from;
@@ -112,7 +115,7 @@ function substitutionEnd(text, from) {
112
115
  }
113
116
  at += 1;
114
117
  }
115
- return at;
118
+ return text.length + 1;
116
119
  }
117
120
  /** The end of the template literal at `from`; each `${…}` expression goes to `substitution`. */
118
121
  function templateEnd(text, from, substitution) {
@@ -198,8 +201,8 @@ function namesOf(text, delimiters) {
198
201
  }
199
202
  }
200
203
  /** Every identifier the markup of one component file writes in an action. */
201
- function markupNamesOf(text) {
202
- return readComponent(text).markup.flatMap((one) => identifiersOf(one.text, true).map(({ name, offset }) => ({
204
+ function markupNamesOf(file) {
205
+ return readComponent(file.text, extensionOf(file.path)).markup.flatMap((one) => identifiersOf(one.text, true).map(({ name, offset }) => ({
203
206
  name,
204
207
  offset: one.start + offset,
205
208
  action: one.action,
@@ -238,7 +241,7 @@ export function templateField(input) {
238
241
  names: namesOf(file.text, templates.delimiters),
239
242
  markup: false,
240
243
  })),
241
- ...components.map((file) => ({ file, names: markupNamesOf(file.text), markup: true })),
244
+ ...components.map((file) => ({ file, names: markupNamesOf(file), markup: true })),
242
245
  ];
243
246
  for (const { file, names, markup } of scanned) {
244
247
  for (const named of names) {
@@ -2,8 +2,8 @@
2
2
  * What the errors a project's own files carry do to the run. An import of a module the
3
3
  * project expects and nothing provides is a setup failure. Any other error in an own file
4
4
  * is a type error: the function, method or file-level statement holding it is skipped,
5
- * which only ever withholds a finding. An error the compiler places in no file is read as
6
- * a project that cannot be analyzed.
5
+ * which only ever withholds a finding. `projectErrors` decides, before any of these, which
6
+ * errors the compiler places in no file leave the project unloaded.
7
7
  */
8
8
  import { isClassDeclaration, isNoSubstitutionTemplateLiteral, isPropertyAccessExpression, isStringLiteral, } from "@typescript/native/ast";
9
9
  import { packageOfSpecifier } from "./dependencies.js";
@@ -23,7 +23,7 @@ const MANIFEST_SECTIONS = [
23
23
  export function insideSkipped(skipped, position) {
24
24
  return skipped.some((unit) => unit.path === position.path && unit.fromLine <= position.line && position.line <= unit.toLine);
25
25
  }
26
- /** The string literal holding one offset, innermost first. */
26
+ /** The string literal holding one offset, innermost first, a doc comment's included. */
27
27
  function literalAt(file, offset) {
28
28
  let found;
29
29
  const visit = (node) => {
@@ -33,32 +33,55 @@ function literalAt(file, offset) {
33
33
  if (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node)) {
34
34
  found = node.text;
35
35
  }
36
+ // `forEachChild` does not enter a doc comment, where a JavaScript file imports types.
37
+ for (const doc of node.jsDoc ?? []) {
38
+ visit(doc);
39
+ }
36
40
  node.forEachChild(visit);
37
41
  };
38
42
  file.forEachChild(visit);
39
43
  return found;
40
44
  }
41
- /** Whether one `paths` key matches one specifier: exactly, or around its one `*`. */
45
+ /**
46
+ * What one `paths` key's `*` matches in one specifier, `""` for a key with no `*` that
47
+ * is the specifier, or `undefined` where the key does not match it.
48
+ */
42
49
  function mappedBy(key, specifier) {
43
50
  const star = key.indexOf("*");
44
51
  if (star < 0) {
45
- return key === specifier;
52
+ return key === specifier ? "" : undefined;
46
53
  }
47
54
  const prefix = key.slice(0, star);
48
55
  const suffix = key.slice(star + 1);
49
- return (specifier.length >= prefix.length + suffix.length &&
56
+ return specifier.length >= prefix.length + suffix.length &&
50
57
  specifier.startsWith(prefix) &&
51
- specifier.endsWith(suffix));
58
+ specifier.endsWith(suffix)
59
+ ? specifier.slice(prefix.length, specifier.length - suffix.length)
60
+ : undefined;
61
+ }
62
+ /**
63
+ * One `paths` target, its first `*` replaced by `star` taken literally, as the compiler does:
64
+ * https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/module/resolver.go#L1282
65
+ */
66
+ function substituted(target, star) {
67
+ const at = target.indexOf("*");
68
+ return at < 0 ? target : target.slice(0, at) + star + target.slice(at + 1);
52
69
  }
53
- /** Whether a relative import's path names something on disk, as written or with any extension. */
54
- function provided(host, path) {
55
- if (host.kindOf(path) !== "absent") {
70
+ /**
71
+ * Whether a path names something on disk, as written or with any extension; under
72
+ * `fileOnly`, only a file counts.
73
+ */
74
+ function provided(host, path, fileOnly) {
75
+ const counts = (at) => fileOnly ? host.kindOf(at) === "file" : host.kindOf(at) !== "absent";
76
+ if (counts(path)) {
56
77
  return true;
57
78
  }
58
79
  const dir = dirnamePath(path);
59
80
  const stem = `${path.slice(dir.length + 1)}.`;
60
81
  try {
61
- return host.readDirectory(dir).some((entry) => entry.name.startsWith(stem));
82
+ return host
83
+ .readDirectory(dir)
84
+ .some((entry) => entry.name.startsWith(stem) && counts(joinPath(dir, entry.name)));
62
85
  }
63
86
  catch {
64
87
  return false;
@@ -90,6 +113,20 @@ function declaringManifest(host, dir, targetRoot, name) {
90
113
  }
91
114
  }
92
115
  }
116
+ /**
117
+ * Whether a package is installed for an importer in `dir`: in the `node_modules` of
118
+ * `dir` or of any ancestor, above the target root too, as Node and the compiler look.
119
+ */
120
+ function installed(host, dir, name) {
121
+ for (let at = dir;; at = dirnamePath(at)) {
122
+ if (host.kindOf(joinPath(at, "node_modules", name)) === "directory") {
123
+ return true;
124
+ }
125
+ if (dirnamePath(at) === at) {
126
+ return false;
127
+ }
128
+ }
129
+ }
93
130
  /**
94
131
  * Why one import that the compiler cannot resolve names a module the project expects,
95
132
  * with the fix, or undefined where the module is one the project does not expect.
@@ -98,17 +135,31 @@ function expectedModule(context, importer, specifier) {
98
135
  const { host, targetRoot } = context;
99
136
  const fix = "run the generator or the build that writes it";
100
137
  if (specifier.startsWith(".") || isAbsolutePath(specifier)) {
101
- return provided(host, resolvePath(dirnamePath(importer), specifier))
138
+ return provided(host, resolvePath(dirnamePath(importer), specifier), false)
102
139
  ? undefined
103
140
  : `which names no file of the target: ${fix}`;
104
141
  }
105
142
  if (specifier.startsWith("#")) {
106
143
  return `which the manifest's imports map to no file: ${fix}`;
107
144
  }
108
- if (context.paths.some((key) => mappedBy(key, specifier))) {
109
- return `which the compiler configuration's paths map to no file: ${fix}`;
110
- }
145
+ const mapped = context.paths.entries.flatMap(({ key, targets }) => {
146
+ const star = mappedBy(key, specifier);
147
+ return star === undefined
148
+ ? []
149
+ : targets.map((target) => resolvePath(context.paths.base, substituted(target, star)));
150
+ });
111
151
  const name = packageOfSpecifier(specifier);
152
+ // The compiler looks up in `node_modules` what `paths` does not resolve, so the error
153
+ // of an installed package's import is the import's own, a subpath its exports omit:
154
+ // https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/module/resolver.go#L548-L575
155
+ if (name !== undefined && installed(host, dirnamePath(importer), name)) {
156
+ return undefined;
157
+ }
158
+ if (mapped.length > 0) {
159
+ return mapped.some((path) => provided(host, path, true))
160
+ ? undefined
161
+ : `which the compiler configuration's paths map to no file: ${fix}`;
162
+ }
112
163
  const manifest = name === undefined
113
164
  ? undefined
114
165
  : declaringManifest(host, dirnamePath(importer), targetRoot, name);
@@ -143,14 +194,8 @@ export function readErrors(project, errors, context) {
143
194
  const files = new Map(project.ownSourceFiles().map((file) => [file.fileName, file]));
144
195
  const missing = [];
145
196
  const skips = [];
146
- const failing = [];
147
- for (const located of errors) {
148
- const { diagnostic, at } = located;
149
- if (diagnostic.fileName === undefined) {
150
- failing.push(located);
151
- continue;
152
- }
153
- const file = files.get(diagnostic.fileName);
197
+ for (const { diagnostic, at } of errors) {
198
+ const file = diagnostic.fileName === undefined ? undefined : files.get(diagnostic.fileName);
154
199
  if (file === undefined || diagnostic.reportsUnnecessary === true) {
155
200
  continue;
156
201
  }
@@ -176,7 +221,7 @@ export function readErrors(project, errors, context) {
176
221
  });
177
222
  }
178
223
  }
179
- return { missing, skips, failing };
224
+ return { missing, skips };
180
225
  }
181
226
  /** The lines each skip withholds, below the target root. */
182
227
  export function skippedUnits(skips, targetRoot) {
@@ -241,13 +286,22 @@ export function skippedRoots(project, held, skips) {
241
286
  }
242
287
  return [...kept];
243
288
  }
244
- /** The keys of one compiler configuration's `paths` mapping, none where it cannot be read. */
289
+ /** One compiler configuration's `paths` mapping, empty where it cannot be read. */
245
290
  export function pathsOf(engine, configFile) {
291
+ const base = dirnamePath(configFile);
246
292
  try {
247
- const paths = engine.parseConfigFile(configFile).options.paths;
248
- return typeof paths === "object" && paths !== null ? Object.keys(paths) : [];
293
+ const { options } = engine.parseConfigFile(configFile);
294
+ const { paths } = options;
295
+ // The parse answers `pathsBasePath`, the directory of the configuration that writes
296
+ // `paths` (an extended one included), though the client's option type omits it:
297
+ // https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/tsoptions/tsconfigparsing.go#L1120
298
+ const declaredAt = options.pathsBasePath;
299
+ return {
300
+ base: typeof declaredAt === "string" ? declaredAt : base,
301
+ entries: Object.entries(paths ?? {}).map(([key, targets]) => ({ key, targets })),
302
+ };
249
303
  }
250
304
  catch {
251
- return [];
305
+ return { base, entries: [] };
252
306
  }
253
307
  }
@@ -10,12 +10,22 @@ import { isAsExpression, isParenthesizedExpression, isSatisfiesExpression, isTyp
10
10
  import { ObjectFlags, SymbolFlags, } from "@typescript/native/sync";
11
11
  import { classNames, typesAt } from "./class-members.js";
12
12
  import { must } from "./query.js";
13
- /** The object types whose members the walk reads whatever declares them. */
14
- const WRITTEN = ObjectFlags.ObjectLiteral | ObjectFlags.Mapped;
13
+ /**
14
+ * The object types whose members the walk reads whatever declares them. A reverse-mapped
15
+ * type has no symbol: it is the shape the checker infers from an argument the target
16
+ * writes against a homomorphic mapped type.
17
+ */
18
+ const WRITTEN = ObjectFlags.ObjectLiteral | ObjectFlags.Mapped | ObjectFlags.ReverseMapped;
15
19
  /** The symbols of an anonymous object type the target writes as a type. */
16
20
  const LITERAL_SYMBOLS = SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral;
17
21
  /** The two conversion methods a serializer or a formatter resolves on a value's own shape. */
18
22
  const CONVERSION_NAMES = new Set(["toJSON", "toString"]);
23
+ /**
24
+ * The members that can answer a conversion: a serializer reads the name with `[[Get]]`
25
+ * and calls what it answers, so a getter answers it as a method does:
26
+ * https://tc39.es/ecma262/#sec-serializejsonproperty (step 2).
27
+ */
28
+ export const CONVERSION_MEMBERS = SymbolFlags.Method | SymbolFlags.GetAccessor;
19
29
  /**
20
30
  * The types each value is checked against by the `satisfies` expressions and type assertions
21
31
  * it is written as, through parentheses, and the innermost operand's own type where there is
@@ -92,7 +102,9 @@ export function valueReach(project, held, stored = () => []) {
92
102
  if ((type.objectFlags & ObjectFlags.Anonymous) !== 0) {
93
103
  return (symbol.flags & LITERAL_SYMBOLS) !== 0;
94
104
  }
95
- return ((symbol.flags & SymbolFlags.Interface) !== 0 &&
105
+ // A class the inventory holds is read as a class before this, so an own class
106
+ // here is one it does not hold, such as a class declared in a function.
107
+ return ((symbol.flags & (SymbolFlags.Interface | SymbolFlags.Class)) !== 0 &&
96
108
  symbol.declarations.every((handle) => own.has(handle.path)));
97
109
  };
98
110
  /** The types a value of one object type holds in its data members. */
@@ -115,9 +127,9 @@ export function valueReach(project, held, stored = () => []) {
115
127
  (target.isObjectType() && (target.objectFlags & ObjectFlags.Tuple) !== 0));
116
128
  return !isList && isWritten(type);
117
129
  };
118
- /** The data members and conversion methods of one written object type. */
130
+ /** The data members and conversion members of one written object type. */
119
131
  const carried = (type) => must(queries.propertiesOf(type)).filter((property) => (property.flags & SymbolFlags.Property) !== 0 ||
120
- ((property.flags & SymbolFlags.Method) !== 0 && CONVERSION_NAMES.has(property.name)));
132
+ ((property.flags & CONVERSION_MEMBERS) !== 0 && CONVERSION_NAMES.has(property.name)));
121
133
  /** The types a value of one type holds, read once per type. */
122
134
  const childrenOf = (type) => {
123
135
  const known = children.get(type.id);
@@ -161,6 +161,7 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
161
161
  let partial;
162
162
  let warnings;
163
163
  let unread;
164
+ let unfound;
164
165
  try {
165
166
  const root = runRelative(invoked, targetRoot);
166
167
  const mode = { production: true };
@@ -178,6 +179,7 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
178
179
  const { result, run } = analysis;
179
180
  warnings = run.componentWarnings;
180
181
  unread = run.componentsUnread.map(({ configFile, reason }) => `${relativePath(resolvePath(invoked, targetRoot), configFile) ?? configFile}: its component files are not read: ${reason}`);
182
+ unfound = run.librariesUnfound.map(({ configFile, error }) => `${relativePath(resolvePath(invoked, targetRoot), configFile) ?? configFile}: ${error}: the configuration is analyzed without that library`);
181
183
  partial = partialNotes({
182
184
  notBuilt: run.notBuilt,
183
185
  unanswered: run.unanswered,
@@ -243,7 +245,7 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
243
245
  for (const note of partial) {
244
246
  err.write(`deadset-ts: ${note}\n`);
245
247
  }
246
- for (const line of unread) {
248
+ for (const line of [...unread, ...unfound]) {
247
249
  err.write(`deadset-ts: ${line}\n`);
248
250
  }
249
251
  for (const warning of warnings) {