@cplieger/deadset-ts 7.2.0-dev.3 → 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 +4 -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 +1 -1
  35. package/src/analysis.ts +32 -6
  36. package/src/component-files.ts +116 -31
  37. package/src/configuration-files.ts +14 -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
@@ -38,10 +38,12 @@ function partialOf(text) {
38
38
  parts.push(Number(part));
39
39
  }
40
40
  const prerelease = prereleaseOf(match[4]);
41
- if (prerelease === undefined || (prerelease.length > 0 && wild)) {
41
+ if (prerelease === undefined) {
42
42
  return undefined;
43
43
  }
44
- return { major: parts[0], minor: parts[1], patch: parts[2], prerelease };
44
+ // node-semver drops the prerelease of a version with a wildcard part (`1.2.x-beta` is `1.2.x`):
45
+ // https://github.com/npm/node-semver/blob/v7.8.5/classes/range.js#L409-L430
46
+ return { major: parts[0], minor: parts[1], patch: parts[2], prerelease: wild ? [] : prerelease };
45
47
  }
46
48
  /** One exact version, or `undefined` where the text is not one. */
47
49
  function versionOf(text) {
@@ -111,11 +113,14 @@ function passes(version, comparator) {
111
113
  return order === 0;
112
114
  }
113
115
  }
114
- /** The comparators a version with missing parts stands for: any value of each. */
116
+ /**
117
+ * The comparators a version with missing parts stands for: any value of each. A wildcard
118
+ * major is no comparator, so it never refuses a 0.0.0 prerelease another comparator admits.
119
+ */
115
120
  function anyOf(partial) {
116
121
  const { major, minor } = partial;
117
122
  if (major === undefined) {
118
- return [{ operator: ">=", version: exact(0, 0, 0) }];
123
+ return [];
119
124
  }
120
125
  if (minor === undefined) {
121
126
  return [
@@ -165,7 +170,7 @@ function primitive(operator, partial) {
165
170
  if (major === undefined) {
166
171
  return operator === "<" || operator === ">"
167
172
  ? [{ operator: "<", version: floorOf(0, 0, 0) }]
168
- : [{ operator: ">=", version: exact(0, 0, 0) }];
173
+ : [];
169
174
  }
170
175
  if (minor !== undefined && patch !== undefined) {
171
176
  return [{ operator, version: lowest(partial) }];
@@ -19,6 +19,7 @@ const DEPENDENCY_MEMBERS = [
19
19
  "optionalDependencies",
20
20
  ];
21
21
  const SOURCE = /(?<!\.d)\.(?:[mc]?ts|tsx)$/u;
22
+ const OUTPUT = /(?:\.d\.[mc]?ts|\.[mc]?js|\.jsx)$/u;
22
23
  function isRecord(value) {
23
24
  return typeof value === "object" && value !== null && !Array.isArray(value);
24
25
  }
@@ -42,9 +43,17 @@ function inside(dir, path) {
42
43
  }
43
44
  /** The `exports` entry one subpath selects, with what a pattern's `*` matched. */
44
45
  function exportsEntry(exportsField, subpath) {
45
- const map = isRecord(exportsField) && Object.keys(exportsField).some((key) => key.startsWith("."))
46
- ? exportsField
47
- : { ".": exportsField };
46
+ const keys = isRecord(exportsField) ? Object.keys(exportsField) : [];
47
+ const dotted = keys.filter((key) => key.startsWith(".")).length;
48
+ // An object mixing subpath keys with others is invalid to Node; the compiler still
49
+ // answers `.` from its `"."` key and no other subpath:
50
+ // https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/module/resolver.go#L673-L695
51
+ if (isRecord(exportsField) && dotted > 0 && dotted < keys.length) {
52
+ return subpath === "." && Object.hasOwn(exportsField, ".")
53
+ ? { value: exportsField["."] }
54
+ : undefined;
55
+ }
56
+ const map = isRecord(exportsField) && dotted > 0 ? exportsField : { ".": exportsField };
48
57
  if (Object.hasOwn(map, subpath)) {
49
58
  return { value: map[subpath] };
50
59
  }
@@ -286,6 +295,13 @@ export function workspaceResolver(host, workspace) {
286
295
  if (answered !== undefined) {
287
296
  return { kind: "default" };
288
297
  }
298
+ // A stylesheet or another asset is no module with a source, so the import of one
299
+ // keeps the compiler's resolution.
300
+ if (targets.some((target) => !SOURCE.test(target) &&
301
+ !OUTPUT.test(target) &&
302
+ host.kindOf(joinPath(member.dir, target)) === "file")) {
303
+ return { kind: "default" };
304
+ }
289
305
  const named = targets.length === 0 ? "no target" : targets.join(", ");
290
306
  const built = targets.some((target) => host.kindOf(joinPath(member.dir, target)) === "file");
291
307
  return {
@@ -205,7 +205,7 @@ function memberDirectories(host, root, patterns) {
205
205
  /** Whether a workspace's patterns name one directory below its root, read without listing any. */
206
206
  function namesDirectory(root, patterns, dir) {
207
207
  const below = relativePath(root, dir);
208
- if (below === undefined || below.startsWith("..")) {
208
+ if (below === undefined) {
209
209
  return false;
210
210
  }
211
211
  const path = below.split("/");
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "7.2.0-dev.3",
3
+ "version": "7.2.0-dev.4",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "7.2.0-dev.3",
3
+ "version": "7.2.0-dev.4",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",
package/src/analysis.ts CHANGED
@@ -80,7 +80,14 @@ import {
80
80
  type Matrix,
81
81
  type SweepResult,
82
82
  } from "./matrix.ts";
83
- import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "./paths.ts";
83
+ import {
84
+ dirnamePath,
85
+ extensionOf,
86
+ joinPath,
87
+ normalizePath,
88
+ relativePath,
89
+ resolvePath,
90
+ } from "./paths.ts";
84
91
  import type { UnansweredQuestion } from "./query.ts";
85
92
  import { byPosition, isComponentFile, type Position } from "./position.ts";
86
93
  import { LANGUAGE, type TypeErrorSkip, type UnansweredCount } from "./report.ts";
@@ -232,6 +239,8 @@ interface ReadProjects<Answer> {
232
239
  readonly unanswered: readonly UnansweredQuestion[];
233
240
  /** Per configuration whose component files the run cannot read, the reason. */
234
241
  readonly componentsUnread: readonly ComponentsUnread[];
242
+ /** Per analyzed configuration, each library it names that the compiler cannot find. */
243
+ readonly librariesUnfound: readonly LibraryUnfound[];
235
244
  /** The convention rows the run applied. */
236
245
  readonly conventionsApplied: readonly AppliedConvention[];
237
246
  /** The dependencies the applied rows' short names use. */
@@ -394,6 +403,7 @@ function readProjects<Answer>(
394
403
  const failures: LocatedDiagnostic[] = [];
395
404
  const unbuildable: SetupFailure[] = [];
396
405
  const setupDropped: SetupFailure[] = [];
406
+ const librariesUnfound: LibraryUnfound[] = [];
397
407
  const consumed = new Map<string, Reference>();
398
408
  const heldFiles = new Set<string>();
399
409
  let outsideTests: readonly OutsideTest[] = [];
@@ -458,10 +468,6 @@ function readProjects<Answer>(
458
468
  : resolvePath(host.workingDirectory(), consumer.path),
459
469
  paths: pathsOf(engine, project.configFile),
460
470
  });
461
- if (reading.failing.length > 0) {
462
- failures.push(...reading.failing);
463
- return undefined;
464
- }
465
471
  if (consumer !== undefined && reading.missing.length > 0) {
466
472
  unbuildable.push(
467
473
  ...reading.missing.map((one) => ({
@@ -502,6 +508,12 @@ function readProjects<Answer>(
502
508
  }
503
509
  return undefined;
504
510
  }
511
+ librariesUnfound.push(
512
+ ...raw.unfound.map((one) => ({
513
+ configFile: project.configFile,
514
+ error: renderDiagnostic(one, (path) => relativePath(absoluteRoot, path) ?? path),
515
+ })),
516
+ );
505
517
  const configuration = ids.get(project.configFile) ?? project.configFile;
506
518
  const read = (): ProjectRead => {
507
519
  const held = inventory(project, host, targetRoot);
@@ -589,6 +601,7 @@ function readProjects<Answer>(
589
601
  notBuilt,
590
602
  unanswered: session.unanswered,
591
603
  componentsUnread: workspace?.componentsUnread ?? [],
604
+ librariesUnfound,
592
605
  conventionsApplied: conventions.applied,
593
606
  conventionUses: conventions.uses,
594
607
  memberDirs,
@@ -775,6 +788,8 @@ interface ReadRun<Extra> {
775
788
  readonly unanswered: readonly UnansweredQuestion[];
776
789
  /** Per configuration whose component files the run cannot read, the reason. */
777
790
  readonly componentsUnread: readonly ComponentsUnread[];
791
+ /** Per analyzed configuration, each library it names that the compiler cannot find. */
792
+ readonly librariesUnfound: readonly LibraryUnfound[];
778
793
  /** The convention rows the run applied. */
779
794
  readonly conventionsApplied: readonly AppliedConvention[];
780
795
  /** The dependencies the applied rows' short names use. */
@@ -842,7 +857,7 @@ function readRun<Extra>(
842
857
  evidence,
843
858
  componentFiles: components.map(pathOf),
844
859
  componentWarnings: components.flatMap((file) =>
845
- readComponent(file.originalText).warnings.map((warning) => ({
860
+ readComponent(file.originalText, extensionOf(file.fileName)).warnings.map((warning) => ({
846
861
  path: pathOf(file),
847
862
  line: lineOf(file.originalText, warning.offset),
848
863
  reason: warning.reason,
@@ -942,6 +957,7 @@ function readRun<Extra>(
942
957
  notBuilt: read.notBuilt,
943
958
  unanswered: read.unanswered,
944
959
  componentsUnread: read.componentsUnread,
960
+ librariesUnfound: read.librariesUnfound,
945
961
  conventionsApplied: read.conventionsApplied,
946
962
  conventionUses: read.conventionUses,
947
963
  memberDirs: read.memberDirs,
@@ -1341,6 +1357,7 @@ function readAnalysis(
1341
1357
  notBuilt: read.notBuilt,
1342
1358
  unanswered: read.unanswered,
1343
1359
  componentsUnread: read.componentsUnread,
1360
+ librariesUnfound: read.librariesUnfound,
1344
1361
  conventionsApplied: read.conventionsApplied,
1345
1362
  },
1346
1363
  configured: read.projects.map((one) => one.configured),
@@ -1418,6 +1435,8 @@ interface RunFacts {
1418
1435
  readonly componentWarnings: readonly ComponentWarning[];
1419
1436
  /** Per configuration whose component files the run cannot read, the reason. */
1420
1437
  readonly componentsUnread: readonly ComponentsUnread[];
1438
+ /** Per analyzed configuration, each library it names that the compiler cannot find. */
1439
+ readonly librariesUnfound: readonly LibraryUnfound[];
1421
1440
  /** The convention rows the run applied. */
1422
1441
  readonly conventionsApplied: readonly AppliedConvention[];
1423
1442
  }
@@ -1428,6 +1447,13 @@ interface ComponentsUnread {
1428
1447
  readonly reason: string;
1429
1448
  }
1430
1449
 
1450
+ /** One library an analyzed configuration names that the compiler cannot find. */
1451
+ interface LibraryUnfound {
1452
+ readonly configFile: string;
1453
+ /** The compiler's error, rendered as a dropped configuration's is. */
1454
+ readonly error: string;
1455
+ }
1456
+
1431
1457
  /** The findings of one run, beside what a report states about the run itself. */
1432
1458
  export interface RunAnalysis {
1433
1459
  readonly result: PassResult;
@@ -188,18 +188,25 @@ function scriptTagAt(text: string, offset: number): boolean {
188
188
  );
189
189
  }
190
190
 
191
+ /** Where the braces opening at one offset end, and whether a `}` matched them. */
192
+ interface Braces {
193
+ /** Past the matching `}`, or where the scan stopped: a line-start `<script` or the end. */
194
+ readonly end: number;
195
+ readonly closed: boolean;
196
+ }
197
+
191
198
  /**
192
- * The offset past the `}` matching the `{` at `from`, quoted strings skipped, or -1 where
193
- * none matches before the end or before a `<script` start tag that begins a line, so a
194
- * stray brace in markup never hides a block.
199
+ * The braces opening at `from`, quoted strings skipped. The scan stops unclosed at the
200
+ * end, or before a `<script` start tag that begins a line, so a stray brace in markup
201
+ * never hides a block.
195
202
  */
196
- function pastBraces(text: string, from: number): number {
203
+ function scanBraces(text: string, from: number): Braces {
197
204
  let depth = 0;
198
205
  let at = from;
199
206
  while (at < text.length) {
200
207
  const code = text.charCodeAt(at);
201
208
  if (scriptTagAt(text, at)) {
202
- return -1;
209
+ return { end: at, closed: false };
203
210
  }
204
211
  if (code === 0x22 || code === 0x27 || code === 0x60) {
205
212
  at = pastQuoted(text, at);
@@ -210,12 +217,59 @@ function pastBraces(text: string, from: number): number {
210
217
  } else if (code === 0x7d) {
211
218
  depth -= 1;
212
219
  if (depth === 0) {
213
- return at + 1;
220
+ return { end: at + 1, closed: true };
214
221
  }
215
222
  }
216
223
  at += 1;
217
224
  }
218
- return -1;
225
+ return { end: text.length, closed: false };
226
+ }
227
+
228
+ /** The offset past the `}` matching the `{` at `from`, or -1 where {@link scanBraces} finds none. */
229
+ function pastBraces(text: string, from: number): number {
230
+ const braces = scanBraces(text, from);
231
+ return braces.closed ? braces.end : -1;
232
+ }
233
+
234
+ /** How one framework's parser reads the forms the frameworks disagree on. */
235
+ interface Dialect {
236
+ /** Whether `<!-->`, `<!--->` and `--!>` end a comment, the opening dashes counted. */
237
+ readonly shortComments: boolean;
238
+ /** Whether a start tag's unmatched `{` is a character of an attribute name or value. */
239
+ readonly braceIsText: boolean;
240
+ }
241
+
242
+ /**
243
+ * The dialect each framework's parser reads: Vue's tokenizer, Svelte's parser and Astro's
244
+ * tokenizer, the last for any other extension too. `.vue` also closes `--!>`, which Vue
245
+ * does not, so a block Vue drops there is still read.
246
+ * https://github.com/vuejs/core/blob/main/packages/compiler-core/src/tokenizer.ts
247
+ * https://github.com/sveltejs/svelte/blob/main/packages/svelte/src/compiler/phases/1-parse/state/element.js
248
+ * https://github.com/withastro/compiler/blob/main/internal/token.go
249
+ */
250
+ function dialectOf(extension: string): Dialect {
251
+ switch (extension.toLowerCase()) {
252
+ case ".svelte":
253
+ return { shortComments: false, braceIsText: false };
254
+ case ".vue":
255
+ return { shortComments: true, braceIsText: true };
256
+ default:
257
+ return { shortComments: true, braceIsText: false };
258
+ }
259
+ }
260
+
261
+ /** The offset past the comment whose `<!--` is at `lt`, or the text's length where none ends it. */
262
+ function pastComment(text: string, lt: number, dialect: Dialect): number {
263
+ if (!dialect.shortComments) {
264
+ const close = text.indexOf("-->", lt + 4);
265
+ return close < 0 ? text.length : close + 3;
266
+ }
267
+ const dashes = text.indexOf("-->", lt + 2);
268
+ const bang = text.indexOf("--!>", lt + 2);
269
+ if (bang >= 0 && (dashes < 0 || bang < dashes)) {
270
+ return bang + 4;
271
+ }
272
+ return dashes < 0 ? text.length : dashes + 3;
219
273
  }
220
274
 
221
275
  interface Attribute {
@@ -234,17 +288,43 @@ interface StartTag {
234
288
  readonly attributes: readonly Attribute[];
235
289
  }
236
290
 
237
- /** The start tag whose `<` is at `from`, to its `>`. A quoted or braced value may hold `>`. */
238
- function readStartTag(text: string, from: number, nameStop: number): StartTag {
291
+ /**
292
+ * Where an unmatched `{` of a start tag ends its braced value: at the first `>`, or where
293
+ * the brace scan stopped where that comes first.
294
+ */
295
+ function unmatchedBraceEnd(text: string, from: number, braces: Braces): number {
296
+ const close = text.indexOf(">", from);
297
+ return close >= 0 && close < braces.end ? close : braces.end;
298
+ }
299
+
300
+ /**
301
+ * The start tag whose `<` is at `from`, to its `>`. A quoted or braced value may hold `>`.
302
+ * Past an unmatched `{`, the tag also ends before a `<script` start tag that begins a line.
303
+ */
304
+ function readStartTag(text: string, from: number, nameStop: number, dialect: Dialect): StartTag {
239
305
  const name = text.slice(from + 1, nameStop).toLowerCase();
240
306
  const attributes: Attribute[] = [];
241
307
  let at = nameStop;
242
308
  let selfClosing = false;
309
+ let unmatched = false;
310
+ /** The braces opening at `at`, and whether they make a braced value rather than text. */
311
+ const bracesAt = (): { readonly braces: Braces; readonly value: boolean } | undefined => {
312
+ if (text.charCodeAt(at) !== 0x7b) {
313
+ return undefined;
314
+ }
315
+ const braces = scanBraces(text, at);
316
+ return { braces, value: braces.closed || !dialect.braceIsText };
317
+ };
318
+ const bracedEnd = (braces: Braces): number =>
319
+ braces.closed ? braces.end : unmatchedBraceEnd(text, at, braces);
243
320
  while (at < text.length) {
244
321
  const code = text.charCodeAt(at);
245
322
  if (code === 0x3e) {
246
323
  return { name, end: at + 1, selfClosing, attributes };
247
324
  }
325
+ if (unmatched && scriptTagAt(text, at)) {
326
+ return { name, end: at, selfClosing, attributes };
327
+ }
248
328
  if (isSpace(code)) {
249
329
  at += 1;
250
330
  continue;
@@ -256,12 +336,13 @@ function readStartTag(text: string, from: number, nameStop: number): StartTag {
256
336
  }
257
337
  selfClosing = false;
258
338
  const start = at;
259
- if (code === 0x7b) {
260
- const past = pastBraces(text, at);
261
- at = past < 0 ? text.length : past;
339
+ const bare = bracesAt();
340
+ unmatched ||= bare?.braces.closed === false;
341
+ if (bare?.value === true) {
342
+ at = bracedEnd(bare.braces);
262
343
  attributes.push({
263
344
  name: "",
264
- value: text.slice(start + 1, at - 1),
345
+ value: text.slice(start + 1, bare.braces.closed ? at - 1 : at),
265
346
  start,
266
347
  end: at,
267
348
  valueStart: start + 1,
@@ -287,16 +368,18 @@ function readStartTag(text: string, from: number, nameStop: number): StartTag {
287
368
  at += 1;
288
369
  }
289
370
  const opening = text.charCodeAt(at);
371
+ const braced = bracesAt();
372
+ unmatched ||= braced?.braces.closed === false;
290
373
  if (opening === 0x22 || opening === 0x27) {
291
- const past = pastQuoted(text, at);
374
+ // An attribute value has no escapes: a backslash before its quote is text.
375
+ const close = text.indexOf(text.charAt(at), at + 1);
292
376
  valueStart = at + 1;
293
- value = text.slice(at + 1, Math.max(past - 1, at + 1));
294
- at = past;
295
- } else if (opening === 0x7b) {
296
- const past = pastBraces(text, at);
297
- const stop = past < 0 ? text.length : past;
377
+ value = text.slice(at + 1, close < 0 ? text.length : close);
378
+ at = close < 0 ? text.length : close + 1;
379
+ } else if (braced?.value === true) {
380
+ const stop = bracedEnd(braced.braces);
298
381
  valueStart = at + 1;
299
- value = text.slice(at + 1, Math.max(stop - 1, at + 1));
382
+ value = text.slice(at + 1, braced.braces.closed ? stop - 1 : stop);
300
383
  at = stop;
301
384
  } else {
302
385
  valueStart = at;
@@ -410,9 +493,11 @@ function scriptsInside(text: string, from: number, to: number): number[] {
410
493
  * The scan keeps a stack of open elements. A comment, a doctype or processing instruction and
411
494
  * an expression in braces are text, and an end tag closes back to the nearest open element of
412
495
  * its name. A raw-text element at the top level runs to its own end tag, and only a `script`
413
- * element whose start tag begins a line at the top level is a block.
496
+ * element whose start tag begins a line at the top level is a block. The file's extension
497
+ * names the framework whose reading of a comment and of a stray brace applies.
414
498
  */
415
- export function readComponent(text: string): ComponentFile {
499
+ export function readComponent(text: string, extension: string): ComponentFile {
500
+ const dialect = dialectOf(extension);
416
501
  const blocks: Block[] = [];
417
502
  const imports: SourceImport[] = [];
418
503
  const warnings: ComponentWarning[] = [];
@@ -442,8 +527,7 @@ export function readComponent(text: string): ComponentFile {
442
527
  }
443
528
  const lt = at;
444
529
  if (text.startsWith("<!--", lt)) {
445
- const close = text.indexOf("-->", lt + 4);
446
- at = close < 0 ? text.length : close + 3;
530
+ at = pastComment(text, lt, dialect);
447
531
  continue;
448
532
  }
449
533
  const next = text.charCodeAt(lt + 1);
@@ -467,7 +551,7 @@ export function readComponent(text: string): ComponentFile {
467
551
  at = lt + 1;
468
552
  continue;
469
553
  }
470
- const tag = readStartTag(text, lt, nameStop);
554
+ const tag = readStartTag(text, lt, nameStop, dialect);
471
555
  const rawText = open.length === 0 && RAW_TEXT.has(tag.name);
472
556
  for (const attribute of rawText ? [] : tag.attributes) {
473
557
  if (attribute.value !== "") {
@@ -542,9 +626,9 @@ export function readComponent(text: string): ComponentFile {
542
626
  }
543
627
 
544
628
  /**
545
- * Whether the module's text exports a default at its top level: an `export default`
546
- * or an export clause naming `default`, outside every comment, string, template and
547
- * regular expression, and outside every bracket.
629
+ * Whether the module's text exports a default at its top level: an `export default`, an
630
+ * `export * as default`, or an export clause naming `default`, outside every comment,
631
+ * string, template and regular expression, and outside every bracket.
548
632
  */
549
633
  export function exportsDefault(text: string): boolean {
550
634
  const words: string[] = [];
@@ -633,7 +717,8 @@ export function exportsDefault(text: string): boolean {
633
717
  const matched = word.exec(text);
634
718
  if (matched !== null) {
635
719
  const name = matched[0];
636
- if (depth === 0 && words.at(-1) === "export" && name === "default") {
720
+ const starAs = words.at(-3) === "export" && words.at(-2) === "*" && words.at(-1) === "as";
721
+ if (depth === 0 && (words.at(-1) === "export" || starAs) && name === "default") {
637
722
  return true;
638
723
  }
639
724
  if (clause && name === "default") {
@@ -662,8 +747,8 @@ export function exportsDefault(text: string): boolean {
662
747
  }
663
748
 
664
749
  /** The module one component file's text is read as, and the span map back to the file. */
665
- export function componentModule(text: string): ComponentModule {
666
- const { blocks, imports } = readComponent(text);
750
+ export function componentModule(text: string, fileExtension: string): ComponentModule {
751
+ const { blocks, imports } = readComponent(text, fileExtension);
667
752
  const out: string[] = [];
668
753
  let at = 0;
669
754
  let language: BlockLanguage | undefined;
@@ -278,8 +278,15 @@ function readDocument(host: Host, path: string): unknown {
278
278
 
279
279
  /** The configuration files one project holds and sits beside. */
280
280
  interface ConfigurationFiles {
281
- /** Every own file that is a configuration module, with the form its name has. */
282
- readonly modules: readonly { readonly file: SourceFile; readonly form: string }[];
281
+ /**
282
+ * Every own file that is a configuration module, with the form its name has and the
283
+ * directory of the manifest whose dependencies its strings are read against.
284
+ */
285
+ readonly modules: readonly {
286
+ readonly file: SourceFile;
287
+ readonly form: string;
288
+ readonly manifestDir: string;
289
+ }[];
283
290
  /**
284
291
  * Every configuration module in a directory of the project that the program does not
285
292
  * hold, and every file such a file imports or re-exports by a relative specifier that
@@ -345,12 +352,13 @@ export function configurationFiles(
345
352
  return held;
346
353
  };
347
354
 
348
- const modules: { file: SourceFile; form: string }[] = [];
355
+ const modules: { file: SourceFile; form: string; manifestDir: string }[] = [];
349
356
  const dirs = new Set<string>([root]);
350
357
  for (const file of files.byPath.values()) {
351
358
  const form = configurationModuleForm(basename(file.fileName));
352
- if (form !== undefined && holdsManifest(dirnamePath(file.fileName))) {
353
- modules.push({ file, form });
359
+ const fileDir = dirnamePath(file.fileName);
360
+ if (form !== undefined && holdsManifest(fileDir)) {
361
+ modules.push({ file, form, manifestDir: fileDir });
354
362
  }
355
363
  for (
356
364
  let dir = dirnamePath(file.fileName);
@@ -383,7 +391,7 @@ export function configurationFiles(
383
391
  if (path.endsWith(".json")) {
384
392
  pending.push({ path, manifestDir: dir });
385
393
  } else if (held !== undefined) {
386
- modules.push({ file: held, form: "a script's configuration flag" });
394
+ modules.push({ file: held, form: "a script's configuration flag", manifestDir: dir });
387
395
  } else {
388
396
  named.push({ path, manifestDir: dir });
389
397
  }
@@ -20,7 +20,7 @@ import { SymbolFlags, type NodeHandle, type Symbol as TSSymbol } from "@typescri
20
20
  import type { AliasChains } from "./alias-chain.ts";
21
21
  import { resolvedTargets } from "./calls.ts";
22
22
  import type { Config, DeclarationEntry } from "./config.ts";
23
- import type { Inventory } from "./inventory.ts";
23
+ import { declaredComponent, type Inventory } from "./inventory.ts";
24
24
  import { must, Unanswerable } from "./query.ts";
25
25
  import { nameComponent } from "./ref.ts";
26
26
  import type { ProjectView } from "./session.ts";
@@ -118,15 +118,44 @@ function candidatesOf<Brand>(
118
118
  ];
119
119
  }
120
120
 
121
- /** The member of one container a path component spells, followed through any alias. */
121
+ /**
122
+ * The properties of the type one container declares, or of its constructor's type for a
123
+ * static member. A member a symbol keys is bound to that type later than the container's
124
+ * symbol table is built, so the table does not hold it.
125
+ */
126
+ function typeMembersOf<Brand>(
127
+ project: ProjectView<Brand>,
128
+ container: TSSymbol,
129
+ isStatic: boolean,
130
+ ): readonly TSSymbol[] {
131
+ const queries = project.queries;
132
+ const type = must(isStatic ? queries.typeOfSymbol(container) : queries.declaredTypeOf(container));
133
+ return type === undefined ? [] : must(queries.propertiesOf(type));
134
+ }
135
+
136
+ /**
137
+ * The member of one container a path component spells, followed through any alias. A
138
+ * component in brackets is a computed key, compared with the key each declaration of a
139
+ * candidate writes: the checker names a member a symbol keys by no spelling of its own.
140
+ */
122
141
  function memberOf<Brand>(
123
142
  project: ProjectView<Brand>,
124
143
  container: TSSymbol,
125
144
  component: string,
126
145
  isStatic: boolean,
127
146
  ): TSSymbol | undefined {
128
- const member = candidatesOf(project, container, isStatic).find(
129
- (candidate) => nameComponent(candidate.name) === component,
147
+ const computed = component.startsWith("[");
148
+ const candidates = candidatesOf(project, container, isStatic);
149
+ const member = (
150
+ computed ? [...candidates, ...typeMembersOf(project, container, isStatic)] : candidates
151
+ ).find(
152
+ (candidate) =>
153
+ nameComponent(candidate.name) === component ||
154
+ (computed &&
155
+ candidate.declarations.some((handle) => {
156
+ const node = project.declarationAt(handle)?.node;
157
+ return node !== undefined && declaredComponent(node) === component;
158
+ })),
130
159
  );
131
160
  return member === undefined ? undefined : unaliased(project, member);
132
161
  }
@@ -4,7 +4,7 @@ export const RECORD = {
4
4
  conformance: {
5
5
  corpusVersion: "5.2.0",
6
6
  result: "pass",
7
- digest: "sha256:4050d1b67fb757bc12a60f8ece7317a3e8485d3383ec52919cd24b9dfd5509dc",
7
+ digest: "sha256:012872542679539ec105f49b5ee502b0a9fa35c6bbd9ef3983b5f6e53af83d8e",
8
8
  },
9
9
  gaps: [],
10
10
  } as const;