@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
@@ -0,0 +1,102 @@
1
+ import {
2
+ isBindingElement,
3
+ isBreakStatement,
4
+ isClassDeclaration,
5
+ isClassExpression,
6
+ isContinueStatement,
7
+ isEnumDeclaration,
8
+ isEnumMember,
9
+ isExportDeclaration,
10
+ isExportSpecifier,
11
+ isFunctionDeclaration,
12
+ isFunctionExpression,
13
+ isGetAccessorDeclaration,
14
+ isImportClause,
15
+ isImportEqualsDeclaration,
16
+ isImportSpecifier,
17
+ isInterfaceDeclaration,
18
+ isJsxAttribute,
19
+ isLabeledStatement,
20
+ isMetaProperty,
21
+ isMethodDeclaration,
22
+ isMethodSignatureDeclaration,
23
+ isModuleDeclaration,
24
+ isNamespaceExport,
25
+ isNamespaceExportDeclaration,
26
+ isNamespaceImport,
27
+ isParameterDeclaration,
28
+ isPropertyAccessExpression,
29
+ isPropertyAssignment,
30
+ isPropertyDeclaration,
31
+ isPropertySignatureDeclaration,
32
+ isQualifiedName,
33
+ isSetAccessorDeclaration,
34
+ isTypeAliasDeclaration,
35
+ isTypeParameterDeclaration,
36
+ isVariableDeclaration,
37
+ type Identifier,
38
+ } from "@typescript/native/ast";
39
+
40
+ /**
41
+ * Whether one identifier spells a name without referring to a binding of it: a member's
42
+ * name after a dot, a property or member an object literal, a class, a type or an enum
43
+ * declares, the name a declaration binds, the property a binding element destructures,
44
+ * an import or export specifier's name in another module or under a rename, a JSX
45
+ * attribute's name, a statement label, or the name of a meta-property. A shorthand
46
+ * property and an unrenamed local export are no such spelling: each reads the binding.
47
+ */
48
+ export function spellsNoBinding(node: Identifier): boolean {
49
+ const parent = node.parent;
50
+ if (isPropertyAccessExpression(parent) || isMetaProperty(parent)) {
51
+ return parent.name === node;
52
+ }
53
+ if (isQualifiedName(parent)) {
54
+ return parent.right === node;
55
+ }
56
+ if (isBindingElement(parent)) {
57
+ return parent.propertyName === node || parent.name === node;
58
+ }
59
+ if (isImportSpecifier(parent)) {
60
+ return true;
61
+ }
62
+ if (isExportSpecifier(parent)) {
63
+ return (
64
+ (isExportDeclaration(parent.parent.parent) &&
65
+ parent.parent.parent.moduleSpecifier !== undefined) ||
66
+ (parent.propertyName !== undefined && parent.name === node)
67
+ );
68
+ }
69
+ if (
70
+ isPropertyAssignment(parent) ||
71
+ isPropertyDeclaration(parent) ||
72
+ isMethodDeclaration(parent) ||
73
+ isPropertySignatureDeclaration(parent) ||
74
+ isMethodSignatureDeclaration(parent) ||
75
+ isGetAccessorDeclaration(parent) ||
76
+ isSetAccessorDeclaration(parent) ||
77
+ isEnumMember(parent) ||
78
+ isJsxAttribute(parent) ||
79
+ isVariableDeclaration(parent) ||
80
+ isParameterDeclaration(parent) ||
81
+ isFunctionDeclaration(parent) ||
82
+ isFunctionExpression(parent) ||
83
+ isClassDeclaration(parent) ||
84
+ isClassExpression(parent) ||
85
+ isInterfaceDeclaration(parent) ||
86
+ isTypeAliasDeclaration(parent) ||
87
+ isEnumDeclaration(parent) ||
88
+ isModuleDeclaration(parent) ||
89
+ isTypeParameterDeclaration(parent) ||
90
+ isImportClause(parent) ||
91
+ isNamespaceImport(parent) ||
92
+ isImportEqualsDeclaration(parent) ||
93
+ isNamespaceExport(parent) ||
94
+ isNamespaceExportDeclaration(parent)
95
+ ) {
96
+ return parent.name === node;
97
+ }
98
+ return (
99
+ (isLabeledStatement(parent) || isBreakStatement(parent) || isContinueStatement(parent)) &&
100
+ parent.label === node
101
+ );
102
+ }
@@ -98,7 +98,7 @@ type ByReference = ReadonlyMap<string, readonly InventorySymbol[]>;
98
98
  */
99
99
  function readRecord(value: unknown, site: Position, byReference: ByReference, shape: Shape): Read {
100
100
  const refuse = (text: string, want: string): never => {
101
- throw new SuppressionError(shape.mechanism, `${shape.file} ${siteText(site)}`, text, want);
101
+ throw new SuppressionError(shape.mechanism, siteText(site), text, want);
102
102
  };
103
103
  if (!isObject(value)) {
104
104
  return refuse(JSON.stringify(value), shape.recordWant);
@@ -217,7 +217,7 @@ function readDocument(
217
217
  const records: SuppressionRecord[] = [];
218
218
  const refusals: Refusal[] = [];
219
219
  (held as readonly unknown[]).forEach((value, index) => {
220
- const offset = walk.opened[index];
220
+ const offset = walk.starts[index];
221
221
  const site = offset === undefined ? documentSite(shape) : positionAt(text, shape.file, offset);
222
222
  const read = readRecord(value, site, byReference, shape);
223
223
  records.push(...read.records);
@@ -134,7 +134,9 @@ function lineComments(file: SourceFile): readonly LineComment[] {
134
134
  const spans: { readonly start: number; readonly end: number }[] = [];
135
135
  const visit = (node: Node): void => {
136
136
  if (LITERALS.has(node.kind)) {
137
- spans.push({ start: tokenStart(text, node.pos), end: node.end });
137
+ // JSX text carries no trivia: its first code unit is at `pos`, even a `//`.
138
+ const start = node.kind === SyntaxKind.JsxText ? node.pos : tokenStart(text, node.pos);
139
+ spans.push({ start, end: node.end });
138
140
  }
139
141
  node.forEachChild(visit);
140
142
  };
@@ -18,7 +18,7 @@ import type { DetectorInput, Evidence } from "./exempt.ts";
18
18
  import type { Host } from "./host.ts";
19
19
  import type { SymbolKind } from "./inventory.ts";
20
20
  import { memberNames } from "./member-names.ts";
21
- import { isAbsolutePath, joinPath, normalizePath, relativePath } from "./paths.ts";
21
+ import { extensionOf, isAbsolutePath, joinPath, normalizePath, relativePath } from "./paths.ts";
22
22
  import { isComponentFile, type Position } from "./position.ts";
23
23
 
24
24
  /** One template, by its path below the target root. */
@@ -125,7 +125,10 @@ function stringEnd(text: string, from: number): number {
125
125
  return at + 1;
126
126
  }
127
127
 
128
- /** The end of the `${` substitution whose expression starts at `from`, past its closing brace. */
128
+ /**
129
+ * The end of the `${` substitution whose expression starts at `from`: past its closing brace,
130
+ * or one past the text's end where none closes it, so the expression is the text to its end.
131
+ */
129
132
  function substitutionEnd(text: string, from: number): number {
130
133
  let depth = 1;
131
134
  let at = from;
@@ -149,7 +152,7 @@ function substitutionEnd(text: string, from: number): number {
149
152
  }
150
153
  at += 1;
151
154
  }
152
- return at;
155
+ return text.length + 1;
153
156
  }
154
157
 
155
158
  /** The end of the template literal at `from`; each `${…}` expression goes to `substitution`. */
@@ -243,8 +246,8 @@ function namesOf(text: string, delimiters: TemplateDelimiters): readonly Named[]
243
246
  }
244
247
 
245
248
  /** Every identifier the markup of one component file writes in an action. */
246
- function markupNamesOf(text: string): readonly Named[] {
247
- return readComponent(text).markup.flatMap((one) =>
249
+ function markupNamesOf(file: TemplateFile): readonly Named[] {
250
+ return readComponent(file.text, extensionOf(file.path)).markup.flatMap((one) =>
248
251
  identifiersOf(one.text, true).map(({ name, offset }) => ({
249
252
  name,
250
253
  offset: one.start + offset,
@@ -287,7 +290,7 @@ export function templateField<Brand>(input: DetectorInput<Brand>): readonly Evid
287
290
  names: namesOf(file.text, templates.delimiters),
288
291
  markup: false,
289
292
  })),
290
- ...components.map((file) => ({ file, names: markupNamesOf(file.text), markup: true })),
293
+ ...components.map((file) => ({ file, names: markupNamesOf(file), markup: true })),
291
294
  ];
292
295
  for (const { file, names, markup } of scanned) {
293
296
  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
 
9
9
  import {
@@ -69,8 +69,6 @@ interface ProjectErrorReading {
69
69
  readonly missing: readonly SetupFailure[];
70
70
  /** The type errors, each with the unit it skips. */
71
71
  readonly skips: readonly Skip[];
72
- /** The errors placed in no file, which leave the project unanalyzable. */
73
- readonly failing: readonly LocatedDiagnostic[];
74
72
  }
75
73
 
76
74
  /** Where one project reads what its imports may name. */
@@ -78,11 +76,17 @@ interface ErrorContext {
78
76
  readonly host: Host;
79
77
  /** The target root, absolute. */
80
78
  readonly targetRoot: string;
81
- /** The keys of the project configuration's `paths` mapping. */
82
- readonly paths: readonly string[];
79
+ /** The project configuration's `paths` mapping. */
80
+ readonly paths: PathsMapping;
83
81
  }
84
82
 
85
- /** The string literal holding one offset, innermost first. */
83
+ /** One configuration's `paths` mapping, its targets read against `base`. */
84
+ interface PathsMapping {
85
+ readonly base: string;
86
+ readonly entries: readonly { readonly key: string; readonly targets: readonly string[] }[];
87
+ }
88
+
89
+ /** The string literal holding one offset, innermost first, a doc comment's included. */
86
90
  function literalAt(file: SourceFile, offset: number): string | undefined {
87
91
  let found: string | undefined;
88
92
  const visit = (node: Node): void => {
@@ -92,36 +96,59 @@ function literalAt(file: SourceFile, offset: number): string | undefined {
92
96
  if (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node)) {
93
97
  found = node.text;
94
98
  }
99
+ // `forEachChild` does not enter a doc comment, where a JavaScript file imports types.
100
+ for (const doc of (node as { readonly jsDoc?: readonly Node[] }).jsDoc ?? []) {
101
+ visit(doc);
102
+ }
95
103
  node.forEachChild(visit);
96
104
  };
97
105
  file.forEachChild(visit);
98
106
  return found;
99
107
  }
100
108
 
101
- /** Whether one `paths` key matches one specifier: exactly, or around its one `*`. */
102
- function mappedBy(key: string, specifier: string): boolean {
109
+ /**
110
+ * What one `paths` key's `*` matches in one specifier, `""` for a key with no `*` that
111
+ * is the specifier, or `undefined` where the key does not match it.
112
+ */
113
+ function mappedBy(key: string, specifier: string): string | undefined {
103
114
  const star = key.indexOf("*");
104
115
  if (star < 0) {
105
- return key === specifier;
116
+ return key === specifier ? "" : undefined;
106
117
  }
107
118
  const prefix = key.slice(0, star);
108
119
  const suffix = key.slice(star + 1);
109
- return (
110
- specifier.length >= prefix.length + suffix.length &&
120
+ return specifier.length >= prefix.length + suffix.length &&
111
121
  specifier.startsWith(prefix) &&
112
122
  specifier.endsWith(suffix)
113
- );
123
+ ? specifier.slice(prefix.length, specifier.length - suffix.length)
124
+ : undefined;
114
125
  }
115
126
 
116
- /** Whether a relative import's path names something on disk, as written or with any extension. */
117
- function provided(host: Host, path: string): boolean {
118
- if (host.kindOf(path) !== "absent") {
127
+ /**
128
+ * One `paths` target, its first `*` replaced by `star` taken literally, as the compiler does:
129
+ * https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/module/resolver.go#L1282
130
+ */
131
+ function substituted(target: string, star: string): string {
132
+ const at = target.indexOf("*");
133
+ return at < 0 ? target : target.slice(0, at) + star + target.slice(at + 1);
134
+ }
135
+
136
+ /**
137
+ * Whether a path names something on disk, as written or with any extension; under
138
+ * `fileOnly`, only a file counts.
139
+ */
140
+ function provided(host: Host, path: string, fileOnly: boolean): boolean {
141
+ const counts = (at: string): boolean =>
142
+ fileOnly ? host.kindOf(at) === "file" : host.kindOf(at) !== "absent";
143
+ if (counts(path)) {
119
144
  return true;
120
145
  }
121
146
  const dir = dirnamePath(path);
122
147
  const stem = `${path.slice(dir.length + 1)}.`;
123
148
  try {
124
- return host.readDirectory(dir).some((entry) => entry.name.startsWith(stem));
149
+ return host
150
+ .readDirectory(dir)
151
+ .some((entry) => entry.name.startsWith(stem) && counts(joinPath(dir, entry.name)));
125
152
  } catch {
126
153
  return false;
127
154
  }
@@ -158,6 +185,21 @@ function declaringManifest(
158
185
  }
159
186
  }
160
187
 
188
+ /**
189
+ * Whether a package is installed for an importer in `dir`: in the `node_modules` of
190
+ * `dir` or of any ancestor, above the target root too, as Node and the compiler look.
191
+ */
192
+ function installed(host: Host, dir: string, name: string): boolean {
193
+ for (let at = dir; ; at = dirnamePath(at)) {
194
+ if (host.kindOf(joinPath(at, "node_modules", name)) === "directory") {
195
+ return true;
196
+ }
197
+ if (dirnamePath(at) === at) {
198
+ return false;
199
+ }
200
+ }
201
+ }
202
+
161
203
  /**
162
204
  * Why one import that the compiler cannot resolve names a module the project expects,
163
205
  * with the fix, or undefined where the module is one the project does not expect.
@@ -170,17 +212,31 @@ function expectedModule(
170
212
  const { host, targetRoot } = context;
171
213
  const fix = "run the generator or the build that writes it";
172
214
  if (specifier.startsWith(".") || isAbsolutePath(specifier)) {
173
- return provided(host, resolvePath(dirnamePath(importer), specifier))
215
+ return provided(host, resolvePath(dirnamePath(importer), specifier), false)
174
216
  ? undefined
175
217
  : `which names no file of the target: ${fix}`;
176
218
  }
177
219
  if (specifier.startsWith("#")) {
178
220
  return `which the manifest's imports map to no file: ${fix}`;
179
221
  }
180
- if (context.paths.some((key) => mappedBy(key, specifier))) {
181
- return `which the compiler configuration's paths map to no file: ${fix}`;
182
- }
222
+ const mapped = context.paths.entries.flatMap(({ key, targets }) => {
223
+ const star = mappedBy(key, specifier);
224
+ return star === undefined
225
+ ? []
226
+ : targets.map((target) => resolvePath(context.paths.base, substituted(target, star)));
227
+ });
183
228
  const name = packageOfSpecifier(specifier);
229
+ // The compiler looks up in `node_modules` what `paths` does not resolve, so the error
230
+ // of an installed package's import is the import's own, a subpath its exports omit:
231
+ // https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/module/resolver.go#L548-L575
232
+ if (name !== undefined && installed(host, dirnamePath(importer), name)) {
233
+ return undefined;
234
+ }
235
+ if (mapped.length > 0) {
236
+ return mapped.some((path) => provided(host, path, true))
237
+ ? undefined
238
+ : `which the compiler configuration's paths map to no file: ${fix}`;
239
+ }
184
240
  const manifest =
185
241
  name === undefined
186
242
  ? undefined
@@ -223,14 +279,8 @@ export function readErrors<Brand>(
223
279
  const files = new Map(project.ownSourceFiles().map((file) => [file.fileName, file]));
224
280
  const missing: SetupFailure[] = [];
225
281
  const skips: Skip[] = [];
226
- const failing: LocatedDiagnostic[] = [];
227
- for (const located of errors) {
228
- const { diagnostic, at } = located;
229
- if (diagnostic.fileName === undefined) {
230
- failing.push(located);
231
- continue;
232
- }
233
- const file = files.get(diagnostic.fileName);
282
+ for (const { diagnostic, at } of errors) {
283
+ const file = diagnostic.fileName === undefined ? undefined : files.get(diagnostic.fileName);
234
284
  if (file === undefined || diagnostic.reportsUnnecessary === true) {
235
285
  continue;
236
286
  }
@@ -257,7 +307,7 @@ export function readErrors<Brand>(
257
307
  });
258
308
  }
259
309
  }
260
- return { missing, skips, failing };
310
+ return { missing, skips };
261
311
  }
262
312
 
263
313
  /** The lines each skip withholds, below the target root. */
@@ -330,12 +380,21 @@ export function skippedRoots<Brand>(
330
380
  return [...kept];
331
381
  }
332
382
 
333
- /** The keys of one compiler configuration's `paths` mapping, none where it cannot be read. */
334
- export function pathsOf(engine: Engine, configFile: string): readonly string[] {
383
+ /** One compiler configuration's `paths` mapping, empty where it cannot be read. */
384
+ export function pathsOf(engine: Engine, configFile: string): PathsMapping {
385
+ const base = dirnamePath(configFile);
335
386
  try {
336
- const paths: unknown = engine.parseConfigFile(configFile).options.paths;
337
- return typeof paths === "object" && paths !== null ? Object.keys(paths) : [];
387
+ const { options } = engine.parseConfigFile(configFile);
388
+ const { paths } = options;
389
+ // The parse answers `pathsBasePath`, the directory of the configuration that writes
390
+ // `paths` (an extended one included), though the client's option type omits it:
391
+ // https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/tsoptions/tsconfigparsing.go#L1120
392
+ const declaredAt = (options as { readonly pathsBasePath?: unknown }).pathsBasePath;
393
+ return {
394
+ base: typeof declaredAt === "string" ? declaredAt : base,
395
+ entries: Object.entries(paths ?? {}).map(([key, targets]) => ({ key, targets })),
396
+ };
338
397
  } catch {
339
- return [];
398
+ return { base, entries: [] };
340
399
  }
341
400
  }
@@ -26,8 +26,12 @@ import type { Inventory } from "./inventory.ts";
26
26
  import { must } from "./query.ts";
27
27
  import type { ProjectView } from "./session.ts";
28
28
 
29
- /** The object types whose members the walk reads whatever declares them. */
30
- const WRITTEN = ObjectFlags.ObjectLiteral | ObjectFlags.Mapped;
29
+ /**
30
+ * The object types whose members the walk reads whatever declares them. A reverse-mapped
31
+ * type has no symbol: it is the shape the checker infers from an argument the target
32
+ * writes against a homomorphic mapped type.
33
+ */
34
+ const WRITTEN = ObjectFlags.ObjectLiteral | ObjectFlags.Mapped | ObjectFlags.ReverseMapped;
31
35
 
32
36
  /** The symbols of an anonymous object type the target writes as a type. */
33
37
  const LITERAL_SYMBOLS = SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral;
@@ -38,7 +42,7 @@ export interface Reached {
38
42
  readonly classes: ReadonlySet<string>;
39
43
  /**
40
44
  * The members of the interfaces and object types the target writes, other than its
41
- * classes: each data member, and each method named `toJSON` or `toString`.
45
+ * classes: each data member, and each method or getter named `toJSON` or `toString`.
42
46
  */
43
47
  readonly members: readonly TSSymbol[];
44
48
  }
@@ -51,6 +55,13 @@ interface ValueReach {
51
55
  /** The two conversion methods a serializer or a formatter resolves on a value's own shape. */
52
56
  const CONVERSION_NAMES: ReadonlySet<string> = new Set(["toJSON", "toString"]);
53
57
 
58
+ /**
59
+ * The members that can answer a conversion: a serializer reads the name with `[[Get]]`
60
+ * and calls what it answers, so a getter answers it as a method does:
61
+ * https://tc39.es/ecma262/#sec-serializejsonproperty (step 2).
62
+ */
63
+ export const CONVERSION_MEMBERS = SymbolFlags.Method | SymbolFlags.GetAccessor;
64
+
54
65
  /**
55
66
  * The types each value is checked against by the `satisfies` expressions and type assertions
56
67
  * it is written as, through parentheses, and the innermost operand's own type where there is
@@ -147,8 +158,10 @@ export function valueReach<Brand>(
147
158
  if ((type.objectFlags & ObjectFlags.Anonymous) !== 0) {
148
159
  return (symbol.flags & LITERAL_SYMBOLS) !== 0;
149
160
  }
161
+ // A class the inventory holds is read as a class before this, so an own class
162
+ // here is one it does not hold, such as a class declared in a function.
150
163
  return (
151
- (symbol.flags & SymbolFlags.Interface) !== 0 &&
164
+ (symbol.flags & (SymbolFlags.Interface | SymbolFlags.Class)) !== 0 &&
152
165
  symbol.declarations.every((handle) => own.has(handle.path))
153
166
  );
154
167
  };
@@ -178,12 +191,12 @@ export function valueReach<Brand>(
178
191
  return !isList && isWritten(type);
179
192
  };
180
193
 
181
- /** The data members and conversion methods of one written object type. */
194
+ /** The data members and conversion members of one written object type. */
182
195
  const carried = (type: Type): readonly TSSymbol[] =>
183
196
  must(queries.propertiesOf(type)).filter(
184
197
  (property) =>
185
198
  (property.flags & SymbolFlags.Property) !== 0 ||
186
- ((property.flags & SymbolFlags.Method) !== 0 && CONVERSION_NAMES.has(property.name)),
199
+ ((property.flags & CONVERSION_MEMBERS) !== 0 && CONVERSION_NAMES.has(property.name)),
187
200
  );
188
201
 
189
202
  /** The types a value of one type holds, read once per type. */
@@ -229,6 +229,7 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
229
229
  let partial: readonly string[];
230
230
  let warnings: readonly ComponentWarning[];
231
231
  let unread: readonly string[];
232
+ let unfound: readonly string[];
232
233
  try {
233
234
  const root = runRelative(invoked, targetRoot);
234
235
  const mode = { production: true };
@@ -256,6 +257,10 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
256
257
  ({ configFile, reason }) =>
257
258
  `${relativePath(resolvePath(invoked, targetRoot), configFile) ?? configFile}: its component files are not read: ${reason}`,
258
259
  );
260
+ unfound = run.librariesUnfound.map(
261
+ ({ configFile, error }) =>
262
+ `${relativePath(resolvePath(invoked, targetRoot), configFile) ?? configFile}: ${error}: the configuration is analyzed without that library`,
263
+ );
259
264
  partial = partialNotes({
260
265
  notBuilt: run.notBuilt,
261
266
  unanswered: run.unanswered,
@@ -325,7 +330,7 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
325
330
  for (const note of partial) {
326
331
  err.write(`deadset-ts: ${note}\n`);
327
332
  }
328
- for (const line of unread) {
333
+ for (const line of [...unread, ...unfound]) {
329
334
  err.write(`deadset-ts: ${line}\n`);
330
335
  }
331
336
  for (const warning of warnings) {
@@ -66,10 +66,12 @@ function partialOf(text: string): Partial | undefined {
66
66
  parts.push(Number(part));
67
67
  }
68
68
  const prerelease = prereleaseOf(match[4]);
69
- if (prerelease === undefined || (prerelease.length > 0 && wild)) {
69
+ if (prerelease === undefined) {
70
70
  return undefined;
71
71
  }
72
- return { major: parts[0], minor: parts[1], patch: parts[2], prerelease };
72
+ // node-semver drops the prerelease of a version with a wildcard part (`1.2.x-beta` is `1.2.x`):
73
+ // https://github.com/npm/node-semver/blob/v7.8.5/classes/range.js#L409-L430
74
+ return { major: parts[0], minor: parts[1], patch: parts[2], prerelease: wild ? [] : prerelease };
73
75
  }
74
76
 
75
77
  /** One exact version, or `undefined` where the text is not one. */
@@ -152,11 +154,14 @@ function passes(version: Version, comparator: Comparator): boolean {
152
154
  }
153
155
  }
154
156
 
155
- /** The comparators a version with missing parts stands for: any value of each. */
157
+ /**
158
+ * The comparators a version with missing parts stands for: any value of each. A wildcard
159
+ * major is no comparator, so it never refuses a 0.0.0 prerelease another comparator admits.
160
+ */
156
161
  function anyOf(partial: Partial): Comparator[] {
157
162
  const { major, minor } = partial;
158
163
  if (major === undefined) {
159
- return [{ operator: ">=", version: exact(0, 0, 0) }];
164
+ return [];
160
165
  }
161
166
  if (minor === undefined) {
162
167
  return [
@@ -210,7 +215,7 @@ function primitive(operator: "<" | "<=" | ">" | ">=", partial: Partial): Compara
210
215
  if (major === undefined) {
211
216
  return operator === "<" || operator === ">"
212
217
  ? [{ operator: "<", version: floorOf(0, 0, 0) }]
213
- : [{ operator: ">=", version: exact(0, 0, 0) }];
218
+ : [];
214
219
  }
215
220
  if (minor !== undefined && patch !== undefined) {
216
221
  return [{ operator, version: lowest(partial) }];
@@ -45,6 +45,7 @@ const DEPENDENCY_MEMBERS = [
45
45
  ] as const;
46
46
 
47
47
  const SOURCE = /(?<!\.d)\.(?:[mc]?ts|tsx)$/u;
48
+ const OUTPUT = /(?:\.d\.[mc]?ts|\.[mc]?js|\.jsx)$/u;
48
49
 
49
50
  function isRecord(value: unknown): value is Record<string, unknown> {
50
51
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -77,10 +78,17 @@ function exportsEntry(
77
78
  exportsField: unknown,
78
79
  subpath: string,
79
80
  ): { readonly value: unknown; readonly match?: string } | undefined {
80
- const map =
81
- isRecord(exportsField) && Object.keys(exportsField).some((key) => key.startsWith("."))
82
- ? exportsField
83
- : { ".": exportsField };
81
+ const keys = isRecord(exportsField) ? Object.keys(exportsField) : [];
82
+ const dotted = keys.filter((key) => key.startsWith(".")).length;
83
+ // An object mixing subpath keys with others is invalid to Node; the compiler still
84
+ // answers `.` from its `"."` key and no other subpath:
85
+ // https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/module/resolver.go#L673-L695
86
+ if (isRecord(exportsField) && dotted > 0 && dotted < keys.length) {
87
+ return subpath === "." && Object.hasOwn(exportsField, ".")
88
+ ? { value: exportsField["."] }
89
+ : undefined;
90
+ }
91
+ const map = isRecord(exportsField) && dotted > 0 ? exportsField : { ".": exportsField };
84
92
  if (Object.hasOwn(map, subpath)) {
85
93
  return { value: map[subpath] };
86
94
  }
@@ -371,6 +379,18 @@ export function workspaceResolver(host: Host, workspace: Workspace): WorkspaceRe
371
379
  if (answered !== undefined) {
372
380
  return { kind: "default" };
373
381
  }
382
+ // A stylesheet or another asset is no module with a source, so the import of one
383
+ // keeps the compiler's resolution.
384
+ if (
385
+ targets.some(
386
+ (target) =>
387
+ !SOURCE.test(target) &&
388
+ !OUTPUT.test(target) &&
389
+ host.kindOf(joinPath(member.dir, target)) === "file",
390
+ )
391
+ ) {
392
+ return { kind: "default" };
393
+ }
374
394
  const named = targets.length === 0 ? "no target" : targets.join(", ");
375
395
  const built = targets.some((target) => host.kindOf(joinPath(member.dir, target)) === "file");
376
396
  return {
package/src/workspace.ts CHANGED
@@ -274,7 +274,7 @@ function memberDirectories(
274
274
  /** Whether a workspace's patterns name one directory below its root, read without listing any. */
275
275
  function namesDirectory(root: string, patterns: readonly string[], dir: string): boolean {
276
276
  const below = relativePath(root, dir);
277
- if (below === undefined || below.startsWith("..")) {
277
+ if (below === undefined) {
278
278
  return false;
279
279
  }
280
280
  const path = below.split("/");