@cplieger/deadset-ts 5.1.1 → 5.2.1

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 (60) hide show
  1. package/dist/src/analysis.js +7 -4
  2. package/dist/src/config.js +3 -0
  3. package/dist/src/configuration-files.js +23 -24
  4. package/dist/src/conformance-record.js +2 -2
  5. package/dist/src/contextual-uses.js +25 -4
  6. package/dist/src/convention-rows.js +7 -0
  7. package/dist/src/conventions.js +97 -33
  8. package/dist/src/dependencies.js +8 -0
  9. package/dist/src/entry-point-gaps.js +10 -26
  10. package/dist/src/entry-points.js +4 -0
  11. package/dist/src/findings/completion.js +27 -11
  12. package/dist/src/findings/interfaces.js +2 -6
  13. package/dist/src/findings/intra-function.js +15 -3
  14. package/dist/src/findings/unused-declarations.js +5 -6
  15. package/dist/src/findings/visibility-narrowing.js +6 -1
  16. package/dist/src/generated-file.js +72 -12
  17. package/dist/src/graph.js +5 -4
  18. package/dist/src/html-entries.js +137 -0
  19. package/dist/src/kinds.js +2 -2
  20. package/dist/src/manifest.js +5 -1
  21. package/dist/src/matrix.js +9 -3
  22. package/dist/src/namespace-objects.js +158 -0
  23. package/dist/src/partial.js +1 -1
  24. package/dist/src/references.js +120 -3
  25. package/dist/src/roots.js +52 -3
  26. package/dist/src/sweep.js +17 -0
  27. package/dist/src/test-support.js +35 -23
  28. package/dist/src/version.js +1 -1
  29. package/jsr.json +1 -1
  30. package/package.json +2 -2
  31. package/src/analysis.ts +12 -5
  32. package/src/config.ts +3 -0
  33. package/src/configuration-files.ts +28 -24
  34. package/src/conformance-record.ts +2 -2
  35. package/src/contextual-uses.ts +31 -4
  36. package/src/convention-rows.ts +15 -0
  37. package/src/conventions.ts +119 -30
  38. package/src/dependencies.ts +9 -0
  39. package/src/entry-point-gaps.ts +11 -28
  40. package/src/entry-points.ts +7 -1
  41. package/src/exempt.ts +1 -4
  42. package/src/file-facts.ts +1 -1
  43. package/src/findings/completion.ts +31 -11
  44. package/src/findings/interfaces.ts +2 -9
  45. package/src/findings/intra-function.ts +17 -3
  46. package/src/findings/unused-declarations.ts +5 -8
  47. package/src/findings/visibility-narrowing.ts +7 -1
  48. package/src/generated-file.ts +80 -12
  49. package/src/graph.ts +5 -4
  50. package/src/html-entries.ts +170 -0
  51. package/src/kinds.ts +2 -2
  52. package/src/manifest.ts +9 -3
  53. package/src/matrix.ts +21 -3
  54. package/src/namespace-objects.ts +216 -0
  55. package/src/partial.ts +1 -1
  56. package/src/references.ts +161 -3
  57. package/src/roots.ts +69 -10
  58. package/src/sweep.ts +19 -0
  59. package/src/test-support.ts +34 -22
  60. package/src/version.ts +1 -1
@@ -11,13 +11,13 @@ import { lineOf, readComponent } from "./component-files.js";
11
11
  import { consumerLoads, consumerReferences, moduleIdentity, } from "./consumers.js";
12
12
  import { configuredDeclarations, configuredRef, declarationsNamed, } from "./configured-declarations.js";
13
13
  import { decorator } from "./decorator.js";
14
- import { dependenciesOf, projectNeeds } from "./dependencies.js";
14
+ import { configurationNeeds, dependenciesOf, projectNeeds } from "./dependencies.js";
15
15
  import { deprecatedDeclarations } from "./deprecation.js";
16
16
  import { discoverProjects, DiscoveryError, isGuess, projectErrors, renderDiagnostic, targetRelative, } from "./discover.js";
17
17
  import { conventionFailuresFor, generatedFiles, readConventions, } from "./conventions.js";
18
18
  import { readEdgeSides } from "./edges.js";
19
19
  import { enumGroup } from "./enum-group.js";
20
- import { generatedFile } from "./generated-file.js";
20
+ import { generatedFile, generatedFilesOf } from "./generated-file.js";
21
21
  import { sourceFilesOf } from "./source-files.js";
22
22
  import { evidenceRecords, heldRecords, disabledClasses, exemptionsOf, holdingWhile, isUnansweredRecord, retainedIn, } from "./exempt.js";
23
23
  import { heldByInclusion, sourcePaths, sourceTree } from "./file-facts.js";
@@ -224,6 +224,7 @@ function readProjects(engine, host, scope, config, stage) {
224
224
  ...kept.map((id) => ({ id, kind: "type-error", source: "" })),
225
225
  ],
226
226
  };
227
+ const own = sourceFilesOf(project, targetRoot);
227
228
  return {
228
229
  configuration,
229
230
  configFile: project.configFile,
@@ -235,7 +236,7 @@ function readProjects(engine, host, scope, config, stage) {
235
236
  .ownSourceFiles()
236
237
  .filter((file) => referenceOnly(file.fileName))
237
238
  .flatMap((file) => relativePath(absoluteRoot, file.fileName) ?? []),
238
- generated: generatedFiles(conventions, sourceFilesOf(project, targetRoot)),
239
+ generated: generatedFilesOf(own, generatedFiles(conventions, own)),
239
240
  };
240
241
  };
241
242
  for (const file of project.ownSourceFiles()) {
@@ -276,6 +277,7 @@ function readProjects(engine, host, scope, config, stage) {
276
277
  unanswered: session.unanswered,
277
278
  componentsUnread: workspace?.componentsUnread ?? [],
278
279
  conventionsApplied: conventions.applied,
280
+ conventionUses: conventions.uses,
279
281
  outsideTests,
280
282
  };
281
283
  }
@@ -470,6 +472,7 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
470
472
  unanswered: read.unanswered,
471
473
  componentsUnread: read.componentsUnread,
472
474
  conventionsApplied: read.conventionsApplied,
475
+ conventionUses: read.conventionUses,
473
476
  };
474
477
  }
475
478
  /**
@@ -646,7 +649,7 @@ function emitterInputOver(host, scope, config, read, input, selfCheck) {
646
649
  references: one.configured.references,
647
650
  accessors: one.extra.accessors,
648
651
  })), swept.exempt, input.mode),
649
- dependencies: dependenciesOf(host, targetRoot, projects.map((one) => one.extra.needs), swept.sweep.candidates.map((candidate) => candidate.id)),
652
+ dependencies: dependenciesOf(host, targetRoot, [...projects.map((one) => one.extra.needs), configurationNeeds(read.conventionUses)], swept.sweep.candidates.map((candidate) => candidate.id)),
650
653
  implementations: mergeImplementations(projects.map((one) => one.extra.implementations)),
651
654
  files: {
652
655
  tree: sourceTree(host, targetRoot),
@@ -49,9 +49,12 @@ export function defaultConfig() {
49
49
  ts: {
50
50
  testFiles: [
51
51
  "**/*.test.{ts,tsx,mts,cts}",
52
+ "**/*.test-d.{ts,tsx,mts,cts}",
53
+ "**/*.spec-d.{ts,tsx,mts,cts}",
52
54
  "**/*.spec.*",
53
55
  "**/__tests__/**",
54
56
  "**/__mocks__/**",
57
+ "**/test-d/**",
55
58
  ],
56
59
  entryFiles: [],
57
60
  componentExtensions: [".vue", ".svelte", ".astro"],
@@ -164,33 +164,31 @@ function relativeSpecifiers(file) {
164
164
  });
165
165
  }
166
166
  /**
167
- * The module one relative specifier from a configuration file reaches: the path it names,
168
- * that path with its JavaScript extension replaced by the TypeScript counterparts, that
169
- * path with an extension appended, and a directory's `index` with one appended.
167
+ * The paths one relative specifier from a configuration file may reach, in the order they
168
+ * are tried: the path it names where that ends with a module extension, that path with its
169
+ * JavaScript extension replaced by the TypeScript counterparts, that path with an
170
+ * extension appended, and a directory's `index` with one appended. A data file such as
171
+ * `./package.json` is no module, so it is no candidate.
170
172
  */
171
- function reachedBy(host, dir, specifier) {
173
+ export function specifierCandidates(dir, specifier) {
172
174
  const path = resolvePath(dir, specifier);
173
- const isFile = (candidate) => host.kindOf(candidate) === "file";
174
- // A data file such as `./package.json` is no module, so its strings use no dependency.
175
- if (isFile(path) && APPENDED.some((extension) => path.endsWith(extension))) {
176
- return path;
175
+ const candidates = [];
176
+ if (APPENDED.some((extension) => path.endsWith(extension))) {
177
+ candidates.push(path);
177
178
  }
178
179
  for (const [extension, counterparts] of COUNTERPARTS) {
179
180
  if (path.endsWith(extension)) {
180
181
  const stem = path.slice(0, -extension.length);
181
- const found = counterparts.map((one) => stem + one).find(isFile);
182
- if (found !== undefined) {
183
- return found;
184
- }
182
+ candidates.push(...counterparts.map((one) => stem + one));
185
183
  }
186
184
  }
187
- const appended = APPENDED.map((extension) => path + extension).find(isFile);
188
- if (appended !== undefined) {
189
- return appended;
190
- }
191
- return host.kindOf(path) === "directory"
192
- ? APPENDED.map((extension) => joinPath(path, `index${extension}`)).find(isFile)
193
- : undefined;
185
+ candidates.push(...APPENDED.map((extension) => path + extension));
186
+ candidates.push(...APPENDED.map((extension) => joinPath(path, `index${extension}`)));
187
+ return candidates;
188
+ }
189
+ /** The file on disk one relative specifier from a configuration file reaches, or undefined. */
190
+ export function resolveRelativeSpecifier(host, dir, specifier) {
191
+ return specifierCandidates(dir, specifier).find((path) => host.kindOf(path) === "file");
194
192
  }
195
193
  /**
196
194
  * The configuration modules of the directories that hold a manifest which the program
@@ -233,7 +231,7 @@ function outsideModules(host, dirs, files, holdsManifest, parse) {
233
231
  const file = parse(next.path, text);
234
232
  found.push({ file, manifestDir: next.manifestDir });
235
233
  for (const specifier of relativeSpecifiers(file)) {
236
- const reached = reachedBy(host, dirnamePath(next.path), specifier);
234
+ const reached = resolveRelativeSpecifier(host, dirnamePath(next.path), specifier);
237
235
  if (reached !== undefined) {
238
236
  pending.push({ path: reached, manifestDir: next.manifestDir });
239
237
  }
@@ -267,12 +265,13 @@ export function dependenciesNamed(text, declared) {
267
265
  return declared.filter((name) => text === name || text.startsWith(`${name}/`));
268
266
  }
269
267
  /**
270
- * The own files one string names read against a directory: none unless it starts with
271
- * `./` or `../`, else the file it names directly or through the emit mapping, or the
272
- * file it names with an extension the module resolution appends.
268
+ * The own files one string names read against a directory: the file it names directly or
269
+ * through the emit mapping, or the file it names with an extension the module resolution
270
+ * appends. An empty string, one starting with `/` and one holding a glob character names
271
+ * none.
273
272
  */
274
273
  export function filesNamedBy(files, dir, text) {
275
- if (!RELATIVE_PREFIXES.some((prefix) => text.startsWith(prefix)) || text.includes("*")) {
274
+ if (text === "" || text.startsWith("/") || /[*?[{]/u.test(text)) {
276
275
  return [];
277
276
  }
278
277
  const path = resolvePath(dir, text);
@@ -2,9 +2,9 @@
2
2
  // Regenerate it with `UPDATE_GOLDEN=1 npx vitest --run src/conformance.corpus.test.ts`.
3
3
  export const RECORD = {
4
4
  conformance: {
5
- corpusVersion: "2.2.0",
5
+ corpusVersion: "3.0.0",
6
6
  result: "pass",
7
- digest: "sha256:ce7c9087ed7b2baf3de9fa2df436b500706f4f9e94fd9e0fed633054305f06a4",
7
+ digest: "sha256:db52e3164347cab70f45f438b4ac9ce89573be9303503daeb237a3c40361b001",
8
8
  },
9
9
  gaps: [],
10
10
  };
@@ -39,6 +39,7 @@ function propertyName(property) {
39
39
  */
40
40
  export function contextualUses(project, held, chains, literals, flows, cap, properties) {
41
41
  const queries = project.queries;
42
+ const ownPaths = project.ownPaths();
42
43
  const kinds = new Map(held.symbols.map((symbol) => [symbol.id, symbol.kind]));
43
44
  let batches = 0;
44
45
  let lookups = 0;
@@ -78,6 +79,7 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
78
79
  .declarationsOf(property)
79
80
  .filter((id) => MEMBER_KINDS.has(kinds.get(id) ?? "file")),
80
81
  handles: new Set(property.declarations.map((handle) => `${handle.path}#${String(handle.index)}`)),
82
+ own: property.declarations.some((handle) => ownPaths.has(handle.path)),
81
83
  });
82
84
  }
83
85
  tables.set(type.id, table);
@@ -109,6 +111,11 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
109
111
  /** The members a position may read structurally: those of object types, not of classes. */
110
112
  const readable = (member) => member.ids.filter((id) => kinds.get(id) === "type-member");
111
113
  const carries = (part) => [...membersOf(part).values()].some((member) => readable(member).length > 0);
114
+ /** Whether an object type has members and declares none of them in the target. */
115
+ const outside = (part) => {
116
+ const members = [...membersOf(part).values()];
117
+ return members.length > 0 && members.every((member) => !member.own);
118
+ };
112
119
  const elements = new Map();
113
120
  /** The element type of an array type, read once per type. */
114
121
  const elementOf = (type) => {
@@ -168,7 +175,10 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
168
175
  const sources = partsOf(source).filter((one) => !targets.some((part) => part.id === one.id));
169
176
  for (const one of sources) {
170
177
  if (carries(one)) {
171
- into.push({ source: one, targets });
178
+ into.push({ source: one, targets, outside: false });
179
+ }
180
+ else if (outside(one)) {
181
+ into.push({ source: one, targets, outside: true });
172
182
  }
173
183
  const element = elementOf(one);
174
184
  const called = element === undefined ? callOf(one) : undefined;
@@ -198,7 +208,7 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
198
208
  }
199
209
  seen.add(type.id);
200
210
  return partsOf(type).some((part) => {
201
- if (carries(part)) {
211
+ if (carries(part) || outside(part)) {
202
212
  return true;
203
213
  }
204
214
  const element = elementOf(part);
@@ -223,9 +233,20 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
223
233
  const checked = [];
224
234
  compared(type, position, checked, new Set());
225
235
  const ids = new Set();
226
- for (const { source, targets } of checked) {
236
+ for (const { source, targets, outside: held } of checked) {
227
237
  const tables = targets.map(membersOf);
228
- for (const [name, member] of membersOf(source)) {
238
+ const sourceMembers = membersOf(source);
239
+ if (held) {
240
+ for (const table of tables) {
241
+ for (const [name, member] of table) {
242
+ if (sourceMembers.has(name)) {
243
+ member.ids.forEach((id) => ids.add(id));
244
+ }
245
+ }
246
+ }
247
+ continue;
248
+ }
249
+ for (const [name, member] of sourceMembers) {
229
250
  const read = tables.some((table) => {
230
251
  const declared = table.get(name);
231
252
  return (declared !== undefined &&
@@ -385,5 +385,12 @@ export const CONVENTION_ROWS = [
385
385
  `**/{vitest,vite}.*.config.${ANY_MODULE}`,
386
386
  ],
387
387
  moves: [],
388
+ shortNames: [
389
+ {
390
+ property: "test.coverage.provider",
391
+ files: [`{vitest,vite}.config.${ANY_MODULE}`],
392
+ package: "@vitest/coverage-{}",
393
+ },
394
+ ],
388
395
  },
389
396
  ];
@@ -5,7 +5,7 @@
5
5
  * directory a configuration property moves read from the property's literal value.
6
6
  * No configuration file is run.
7
7
  */
8
- import { isAsExpression, isBinaryExpression, isCallExpression, isClassDeclaration, isComputedPropertyName, isExportAssignment, isExportDeclaration, isExpressionStatement, isFunctionDeclaration, isIdentifier, isImportDeclaration, isNamedExports, isNamespaceImport, isNoSubstitutionTemplateLiteral, isNonNullExpression, isNumericLiteral, isObjectBindingPattern, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isShorthandPropertyAssignment, isSpreadAssignment, isStringLiteral, isVariableStatement, SyntaxKind, } from "@typescript/native/unstable/ast";
8
+ import { isArrayLiteralExpression, isAsExpression, isBinaryExpression, isCallExpression, isClassDeclaration, isComputedPropertyName, isExportAssignment, isExportDeclaration, isExpressionStatement, isFunctionDeclaration, isIdentifier, isImportDeclaration, isNamedExports, isNamespaceImport, isNoSubstitutionTemplateLiteral, isNonNullExpression, isNumericLiteral, isObjectBindingPattern, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isShorthandPropertyAssignment, isSpreadAssignment, isStringLiteral, isVariableStatement, SyntaxKind, } from "@typescript/native/unstable/ast";
9
9
  import { lineOf } from "./component-files.js";
10
10
  import { CONVENTION_ROWS, } from "./convention-rows.js";
11
11
  import { globExpression } from "./glob.js";
@@ -288,12 +288,32 @@ function objectOf(node, declared, throughCalls, seen = new Set()) {
288
288
  }
289
289
  return at;
290
290
  }
291
+ /** The text of a string literal or a template literal with no substitution, or undefined. */
292
+ function literalText(node) {
293
+ const at = unwrapped(node);
294
+ return isStringLiteral(at) || isNoSubstitutionTemplateLiteral(at) ? at.text : undefined;
295
+ }
296
+ /**
297
+ * The literal texts one value at a path's end writes: the literal itself, or, where
298
+ * `elements`, each element of an array literal of literals. Undefined for any other value.
299
+ */
300
+ function literalTexts(held, elements) {
301
+ const one = literalText(held);
302
+ if (one !== undefined) {
303
+ return [one];
304
+ }
305
+ if (!elements || !isArrayLiteralExpression(held)) {
306
+ return undefined;
307
+ }
308
+ const texts = held.elements.map(literalText);
309
+ return texts.every((text) => text !== undefined) ? texts : undefined;
310
+ }
291
311
  /**
292
312
  * Reads the values at one path of keys below one object literal into `values`, and
293
313
  * answers the first member that does not write its value out: a spread, a value that is
294
314
  * not a literal at the path's end, or one that is not an object literal before it.
295
315
  */
296
- function readPath(object, keys, values) {
316
+ function readPath(object, keys, values, elements) {
297
317
  const [key, ...rest] = keys;
298
318
  for (const member of object.properties) {
299
319
  if (isSpreadAssignment(member) || computesKey(member)) {
@@ -304,16 +324,17 @@ function readPath(object, keys, values) {
304
324
  }
305
325
  const held = isPropertyAssignment(member) ? unwrapped(member.initializer) : undefined;
306
326
  if (rest.length === 0) {
307
- if (held === undefined || !(isStringLiteral(held) || isNoSubstitutionTemplateLiteral(held))) {
327
+ const texts = held === undefined ? undefined : literalTexts(held, elements);
328
+ if (texts === undefined) {
308
329
  return member;
309
330
  }
310
- values.push(held.text);
331
+ values.push(...texts);
311
332
  }
312
333
  else {
313
334
  if (held === undefined || !isObjectLiteralExpression(held)) {
314
335
  return member;
315
336
  }
316
- const failed = readPath(held, rest, values);
337
+ const failed = readPath(held, rest, values, elements);
317
338
  if (failed !== undefined) {
318
339
  return failed;
319
340
  }
@@ -324,11 +345,12 @@ function readPath(object, keys, values) {
324
345
  /**
325
346
  * Every value one file writes at one property's path inside the object it is read from:
326
347
  * the options argument of every call `call` names, or the file's default export. A string
327
- * literal or a template literal with no substitution is the value. A read object, or an
328
- * object along the path, that is not written out as an object literal, or that spreads
329
- * another object into itself, is not a literal, and neither is any other value at the path.
348
+ * literal or a template literal with no substitution is the value, and so is each element
349
+ * of an array literal of them where `elements`. A read object, or an object along the
350
+ * path, that is not written out as an object literal, or that spreads another object into
351
+ * itself, is not a literal, and neither is any other value at the path.
330
352
  */
331
- export function readProperty(file, property, call) {
353
+ export function readProperty(file, property, call, elements = false) {
332
354
  const declared = declaredValues(file);
333
355
  const objects = call === undefined
334
356
  ? defaultExports(file).map((one) => objectOf(one, declared, true))
@@ -339,7 +361,7 @@ export function readProperty(file, property, call) {
339
361
  continue;
340
362
  }
341
363
  const notLiteral = isObjectLiteralExpression(object)
342
- ? readPath(object, property.split("."), values)
364
+ ? readPath(object, property.split("."), values, elements)
343
365
  : object;
344
366
  if (notLiteral !== undefined) {
345
367
  return { values, notLiteral };
@@ -392,29 +414,18 @@ function configurationFiles(host, dir, glob) {
392
414
  function resolveMove(host, parse, dir, row, move, resolved) {
393
415
  const set = [];
394
416
  for (const reading of move.readings) {
395
- for (const glob of reading.files) {
396
- for (const name of configurationFiles(host, dir, glob)) {
397
- const path = joinPath(dir, name);
398
- let text;
399
- try {
400
- text = host.readFile(path);
401
- }
402
- catch {
403
- continue;
404
- }
405
- const file = parse(path, text);
406
- const read = readProperty(file, reading.property, reading.call);
407
- if (read.notLiteral !== undefined) {
408
- const line = lineOf(text, read.notLiteral.getStart());
409
- return {
410
- failure: `${name}:${String(line)} sets ${reading.property}, which moves a directory ` +
411
- `the convention row ${row.name} reads, to a value that is not a string literal: ` +
412
- `write it as a literal, or name ${row.name} in ts.disabled_conventions and its ` +
413
- `files in ts.entry_files`,
414
- };
415
- }
416
- set.push(...read.values);
417
+ for (const { name, text, file } of readingFiles(host, parse, dir, reading.files)) {
418
+ const read = readProperty(file, reading.property, reading.call);
419
+ if (read.notLiteral !== undefined) {
420
+ const line = lineOf(text, read.notLiteral.getStart());
421
+ return {
422
+ failure: `${name}:${String(line)} sets ${reading.property}, which moves a directory ` +
423
+ `the convention row ${row.name} reads, to a value that is not a string literal: ` +
424
+ `write it as a literal, or name ${row.name} in ts.disabled_conventions and its ` +
425
+ `files in ts.entry_files`,
426
+ };
417
427
  }
428
+ set.push(...read.values);
418
429
  }
419
430
  }
420
431
  const dirs = [];
@@ -427,6 +438,55 @@ function resolveMove(host, parse, dir, row, move, resolved) {
427
438
  }
428
439
  return { dirs: [...new Set(dirs)] };
429
440
  }
441
+ /** The texts each configuration file a reading names holds, parsed, in name order. */
442
+ function readingFiles(host, parse, dir, files) {
443
+ const found = [];
444
+ for (const glob of files) {
445
+ for (const name of configurationFiles(host, dir, glob)) {
446
+ const path = joinPath(dir, name);
447
+ let text;
448
+ try {
449
+ text = host.readFile(path);
450
+ }
451
+ catch {
452
+ continue;
453
+ }
454
+ found.push({ name, text, file: parse(path, text) });
455
+ }
456
+ }
457
+ return found;
458
+ }
459
+ /**
460
+ * The dependencies one manifest declares that an applied row's short names use: the
461
+ * package each literal value stands for, and, for a value that is not a literal, every
462
+ * declared dependency the row's package name stands for with some value.
463
+ */
464
+ function shortNameUses(host, parse, dir, manifest, readings) {
465
+ const declared = new Set(DEPENDENCY_SECTIONS.flatMap((section) => {
466
+ const held = isRecord(manifest) ? manifest[section] : undefined;
467
+ return isRecord(held) ? Object.keys(held) : [];
468
+ }));
469
+ const used = new Set();
470
+ for (const reading of readings) {
471
+ const [before = "", after = ""] = reading.package.split("{}");
472
+ for (const { file } of readingFiles(host, parse, dir, reading.files)) {
473
+ const read = readProperty(file, reading.property, reading.call, true);
474
+ for (const value of read.values) {
475
+ used.add(`${before}${value}${after}`);
476
+ }
477
+ if (read.notLiteral !== undefined) {
478
+ for (const name of declared) {
479
+ if (name.length >= before.length + after.length &&
480
+ name.startsWith(before) &&
481
+ name.endsWith(after)) {
482
+ used.add(name);
483
+ }
484
+ }
485
+ }
486
+ }
487
+ }
488
+ return [...used].filter((name) => declared.has(name)).sort(compare);
489
+ }
430
490
  /** Two strings ordered bytewise. */
431
491
  function compare(a, b) {
432
492
  if (a === b) {
@@ -443,6 +503,7 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
443
503
  const globs = [];
444
504
  const generated = [];
445
505
  const failures = [];
506
+ const uses = new Set();
446
507
  for (const manifest of manifests) {
447
508
  const declared = readJSON(host, joinPath(manifest.dir, "package.json"));
448
509
  const versions = new Map();
@@ -500,6 +561,9 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
500
561
  version,
501
562
  manifest: manifest.path,
502
563
  });
564
+ for (const name of shortNameUses(host, parse, manifest.dir, declared, row.shortNames ?? [])) {
565
+ uses.add(name);
566
+ }
503
567
  const excludes = [
504
568
  ...new Set((row.excludes ?? []).flatMap((one) => expand(one, resolved))),
505
569
  ].map(globExpression);
@@ -520,7 +584,7 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
520
584
  unique.sort((a, b) => compare(a.name, b.name) ||
521
585
  compare(a.manifest, b.manifest) ||
522
586
  compare(JSON.stringify(a), JSON.stringify(b)));
523
- return { applied: unique, globs, generated, failures };
587
+ return { applied: unique, globs, generated, failures, uses: [...uses].sort(compare) };
524
588
  }
525
589
  /**
526
590
  * Every own file of one project below a directory an applied row names as generated, by
@@ -24,6 +24,14 @@ const SECTIONS = [
24
24
  ];
25
25
  /** The owner of a use no declaration of the inventory holds: a configuration's. */
26
26
  const PROJECT_OWNER = "";
27
+ /** What a configuration's own reading needs: packages no deletion of a declaration removes. */
28
+ export function configurationNeeds(packages) {
29
+ return {
30
+ packages: new Set(packages),
31
+ uses: new Map([[PROJECT_OWNER, new Set(packages)]]),
32
+ unanswered: false,
33
+ };
34
+ }
27
35
  function isRecord(value) {
28
36
  return typeof value === "object" && value !== null && !Array.isArray(value);
29
37
  }
@@ -13,9 +13,9 @@ const EVERY_CONVENTION = "every file it loads by its own convention beyond its c
13
13
  const UNREAD = "No rule reads it beyond a configuration file the file-name form or a string enters, so a " +
14
14
  "file only it loads is reported as never imported unless ts.entry_files or roots.patterns " +
15
15
  "names it.";
16
- /** Why a configuration's file names that are not relative literals are declined. */
17
- const NOT_RELATIVE = "A string of a configuration file names a file only where it is a literal starting with ./ " +
18
- "or ../, so a file named otherwise is reported as never imported.";
16
+ /** Why a configuration's file names that are not literals are declined. */
17
+ const NOT_LITERAL = "A string of a configuration file names a file only where it is a literal, so a file a " +
18
+ "computed value names is reported as never imported.";
19
19
  /** The tools whose own conventions are not read. */
20
20
  const UNREAD_TOOLS = [
21
21
  "angular",
@@ -211,11 +211,6 @@ const PARTLY_ENTERED = [
211
211
  reason: "A testMatch that is not a string literal leaves the test-file set unknown, and an " +
212
212
  "unknown set enters nothing.",
213
213
  },
214
- {
215
- tool: "playwright",
216
- convention: "a globalSetup or globalTeardown file named without a leading ./ or ../",
217
- reason: NOT_RELATIVE,
218
- },
219
214
  {
220
215
  tool: "playwright",
221
216
  convention: "the testDir and testMatch of an entry in projects",
@@ -272,20 +267,14 @@ const PARTLY_ENTERED = [
272
267
  },
273
268
  {
274
269
  tool: "vite",
275
- convention: "an entry point its configuration names without a leading ./ or ../, or computes",
276
- reason: NOT_RELATIVE,
277
- },
278
- {
279
- tool: "vite",
280
- convention: "the module scripts an index.html loads",
281
- reason: "No rule reads an HTML file, so a script only index.html loads is reported as never " +
282
- "imported unless ts.entry_files names it.",
270
+ convention: "an entry point its configuration computes",
271
+ reason: NOT_LITERAL,
283
272
  },
284
273
  {
285
274
  tool: "vitest",
286
- convention: "the benchmark and type-test files",
287
- reason: "A test file is one ts.test_files classifies, so a *.bench.* or *.test-d.* file is " +
288
- "entered only where a configured pattern names it.",
275
+ convention: "the benchmark files",
276
+ reason: "A test file is one ts.test_files classifies, so a *.bench.* file is entered only " +
277
+ "where a configured pattern names it.",
289
278
  },
290
279
  {
291
280
  tool: "vitest",
@@ -293,11 +282,6 @@ const PARTLY_ENTERED = [
293
282
  reason: "A string is read against the configuration file's directory, so a setup file named " +
294
283
  "against a root the configuration moves is not entered.",
295
284
  },
296
- {
297
- tool: "vitest",
298
- convention: "a setup file named without a leading ./ or ../",
299
- reason: NOT_RELATIVE,
300
- },
301
285
  {
302
286
  tool: "vitest",
303
287
  convention: "the test files a configuration's include member names",
@@ -306,8 +290,8 @@ const PARTLY_ENTERED = [
306
290
  },
307
291
  {
308
292
  tool: "webpack",
309
- convention: "an entry point its configuration names without a leading ./ or ../, or computes",
310
- reason: NOT_RELATIVE,
293
+ convention: "an entry point its configuration computes",
294
+ reason: NOT_LITERAL,
311
295
  },
312
296
  ];
313
297
  /** Two strings ordered bytewise. */
@@ -14,6 +14,7 @@ import { isArrayLiteralExpression, isAsExpression, isCallExpression, isExportAss
14
14
  import { SymbolFlags } from "@typescript/native/unstable/sync";
15
15
  import { configurationFiles, filesNamedBy, moduleStrings, } from "./configuration-files.js";
16
16
  import { globExpression } from "./glob.js";
17
+ import { htmlEntries } from "./html-entries.js";
17
18
  import { dirnamePath, relativePath, resolvePath } from "./paths.js";
18
19
  import { UNANSWERED } from "./query.js";
19
20
  /** The extensions the test runner and the browser-test runner load a configuration from. */
@@ -406,5 +407,8 @@ export function entryPoints(project, files, testFiles, host, targetRoot, parse)
406
407
  }
407
408
  found.push(...workersOf(project, file, files));
408
409
  }
410
+ for (const { file, source } of htmlEntries(host, targetRoot, files, parse)) {
411
+ found.push({ file, rule: "html-entry", source, exports: true });
412
+ }
409
413
  return found;
410
414
  }
@@ -39,23 +39,29 @@ function publishedIds(union) {
39
39
  }
40
40
  /**
41
41
  * The reachability class of one declaration of the run. A declaration of a library's
42
- * published API, a public member of a published type included, has callers outside the
43
- * target: it is `certain` where the run loaded every consumer the scope declared and
44
- * declared at least one, `probable` where it declared consumers and did not load them
45
- * all, and `possible` where it declared none. Whether the configuration declares the
46
- * consumer set complete decides nothing here. Every other declaration has every reference
47
- * in the loaded program, a private member and a `#private` name among them, and is
48
- * `certain`.
42
+ * published API, a public member of a published type included, is `certain` where the
43
+ * run loaded every consumer the scope declared and declared one, `probable` where one did
44
+ * not load, and `possible` where it declared none, whatever the configuration says of the
45
+ * consumer set. A declaration of test-support code that test code references, outside the
46
+ * test-of-dead-code rule, is `possible`. Every other declaration is `certain`.
49
47
  */
50
48
  function reachabilityClasses(input) {
51
49
  const published = publishedIds(input.swept.matrix.union);
50
+ const usedByTests = new Set(input.swept.sweep.candidates
51
+ .filter((candidate) => candidate.heldByTests === candidate.id)
52
+ .map((candidate) => candidate.id));
52
53
  const { declared } = input.boundary.consumers;
53
54
  const outside = declared.length === 0
54
55
  ? "possible"
55
56
  : everyDeclaredLoaded(input.boundary)
56
57
  ? "certain"
57
58
  : "probable";
58
- return (id) => (published.has(id) ? outside : "certain");
59
+ return (id) => {
60
+ if (usedByTests.has(id)) {
61
+ return "possible";
62
+ }
63
+ return published.has(id) ? outside : "certain";
64
+ };
59
65
  }
60
66
  /**
61
67
  * The component each dead declaration of the run falls in, and none for a live one, with
@@ -117,6 +123,12 @@ export function completed(input, findings, kinds = KINDS) {
117
123
  const componentOf = findingComponents(swept, input.config.reporters.cascade);
118
124
  let minted = swept.sweep.components.length;
119
125
  const dead = new Set(swept.sweep.components.map((component) => component.id));
126
+ const fallingWith = new Map();
127
+ for (const { heldByTests } of swept.sweep.candidates) {
128
+ if (heldByTests !== undefined) {
129
+ fallingWith.set(heldByTests, (fallingWith.get(heldByTests) ?? 0) + 1);
130
+ }
131
+ }
120
132
  const done = findings.map((finding) => {
121
133
  const row = kinds.get(finding.code);
122
134
  if (row === undefined) {
@@ -134,8 +146,12 @@ export function completed(input, findings, kinds = KINDS) {
134
146
  const judged = !part && union.subject[at] === true ? symbol : undefined;
135
147
  const candidate = judged === undefined ? undefined : candidates.get(judged.id);
136
148
  const found = symbol === undefined || part ? undefined : componentOf(symbol.id);
137
- // A test file's declaration is a component member only as a test of dead code.
138
- if (candidate !== undefined && found === undefined && union.test[at] !== true) {
149
+ // A test file's declaration is a component member only as a test of dead code, and
150
+ // test code keeps the test-support code it references out of every component.
151
+ if (candidate !== undefined &&
152
+ found === undefined &&
153
+ union.test[at] !== true &&
154
+ candidate.heldByTests === undefined) {
139
155
  throw new Error(`${candidate.id} is judged dead and falls in no dead component`);
140
156
  }
141
157
  let component = found;
@@ -144,7 +160,7 @@ export function completed(input, findings, kinds = KINDS) {
144
160
  component = {
145
161
  id: componentId(minted),
146
162
  root: true,
147
- symbolCount: 1,
163
+ symbolCount: symbol?.kind === "interface" ? (fallingWith.get(symbol.id) ?? 1) : 1,
148
164
  deletableLines: 0,
149
165
  };
150
166
  }