@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
@@ -7,7 +7,7 @@ import type { Position } from "./position.ts";
7
7
 
8
8
  /** The token walk over a document `JSON.parse` accepted, so every token is well formed. */
9
9
  class Walk {
10
- readonly opened: number[] = [];
10
+ readonly starts: number[] = [];
11
11
  /** The member written twice, where one is, as the offset of its name. */
12
12
  twice: { readonly name: string; readonly offset: number } | undefined;
13
13
  private readonly text: string;
@@ -41,11 +41,11 @@ class Walk {
41
41
 
42
42
  private value(role: "document" | "array" | "record" | "other"): void {
43
43
  this.skip();
44
+ if (role === "record") {
45
+ this.starts.push(this.at);
46
+ }
44
47
  const char = this.text[this.at];
45
48
  if (char === "{") {
46
- if (role === "record") {
47
- this.opened.push(this.at);
48
- }
49
49
  this.object(role);
50
50
  } else if (char === "[") {
51
51
  this.list(role === "array" ? "record" : "other");
@@ -101,8 +101,8 @@ class Walk {
101
101
 
102
102
  /** What a walk of one document found. */
103
103
  interface DocumentWalk {
104
- /** The offset of the brace opening each object of the document's array member, in order. */
105
- readonly opened: readonly number[];
104
+ /** The offset of each element of the document's array member, in order: an object's `{`. */
105
+ readonly starts: readonly number[];
106
106
  /** The first member one object writes twice, at any depth, as the offset of its name. */
107
107
  readonly twice: { readonly name: string; readonly offset: number } | undefined;
108
108
  }
@@ -116,7 +116,7 @@ interface DocumentWalk {
116
116
  export function walkDocument(text: string, array: string): DocumentWalk {
117
117
  const walk = new Walk(text, array);
118
118
  walk.run();
119
- return { opened: walk.opened, twice: walk.twice };
119
+ return { starts: walk.starts, twice: walk.twice };
120
120
  }
121
121
 
122
122
  /**
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Module namespace objects: the binding of a namespace import, the value an `await` of a
3
3
  * dynamic `import()` produces, the first parameter of the first callback passed to `then`
4
- * on one, and a `const` binding initialized with one of these. A use reads the exports it
4
+ * on one, and a variable binding initialized with one of these. A use reads the exports it
5
5
  * names and no other: a property access by name or by a literal index, or a destructured
6
6
  * property, names one export, and any other use may read every export.
7
7
  */
@@ -9,22 +9,26 @@
9
9
  import {
10
10
  isArrowFunction,
11
11
  isAwaitExpression,
12
- isBindingElement,
13
12
  isCallExpression,
13
+ isClassStaticBlockDeclaration,
14
14
  isElementAccessExpression,
15
15
  isExpressionStatement,
16
16
  isFunctionExpression,
17
+ isFunctionLikeDeclaration,
17
18
  isIdentifier,
18
19
  isImportDeclaration,
20
+ isModuleBlock,
19
21
  isNamespaceImport,
20
22
  isNoSubstitutionTemplateLiteral,
21
23
  isObjectBindingPattern,
22
24
  isParenthesizedExpression,
23
25
  isPropertyAccessExpression,
24
26
  isQualifiedName,
27
+ isSourceFile,
25
28
  isStringLiteral,
26
29
  isVariableDeclaration,
27
30
  isVariableDeclarationList,
31
+ isVariableStatement,
28
32
  isVoidExpression,
29
33
  NodeFlags,
30
34
  SyntaxKind,
@@ -32,6 +36,7 @@ import {
32
36
  type Node,
33
37
  type SourceFile,
34
38
  } from "@typescript/native/ast";
39
+ import { spellsNoBinding } from "./spelled-names.ts";
35
40
 
36
41
  /** What one use of a namespace object reads beyond the names the checker resolves. */
37
42
  type NamespaceUse =
@@ -62,23 +67,33 @@ function isLiteralImport(node: Node): boolean {
62
67
  return argument !== undefined && isLiteralText(argument);
63
68
  }
64
69
 
65
- /** The node a `const` binding's name is visible below: the block or file its statement is in. */
66
- function scopeOf(declaration: Node): Node {
67
- const statement = declaration.parent.parent;
68
- return statement.parent;
70
+ /**
71
+ * The node the binding one declaration list declares is visible below: for `let` and
72
+ * `const`, the loop whose head declares it, or else the block or file its statement is
73
+ * in; for `var`, the function, namespace body, static block or file that holds it.
74
+ */
75
+ function scopeOf(list: Node): Node {
76
+ if ((list.flags & (NodeFlags.Let | NodeFlags.Const | NodeFlags.Using)) !== 0) {
77
+ return isVariableStatement(list.parent) ? list.parent.parent : list.parent;
78
+ }
79
+ let at = list.parent;
80
+ while (
81
+ !isSourceFile(at) &&
82
+ !isFunctionLikeDeclaration(at) &&
83
+ !isModuleBlock(at) &&
84
+ !isClassStaticBlockDeclaration(at)
85
+ ) {
86
+ at = at.parent;
87
+ }
88
+ return at;
69
89
  }
70
90
 
71
- /** Every identifier below `scope` that spells `name`, other than `declared` and a member's name. */
91
+ /** Every identifier below `scope` that spells `name` as a reference, other than `declared`. */
72
92
  function occurrences(scope: Node, name: string, declared: Node): readonly Identifier[] {
73
93
  const found: Identifier[] = [];
74
94
  const visit = (node: Node): void => {
75
95
  if (isIdentifier(node)) {
76
- const parent = node.parent;
77
- const member =
78
- (isPropertyAccessExpression(parent) && parent.name === node) ||
79
- (isQualifiedName(parent) && parent.right === node) ||
80
- (isBindingElement(parent) && parent.propertyName === node);
81
- if (node !== declared && node.text === name && !member) {
96
+ if (node !== declared && node.text === name && !spellsNoBinding(node)) {
82
97
  found.push(node);
83
98
  }
84
99
  return;
@@ -92,8 +107,8 @@ function occurrences(scope: Node, name: string, declared: Node): readonly Identi
92
107
  /**
93
108
  * Every use of the namespace objects one file holds, keyed by the node the use is written
94
109
  * at, that reads more than a name the checker resolves: an indexed read by a literal, and
95
- * any use that may read every export. A shadowing declaration of a binding's name inside
96
- * its scope is read as the binding, which can only keep an export live.
110
+ * any use that may read every export. A use of a shadowing declaration of a binding's
111
+ * name inside its scope is read as a use of the binding, which can only keep an export live.
97
112
  */
98
113
  export function namespaceUses(file: SourceFile): ReadonlyMap<Node, NamespaceUse> {
99
114
  const uses = new Map<Node, NamespaceUse>();
@@ -142,13 +157,11 @@ export function namespaceUses(file: SourceFile): ReadonlyMap<Node, NamespaceUse>
142
157
  }
143
158
  return;
144
159
  }
160
+ // A binding that is not `const` is followed too: each assignment to it is an
161
+ // occurrence `value` cannot narrow, so a reassigned binding still reads every export.
145
162
  const list = parent.parent;
146
- if (
147
- isIdentifier(pattern) &&
148
- isVariableDeclarationList(list) &&
149
- (list.flags & NodeFlags.Const) !== 0
150
- ) {
151
- binding(pattern, scopeOf(parent), specifier, seen);
163
+ if (isIdentifier(pattern) && isVariableDeclarationList(list)) {
164
+ binding(pattern, scopeOf(list), specifier, seen);
152
165
  return;
153
166
  }
154
167
  }
package/src/overrides.ts CHANGED
@@ -26,6 +26,11 @@ import { nameComponent } from "./ref.ts";
26
26
  import type { Reference } from "./references.ts";
27
27
  import type { ProjectView } from "./session.ts";
28
28
 
29
+ const CLASS_DECLARATIONS: ReadonlySet<SyntaxKind> = new Set([
30
+ SyntaxKind.ClassDeclaration,
31
+ SyntaxKind.ClassExpression,
32
+ ]);
33
+
29
34
  /** The member kinds a subclass overrides. */
30
35
  const OVERRIDABLE: ReadonlySet<string> = new Set(["method", "class-member"]);
31
36
 
@@ -111,8 +116,11 @@ export function overrideReferences<Brand>(
111
116
  inTarget.length === 0 &&
112
117
  declared !== UNANSWERED &&
113
118
  (declared.flags & SymbolFlags.Class) !== 0 &&
114
- declared.declarations.length > 0 &&
115
- declared.declarations.every((handle) => !own.has(handle.path))
119
+ // The target's augmentation of an outside class merges an interface into it,
120
+ // so only a class declaration says where the class is.
121
+ declared.declarations.some(
122
+ (handle) => CLASS_DECLARATIONS.has(handle.kind) && !own.has(handle.path),
123
+ )
116
124
  ) {
117
125
  outsideBases.set(id, declared);
118
126
  }
package/src/paths.ts CHANGED
@@ -67,6 +67,16 @@ export function dirnamePath(path: string): string {
67
67
  return cut === 0 ? SEPARATOR : normalized.slice(0, cut);
68
68
  }
69
69
 
70
+ /**
71
+ * The extension of one path's last segment, from its last full stop, or the empty string
72
+ * where it has none. A segment that only starts with a full stop has none.
73
+ */
74
+ export function extensionOf(path: string): string {
75
+ const name = path.slice(path.lastIndexOf(SEPARATOR) + 1);
76
+ const dot = name.lastIndexOf(".");
77
+ return dot <= 0 ? "" : name.slice(dot);
78
+ }
79
+
70
80
  /** One path resolved against a directory, absolute where that directory is. */
71
81
  export function resolvePath(against: string, path: string): string {
72
82
  return isAbsolutePath(path) ? normalizePath(path) : joinPath(against, path);
package/src/references.ts CHANGED
@@ -196,10 +196,10 @@ export interface ReferenceCost {
196
196
  /** Per-type lookups for the contextual uses: each distinct type's constituents, properties and symbol. */
197
197
  readonly contextualLookups: number;
198
198
  /**
199
- * The requests the passes that read members through the checker's types make: element
200
- * accesses, key comparisons, instantiated signatures and values handed to unimplemented
201
- * callees. They are
202
- * counted where the caller measures the client, through `requestCount`, and are 0 otherwise.
199
+ * The requests the passes after the walk make: the bases overrides are read against,
200
+ * element accesses, key comparisons, instantiated signatures, values handed to
201
+ * unimplemented callees and the modules a `declare module` names. They are counted
202
+ * where the caller measures the client, through `requestCount`, and are 0 otherwise.
203
203
  */
204
204
  readonly typedLookups: number;
205
205
  }
@@ -1060,19 +1060,28 @@ export function references<Brand>(
1060
1060
  const fileIds = new Set(
1061
1061
  held.symbols.filter((symbol) => symbol.kind === "file").map((symbol) => symbol.id),
1062
1062
  );
1063
- let topLevel: ReadonlyMap<string, readonly InventorySymbol[]> | undefined;
1064
- /** The declarations one file's top level holds. */
1065
- const declaredIn = (fileId: string): readonly InventorySymbol[] => {
1066
- if (topLevel === undefined) {
1067
- const byParent = new Map<string, InventorySymbol[]>();
1063
+ let moduleBodies: ReadonlyMap<string, readonly InventorySymbol[]> | undefined;
1064
+ /**
1065
+ * The declarations one module holds directly: a file's top level, or the body of a
1066
+ * `declare module` whose name is a string. Any other declaration holds none here.
1067
+ */
1068
+ const declaredIn = (moduleId: string): readonly InventorySymbol[] => {
1069
+ if (moduleBodies === undefined) {
1070
+ const byModule = new Map<string, InventorySymbol[]>();
1068
1071
  for (const symbol of held.symbols) {
1069
- if (fileIds.has(symbol.parent)) {
1070
- byParent.set(symbol.parent, [...(byParent.get(symbol.parent) ?? []), symbol]);
1072
+ if (!fileIds.has(symbol.parent) && !held.modules.has(symbol.parent)) {
1073
+ continue;
1074
+ }
1075
+ const siblings = byModule.get(symbol.parent);
1076
+ if (siblings === undefined) {
1077
+ byModule.set(symbol.parent, [symbol]);
1078
+ } else {
1079
+ siblings.push(symbol);
1071
1080
  }
1072
1081
  }
1073
- topLevel = byParent;
1082
+ moduleBodies = byModule;
1074
1083
  }
1075
- return topLevel.get(fileId) ?? [];
1084
+ return moduleBodies.get(moduleId) ?? [];
1076
1085
  };
1077
1086
 
1078
1087
  const exportsCache = new Map<number, readonly ChainTarget[]>();
@@ -1431,9 +1440,12 @@ export function references<Brand>(
1431
1440
 
1432
1441
  for (const url of urls) {
1433
1442
  const constructor = url.constructor === undefined ? undefined : answered.get(url.constructor);
1443
+ // An augmentation of the global `URL` merges into it, which keeps the
1444
+ // library's declaration; a `URL` the target declares has no other.
1434
1445
  if (
1435
1446
  typeof constructor === "object" &&
1436
- constructor.declarations.some((handle) => ownPaths.has(handle.path))
1447
+ constructor.declarations.length > 0 &&
1448
+ constructor.declarations.every((handle) => ownPaths.has(handle.path))
1437
1449
  ) {
1438
1450
  continue;
1439
1451
  }
@@ -1604,14 +1616,14 @@ export function references<Brand>(
1604
1616
 
1605
1617
  let typedLookups = 0;
1606
1618
  if (consumer === undefined) {
1607
- found.push(...overrideReferences(project, held, chains, new Set(tests)));
1608
1619
  const before = options.requestCount?.() ?? 0;
1620
+ found.push(...overrideReferences(project, held, chains, new Set(tests)));
1609
1621
  found.push(...indexedAccessReferences(project, held, chains, root, new Set(tests)));
1610
1622
  found.push(...elementAccessReferences(project, held, chains, root, new Set(tests)));
1611
1623
  found.push(...keyofComparisonReferences(project, held, chains, root, new Set(tests)));
1612
1624
  found.push(...unimplementedCalleeReads(project, held, chains, root, new Set(tests)));
1613
- typedLookups = (options.requestCount?.() ?? 0) - before;
1614
1625
  found.push(...moduleDeclarationReferences(project, held, root, new Set(tests)));
1626
+ typedLookups = (options.requestCount?.() ?? 0) - before;
1615
1627
  }
1616
1628
  const scripts = new Set(
1617
1629
  walked.filter((one) => one.file.externalModuleIndicator === undefined).map((one) => one.fileId),
@@ -120,8 +120,9 @@ export function reflectiveLookup<Brand>(input: DetectorInput<Brand>): readonly E
120
120
  return [];
121
121
  }
122
122
 
123
- // Whether each written `Reflect` is the global one, asked in one batch: a
124
- // declaration of that name in one of the project's own files shadows it.
123
+ // Whether each written `Reflect` is the global one, asked in one batch: a name
124
+ // declared only in the project's own files shadows it, and an augmentation of the
125
+ // global merges into it, which keeps the library's declaration.
125
126
  const ownFiles = project.ownPaths();
126
127
  const reflects = lookups.flatMap((lookup) =>
127
128
  lookup.reflect === undefined ? [] : [lookup.reflect],
@@ -131,11 +132,12 @@ export function reflectiveLookup<Brand>(input: DetectorInput<Brand>): readonly E
131
132
  project.symbolsAt(reflects.map((node) => project.handle(node))).forEach((symbol, index) => {
132
133
  const node = reflects[index];
133
134
  // A name the checker did not answer for is read as the global, which retains.
134
- const declaredHere =
135
- symbol === UNANSWERED
136
- ? false
137
- : symbol?.declarations.some((declaration) => ownFiles.has(declaration.path));
138
- if (node !== undefined && declaredHere === false) {
135
+ const isGlobal =
136
+ symbol === UNANSWERED ||
137
+ (symbol !== undefined &&
138
+ (symbol.declarations.length === 0 ||
139
+ symbol.declarations.some((declaration) => !ownFiles.has(declaration.path))));
140
+ if (node !== undefined && isGlobal) {
139
141
  global.add(node);
140
142
  }
141
143
  });
@@ -912,7 +912,7 @@ class Execution {
912
912
  case "lit":
913
913
  return arg.value;
914
914
  case "pipe":
915
- return this.pipeline(arg.pipe, dot, false);
915
+ return this.pipeline(arg.pipe, dot);
916
916
  case "field":
917
917
  return this.field(arg.recv === undefined ? dot : this.arg(arg.recv, dot), arg.names);
918
918
  case "func":
@@ -958,18 +958,17 @@ class Execution {
958
958
  return call(head.name, operands());
959
959
  }
960
960
 
961
- private pipeline(pipe: Pipeline, dot: Value, declares: boolean): Value {
961
+ private pipeline(pipe: Pipeline, dot: Value): Value {
962
962
  let value: Value | undefined;
963
963
  for (const command of pipe.commands) {
964
964
  value = this.command(command, dot, value);
965
965
  }
966
966
  const result = value ?? null;
967
- const [only] = pipe.vars;
968
- if (only !== undefined && (declares || pipe.assign)) {
967
+ for (const name of pipe.vars) {
969
968
  if (pipe.assign) {
970
- this.assign(only, result);
969
+ this.assign(name, result);
971
970
  } else {
972
- this.scopes.at(-1)?.set(only, result);
971
+ this.scopes.at(-1)?.set(name, result);
973
972
  }
974
973
  }
975
974
  return result;
@@ -982,7 +981,7 @@ class Execution {
982
981
  this.write(node.text);
983
982
  break;
984
983
  case "action": {
985
- const value = this.pipeline(node.pipe, dot, true);
984
+ const value = this.pipeline(node.pipe, dot);
986
985
  if (node.pipe.vars.length === 0) {
987
986
  this.write(formatted(value, false));
988
987
  }
@@ -995,7 +994,7 @@ class Execution {
995
994
  case "with": {
996
995
  this.scopes.push(new Map());
997
996
  try {
998
- const value = this.pipeline(node.pipe, dot, true);
997
+ const value = this.pipeline(node.pipe, dot);
999
998
  const taken = truthy(value);
1000
999
  this.run(taken ? node.list : node.otherwise, taken && node.k === "with" ? value : dot);
1001
1000
  } finally {
@@ -1013,7 +1012,9 @@ class Execution {
1013
1012
  private range(node: Control, dot: Value): void {
1014
1013
  this.scopes.push(new Map());
1015
1014
  try {
1016
- const value = this.pipeline({ ...node.pipe, vars: [] }, dot, false);
1015
+ // Go's `evalPipeline` gives every variable the range declares or assigns the whole value first:
1016
+ // https://github.com/golang/go/blob/go1.27.2/src/text/template/exec.go#L525
1017
+ const value = this.pipeline(node.pipe, dot);
1017
1018
  let entries: [Value, Value][];
1018
1019
  if (value === null) {
1019
1020
  entries = [];
@@ -1031,13 +1032,20 @@ class Execution {
1031
1032
  return;
1032
1033
  }
1033
1034
  const [first, second] = node.pipe.vars;
1035
+ const scope = this.scopes.at(-1);
1036
+ const bind = (name: string, held: Value): void => {
1037
+ if (node.pipe.assign) {
1038
+ this.assign(name, held);
1039
+ } else {
1040
+ scope?.set(name, held);
1041
+ }
1042
+ };
1034
1043
  for (const [key, element] of entries) {
1035
- const scope = this.scopes.at(-1);
1036
1044
  if (second !== undefined && first !== undefined) {
1037
- scope?.set(first, key);
1038
- scope?.set(second, element);
1045
+ bind(first, key);
1046
+ bind(second, element);
1039
1047
  } else if (first !== undefined) {
1040
- scope?.set(first, element);
1048
+ bind(first, element);
1041
1049
  }
1042
1050
  try {
1043
1051
  this.run(node.list, element);
@@ -45,6 +45,8 @@ import {
45
45
  SignatureKind,
46
46
  SymbolFlags,
47
47
  TypeFlags,
48
+ type NodeHandle,
49
+ type Signature,
48
50
  type Symbol as TSSymbol,
49
51
  type Type,
50
52
  } from "@typescript/native/sync";
@@ -64,7 +66,7 @@ import { renderPosition } from "./position.ts";
64
66
  import { must } from "./query.ts";
65
67
  import type { ProjectView } from "./session.ts";
66
68
  import { erasedStores, type ErasedStores } from "./erased-stores.ts";
67
- import { checkedTypes, valueReach, type Reached } from "./value-reach.ts";
69
+ import { checkedTypes, CONVERSION_MEMBERS, valueReach, type Reached } from "./value-reach.ts";
68
70
 
69
71
  /** The serializer every run reads, whatever the configuration names. */
70
72
  const JSON_STRINGIFY: DeclarationEntry = { shape: "global", global: "JSON.stringify" };
@@ -484,14 +486,19 @@ function destinations<Brand>(
484
486
  own.has(path) || input.consumers.some((root) => path === root || path.startsWith(`${root}/`));
485
487
  const implemented = implementedOwners(project, inAnalysis);
486
488
  /**
487
- * Whether every declaration of a callee is outside the analysis or ambient. An ambient
488
- * declaration has no body, so the code that runs is outside the analysis, unless it is a
489
- * member of an interface or an object type a class or a typed object literal of the
490
- * analysis implements; `settled` false skips that question, which asks the checker.
489
+ * Whether every one of `declarations`, all of the callee's by default, is outside the
490
+ * analysis or ambient. An ambient declaration has no body, so the code that runs is
491
+ * outside the analysis, unless it is a member of an interface or an object type a class
492
+ * or a typed object literal of the analysis implements; `settled` false skips that
493
+ * question, which asks the checker.
491
494
  */
492
- const outside = (target: TSSymbol, settled: boolean): boolean =>
493
- target.declarations.length > 0 &&
494
- target.declarations.every((handle) => {
495
+ const outside = (
496
+ target: TSSymbol,
497
+ settled: boolean,
498
+ declarations: readonly NodeHandle[] = target.declarations,
499
+ ): boolean =>
500
+ declarations.length > 0 &&
501
+ declarations.every((handle) => {
495
502
  if (!inAnalysis(handle.path)) {
496
503
  return true;
497
504
  }
@@ -505,6 +512,13 @@ function destinations<Brand>(
505
512
  const owner = must(queries.parentOf(target));
506
513
  return owner === undefined || !implemented().has(owner.id);
507
514
  });
515
+ /**
516
+ * Whether some one declaration of a callee is outside the analysis or ambient, without
517
+ * asking the checker. A project's declaration merged into a library's symbol leaves the
518
+ * library's beside it, so a call can still resolve to the library's.
519
+ */
520
+ const mayBeOutside = (target: TSSymbol): boolean =>
521
+ target.declarations.some((handle) => outside(target, false, [handle]));
508
522
  const invocation = (call: CallExpression): Invocation => {
509
523
  const form = forms.get(call);
510
524
  const method = form === undefined ? undefined : callees.get(form.method);
@@ -513,7 +527,7 @@ function destinations<Brand>(
513
527
  if (
514
528
  form !== undefined &&
515
529
  method !== undefined &&
516
- outside(method, false) &&
530
+ mayBeOutside(method) &&
517
531
  FUNCTION_INTERFACES.has(must(project.queries.parentOf(method))?.name ?? "")
518
532
  ) {
519
533
  return form.invocation;
@@ -524,6 +538,32 @@ function destinations<Brand>(
524
538
  const name = calleeName(invocation(call).callee);
525
539
  return name === undefined ? undefined : callees.get(name);
526
540
  };
541
+ const signatures = new Map<CallExpression, Signature | undefined>();
542
+ const resolvedOf = (call: CallExpression): Signature | undefined => {
543
+ if (!signatures.has(call)) {
544
+ signatures.set(call, must(queries.resolvedSignature(call)));
545
+ }
546
+ return signatures.get(call);
547
+ };
548
+ /**
549
+ * The declarations of a callee that the declaration of a direct call's resolved
550
+ * signature lies in, or all of them where it lies in none: a declaration merged into
551
+ * the callee's symbol that the call does not resolve to does not decide where it runs.
552
+ */
553
+ const calledDeclarations = (call: CallExpression, target: TSSymbol): readonly NodeHandle[] => {
554
+ const declaration = invocation(call).indirect ? undefined : resolvedOf(call)?.declaration;
555
+ const signature =
556
+ declaration === undefined ? undefined : project.declarationAt(declaration)?.node;
557
+ if (declaration === undefined || signature === undefined) {
558
+ return target.declarations;
559
+ }
560
+ const containing = target.declarations.filter((handle) => {
561
+ const node =
562
+ handle.path === declaration.path ? project.declarationAt(handle)?.node : undefined;
563
+ return node !== undefined && node.pos <= signature.pos && signature.end <= node.end;
564
+ });
565
+ return containing.length === 0 ? target.declarations : containing;
566
+ };
527
567
  const erasedArguments = new Map<CallExpression, ReadonlySet<number>>();
528
568
  const erasedAt = (call: CallExpression, argument: number): boolean => {
529
569
  let indexes = erasedArguments.get(call);
@@ -536,7 +576,7 @@ function destinations<Brand>(
536
576
  ? type === undefined
537
577
  ? []
538
578
  : must(queries.signaturesOf(type, SignatureKind.Call))
539
- : [must(queries.resolvedSignature(call))].filter((one) => one !== undefined);
579
+ : [resolvedOf(call)].filter((one) => one !== undefined);
540
580
  indexes = new Set(
541
581
  invoked.arguments.flatMap((_unused, index) =>
542
582
  signatures.some((signature) => {
@@ -563,7 +603,7 @@ function destinations<Brand>(
563
603
  if (entry !== undefined) {
564
604
  return { reach: "properties", callee: spelledEntry(entry.entry, held) };
565
605
  }
566
- if (outside(target, true)) {
606
+ if (outside(target, true, calledDeclarations(call, target))) {
567
607
  return erasedAt(call, argument)
568
608
  ? {
569
609
  reach: "conversions",
@@ -602,7 +642,7 @@ function destinations<Brand>(
602
642
  if (target === undefined) {
603
643
  return false;
604
644
  }
605
- if (named.some((one) => names(one, target, chains)) || outside(target, false)) {
645
+ if (named.some((one) => names(one, target, chains)) || mayBeOutside(target)) {
606
646
  return true;
607
647
  }
608
648
  return chains
@@ -733,7 +773,7 @@ export function serializationContract<Brand>(input: DetectorInput<Brand>): reado
733
773
  // the value left the analysis.
734
774
  const holdsBack = (flags: SymbolFlags, name: string, to: Destination): boolean =>
735
775
  (flags & SymbolFlags.Property) !== 0 ||
736
- ((flags & SymbolFlags.Method) !== 0 &&
776
+ ((flags & CONVERSION_MEMBERS) !== 0 &&
737
777
  (name === SERIALIZED_CONVERSION || (to.reach === "conversions" && CONVERSIONS.has(name))));
738
778
  const found: Evidence[] = [];
739
779
  for (const flow of flows) {
package/src/session.ts CHANGED
@@ -419,6 +419,8 @@ export interface ProjectDiagnostics {
419
419
  readonly syntactic: readonly Diagnostic[];
420
420
  readonly semantic: readonly Diagnostic[];
421
421
  readonly configParsing: readonly Diagnostic[];
422
+ /** The program's diagnostics placed in no file, each once, its `fileName` undefined. */
423
+ readonly unplaced: readonly Diagnostic[];
422
424
  /** The line and column of one offset into one of the program's files, counted from one. */
423
425
  readonly locate: (
424
426
  fileName: string,
@@ -426,12 +428,31 @@ export interface ProjectDiagnostics {
426
428
  ) => { line: number; column: number } | undefined;
427
429
  }
428
430
 
429
- /** The three diagnostic sets one project carries, read before any analysis. */
431
+ /**
432
+ * The program's diagnostics placed in no file. The per-file sets never carry one; the
433
+ * client answers its `fileName` as `null`, and the global set repeats the program set:
434
+ * https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/api/session.go#L3726-L3752
435
+ */
436
+ function unplacedOf(program: Program): readonly Diagnostic[] {
437
+ const seen = new Set<string>();
438
+ return [...program.getGlobalDiagnostics(), ...program.getProgramDiagnostics()]
439
+ .filter((diagnostic) => diagnostic.fileName == null)
440
+ .filter((diagnostic) => {
441
+ const key = `${String(diagnostic.code)}\u0000${diagnostic.text}`;
442
+ const fresh = !seen.has(key);
443
+ seen.add(key);
444
+ return fresh;
445
+ })
446
+ .map((diagnostic) => ({ ...diagnostic, fileName: undefined }));
447
+ }
448
+
449
+ /** The diagnostic sets one project carries, read before any analysis. */
430
450
  export function diagnosticsOf<Brand>(project: ProjectView<Brand>): ProjectDiagnostics {
431
451
  return {
432
452
  syntactic: project.program.getSyntacticDiagnostics(),
433
453
  semantic: project.program.getSemanticDiagnostics(),
434
454
  configParsing: project.program.getConfigFileParsingDiagnostics(),
455
+ unplaced: unplacedOf(project.program),
435
456
  locate: (fileName, offset) => {
436
457
  const file = project.program.getSourceFile(fileName);
437
458
  if (file === undefined || offset < 0 || offset > file.text.length) {