@cplieger/deadset-ts 5.1.1 → 5.2.0

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 (58) 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/references.js +120 -3
  24. package/dist/src/roots.js +52 -3
  25. package/dist/src/sweep.js +17 -0
  26. package/dist/src/test-support.js +35 -23
  27. package/dist/src/version.js +1 -1
  28. package/jsr.json +1 -1
  29. package/package.json +1 -1
  30. package/src/analysis.ts +12 -5
  31. package/src/config.ts +3 -0
  32. package/src/configuration-files.ts +28 -24
  33. package/src/conformance-record.ts +2 -2
  34. package/src/contextual-uses.ts +31 -4
  35. package/src/convention-rows.ts +15 -0
  36. package/src/conventions.ts +119 -30
  37. package/src/dependencies.ts +9 -0
  38. package/src/entry-point-gaps.ts +11 -28
  39. package/src/entry-points.ts +7 -1
  40. package/src/exempt.ts +1 -4
  41. package/src/file-facts.ts +1 -1
  42. package/src/findings/completion.ts +31 -11
  43. package/src/findings/interfaces.ts +2 -9
  44. package/src/findings/intra-function.ts +17 -3
  45. package/src/findings/unused-declarations.ts +5 -8
  46. package/src/findings/visibility-narrowing.ts +7 -1
  47. package/src/generated-file.ts +80 -12
  48. package/src/graph.ts +5 -4
  49. package/src/html-entries.ts +170 -0
  50. package/src/kinds.ts +2 -2
  51. package/src/manifest.ts +9 -3
  52. package/src/matrix.ts +21 -3
  53. package/src/namespace-objects.ts +216 -0
  54. package/src/references.ts +161 -3
  55. package/src/roots.ts +69 -10
  56. package/src/sweep.ts +19 -0
  57. package/src/test-support.ts +34 -22
  58. package/src/version.ts +1 -1
@@ -1,7 +1,9 @@
1
- import { isArrayLiteralExpression, isAsExpression, isBinaryExpression, isCallExpression, isDecorator, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isExpressionStatement, isExternalModuleReference, isForInStatement, isForOfStatement, isForStatement, isImportDeclaration, isImportEqualsDeclaration, isNamespaceExport, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, isVoidExpression, SyntaxKind, } from "@typescript/native/unstable/ast";
1
+ import { isArrayLiteralExpression, isAsExpression, isBinaryExpression, isCallExpression, isDecorator, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isExpressionStatement, isExternalModuleReference, isForInStatement, isForOfStatement, isForStatement, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isMetaProperty, isNewExpression, isNamespaceExport, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, isVoidExpression, SyntaxKind, } from "@typescript/native/unstable/ast";
2
2
  import { aliasChains } from "./alias-chain.js";
3
3
  import { contextualUses } from "./contextual-uses.js";
4
4
  import { destructuring, propertyReadsOf } from "./destructuring.js";
5
+ import { specifierCandidates } from "./configuration-files.js";
6
+ import { namespaceUses } from "./namespace-objects.js";
5
7
  import { globExpression } from "./glob.js";
6
8
  import { declarationsByName, nodeKey } from "./inventory.js";
7
9
  import { overrideReferences } from "./overrides.js";
@@ -169,6 +171,44 @@ function markStores(node, writes, reads) {
169
171
  }
170
172
  }
171
173
  }
174
+ /** Whether one node is `import.meta.url`. */
175
+ function isImportMetaURL(node) {
176
+ const at = unparenthesized(node);
177
+ return (isPropertyAccessExpression(at) &&
178
+ isMetaProperty(at.expression) &&
179
+ at.expression.keywordToken === SyntaxKind.ImportKeyword &&
180
+ at.name.text === "url");
181
+ }
182
+ /** The module URL one node builds, where it builds one. */
183
+ function moduleURLOf(node) {
184
+ const relative = (argument) => {
185
+ const at = argument === undefined ? undefined : unparenthesized(argument);
186
+ return at !== undefined &&
187
+ (isStringLiteral(at) || isNoSubstitutionTemplateLiteral(at)) &&
188
+ /^\.\.?\//u.test(at.text)
189
+ ? at.text
190
+ : undefined;
191
+ };
192
+ if (isNewExpression(node) && isIdentifier(node.expression) && node.expression.text === "URL") {
193
+ const [reference, base] = node.arguments ?? [];
194
+ const specifier = relative(reference);
195
+ return specifier !== undefined && base !== undefined && isImportMetaURL(base)
196
+ ? { specifier, constructor: node.expression, at: node }
197
+ : undefined;
198
+ }
199
+ if (isCallExpression(node)) {
200
+ const callee = unparenthesized(node.expression);
201
+ const specifier = relative(node.arguments[0]);
202
+ if (specifier !== undefined &&
203
+ isPropertyAccessExpression(callee) &&
204
+ isMetaProperty(callee.expression) &&
205
+ callee.expression.keywordToken === SyntaxKind.ImportKeyword &&
206
+ callee.name.text === "resolve") {
207
+ return { specifier, constructor: undefined, at: node };
208
+ }
209
+ }
210
+ return undefined;
211
+ }
172
212
  /** Whether one node is a string literal or a template literal with no substitution. */
173
213
  function isLiteralText(node) {
174
214
  return node !== undefined && (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node));
@@ -286,7 +326,10 @@ function sitesOf(file, declarations, fileId, unread) {
286
326
  const reads = [];
287
327
  const links = [];
288
328
  const evaluations = [];
329
+ const wholes = [];
330
+ const urls = [];
289
331
  const decorated = [];
332
+ const namespaces = namespaceUses(file);
290
333
  const literals = [];
291
334
  const flows = [];
292
335
  const writes = new Set();
@@ -315,6 +358,13 @@ function sitesOf(file, declarations, fileId, unread) {
315
358
  if (unread.has(node)) {
316
359
  return;
317
360
  }
361
+ const namespace = namespaces.get(node);
362
+ if (namespace?.kind === "whole") {
363
+ wholes.push({ specifier: namespace.specifier, at: node, from: enclosing });
364
+ }
365
+ else if (namespace?.kind === "indexed") {
366
+ found.push({ node: namespace.literal, shorthand: undefined, from: enclosing, use: "read" });
367
+ }
318
368
  if (isName(node)) {
319
369
  if (!declared.has(node.pos)) {
320
370
  const site = { node, shorthand: shorthands.get(node.pos), from: enclosing };
@@ -344,6 +394,10 @@ function sitesOf(file, declarations, fileId, unread) {
344
394
  if (evaluated !== undefined) {
345
395
  evaluations.push({ node: evaluated, from: enclosing });
346
396
  }
397
+ const url = moduleURLOf(node);
398
+ if (url !== undefined) {
399
+ urls.push(url);
400
+ }
347
401
  const held = declarations.get(nodeKey(file, node));
348
402
  const outer = enclosing;
349
403
  if (held !== undefined) {
@@ -358,7 +412,7 @@ function sitesOf(file, declarations, fileId, unread) {
358
412
  enclosing = outer;
359
413
  };
360
414
  file.forEachChild(visit);
361
- return { uses: found, literals, flows, reads, links, evaluations, decorated };
415
+ return { uses: found, literals, flows, reads, links, evaluations, wholes, urls, decorated };
362
416
  }
363
417
  /** The declaration kinds of a property an object literal declares. */
364
418
  const LITERAL_PROPERTY_KINDS = new Set([
@@ -583,6 +637,30 @@ export function references(project, held, targetRoot, options) {
583
637
  }
584
638
  return topLevel.get(fileId) ?? [];
585
639
  };
640
+ const exportsCache = new Map();
641
+ /**
642
+ * Every declaration one module exports, each re-export walked along its chain. A module
643
+ * the checker left unanswered may be any file, so it reads every file's exports.
644
+ */
645
+ const exportsRead = (module) => {
646
+ if (module === UNANSWERED) {
647
+ return [...fileIds].flatMap((id) => declaredIn(id)
648
+ .filter((symbol) => symbol.exported)
649
+ .map((symbol) => ({ id: symbol.id, stepped: false, guessed: true })));
650
+ }
651
+ const known = exportsCache.get(module.id);
652
+ if (known !== undefined) {
653
+ return known;
654
+ }
655
+ const table = project.queries.exportsOfModule(module);
656
+ const read = table === UNANSWERED
657
+ ? chains.declarationsOf(module).flatMap((id) => declaredIn(id)
658
+ .filter((symbol) => symbol.exported)
659
+ .map((symbol) => ({ id: symbol.id, stepped: false, guessed: true })))
660
+ : table.flatMap((exported) => chains.chainOf(exported));
661
+ exportsCache.set(module.id, read);
662
+ return read;
663
+ };
586
664
  /** The identifier of the declaration one node of this program is, where it is one. */
587
665
  const declaredAt = (node) => declarations.get(nodeKey(node.getSourceFile(), node));
588
666
  /**
@@ -746,7 +824,7 @@ export function references(project, held, targetRoot, options) {
746
824
  }
747
825
  }
748
826
  }
749
- for (const { file, fileId, uses, reads, links, evaluations, decorated } of walked) {
827
+ for (const { file, fileId, uses, reads, links, evaluations, wholes, urls, decorated } of walked) {
750
828
  const test = testFile.get(file) === true;
751
829
  // A component file's markup may use any binding its blocks declare at the top level.
752
830
  const markup = isComponentFile(file);
@@ -774,6 +852,8 @@ export function references(project, held, targetRoot, options) {
774
852
  ...uses.filter((site) => site.shorthand === undefined).map((site) => site.node),
775
853
  ...links.map((link) => link.node),
776
854
  ...evaluations.map((evaluation) => evaluation.node),
855
+ ...wholes.map((whole) => whole.specifier),
856
+ ...urls.flatMap((url) => url.constructor ?? []),
777
857
  ...bindings.map((binding) => binding.node),
778
858
  ]),
779
859
  ];
@@ -842,6 +922,43 @@ export function references(project, held, targetRoot, options) {
842
922
  }
843
923
  }
844
924
  }
925
+ for (const url of urls) {
926
+ const constructor = url.constructor === undefined ? undefined : answered.get(url.constructor);
927
+ if (typeof constructor === "object" &&
928
+ constructor.declarations.some((handle) => ownPaths.has(handle.path))) {
929
+ continue;
930
+ }
931
+ const loaded = specifierCandidates(dirnamePath(file.fileName), url.specifier)
932
+ .map((path) => fileIdByName.get(path))
933
+ .find((id) => id !== undefined);
934
+ if (loaded !== undefined) {
935
+ found.push({
936
+ from: fileId,
937
+ to: loaded,
938
+ position: renderPosition(file, root, url.at.getStart()),
939
+ use: "evaluation",
940
+ resolution: "syntax",
941
+ test,
942
+ });
943
+ }
944
+ }
945
+ for (const whole of wholes) {
946
+ const module = answered.get(whole.specifier);
947
+ if (module === undefined) {
948
+ continue;
949
+ }
950
+ const position = renderPosition(file, root, whole.at.getStart());
951
+ for (const target of exportsRead(module)) {
952
+ found.push({
953
+ from: whole.from,
954
+ to: target.id,
955
+ position,
956
+ use: "read",
957
+ resolution: target.guessed ? "by-name" : "batch",
958
+ test,
959
+ });
960
+ }
961
+ }
845
962
  if (markup) {
846
963
  for (const symbol of declaredIn(fileId)) {
847
964
  found.push({
package/dist/src/roots.js CHANGED
@@ -13,6 +13,7 @@ import { conventionEntries } from "./conventions.js";
13
13
  import { entryPoints } from "./entry-points.js";
14
14
  import { globExpression } from "./glob.js";
15
15
  import { nodeKey } from "./inventory.js";
16
+ import { relativePath, resolvePath } from "./paths.js";
16
17
  import { byPosition } from "./position.js";
17
18
  import { UNANSWERED } from "./query.js";
18
19
  import { sourceFilesOf } from "./source-files.js";
@@ -23,6 +24,42 @@ const ENTRY_KINDS = {
23
24
  run: "manifest-binary",
24
25
  script: "script",
25
26
  };
27
+ /** The rules whose rooted files a manifest target is read back to by name. */
28
+ const READ_BACK_RULES = new Set([
29
+ "configuration-file",
30
+ "configuration-string",
31
+ "html-entry",
32
+ ]);
33
+ /** The extensions a manifest target's name drops: the ones a build writes. */
34
+ const OUTPUT_EXTENSION = /\.(?:d\.ts|d\.mts|d\.cts|js|mjs|cjs|jsx)$/u;
35
+ /** The extension a source file's name drops. */
36
+ const SOURCE_EXTENSION = /\.(?:d\.)?[cm]?[jt]sx?$/u;
37
+ /**
38
+ * The rooted files one manifest target reads back to by name: its path below the
39
+ * manifest's directory, past its first directory where it names one, without its output
40
+ * extension, is the name, and a rooted file is one whose path below the same directory,
41
+ * without its source extension, ends with that name at a directory boundary.
42
+ */
43
+ function readBackByName(host, entry, rooted) {
44
+ const dir = resolvePath(host.workingDirectory(), entry.dir);
45
+ const below = relativePath(dir, resolvePath(host.workingDirectory(), entry.path));
46
+ if (below === undefined || below === "") {
47
+ return [];
48
+ }
49
+ const cut = below.indexOf("/");
50
+ const name = (cut === -1 ? below : below.slice(cut + 1)).replace(OUTPUT_EXTENSION, "");
51
+ if (name === "") {
52
+ return [];
53
+ }
54
+ const found = new Map();
55
+ for (const file of rooted) {
56
+ const path = relativePath(dir, file.fileName)?.replace(SOURCE_EXTENSION, "");
57
+ if (path !== undefined && (path === name || path.endsWith(`/${name}`))) {
58
+ found.set(file.fileName, file);
59
+ }
60
+ }
61
+ return [...found.values()];
62
+ }
26
63
  /** Whether a configured string carries either special character. */
27
64
  function isPattern(text) {
28
65
  return text.includes("*") || text.includes("?");
@@ -125,6 +162,7 @@ export function roots(project, held, targetRoot, options) {
125
162
  const enter = (file, kind, source, exports) => {
126
163
  entered.push({ file, kind, source, exports });
127
164
  };
165
+ const points = entryPoints(project, files, options.testFiles, options.host, targetRoot, options.parse);
128
166
  const published = [];
129
167
  const entries = [
130
168
  ...options.manifest.entries.map((entry) => ({
@@ -134,14 +172,25 @@ export function roots(project, held, targetRoot, options) {
134
172
  })),
135
173
  ...(options.otherPackages ?? []).map((entry) => ({ entry, publishes: entry.published })),
136
174
  ];
175
+ const scripted = entries.flatMap(({ entry }) => entry.role === "script" ? files.named(entry.path) : []);
176
+ const readBackTo = [
177
+ ...scripted,
178
+ ...points.filter((point) => READ_BACK_RULES.has(point.rule)).map((point) => point.file),
179
+ ];
180
+ const entryFiles = (entry) => {
181
+ const named = files.named(entry.path);
182
+ return named.length > 0 || entry.role === "script"
183
+ ? named
184
+ : readBackByName(options.host, entry, readBackTo);
185
+ };
137
186
  for (const { entry, publishes } of entries) {
138
187
  if (options.publishedAPI && publishes) {
139
- published.push(...files.named(entry.path));
188
+ published.push(...entryFiles(entry));
140
189
  }
141
190
  }
142
191
  const publishedNames = new Set(published.map((file) => file.fileName));
143
192
  for (const { entry } of entries) {
144
- for (const file of files.named(entry.path)) {
193
+ for (const file of entryFiles(entry)) {
145
194
  const kind = ENTRY_KINDS[entry.role];
146
195
  enter(file, kind, entry.member, !publishedNames.has(file.fileName));
147
196
  }
@@ -159,7 +208,7 @@ export function roots(project, held, targetRoot, options) {
159
208
  : conventionEntries(options.conventions, files)) {
160
209
  enter(file, "convention", row, true);
161
210
  }
162
- for (const point of entryPoints(project, files, options.testFiles, options.host, targetRoot, options.parse)) {
211
+ for (const point of points) {
163
212
  enter(point.file, point.rule, point.source, point.exports);
164
213
  }
165
214
  for (const file of files.byPath.values()) {
package/dist/src/sweep.js CHANGED
@@ -70,6 +70,23 @@ export function sweep(graph, input) {
70
70
  });
71
71
  return { liveUnder: live, candidates };
72
72
  }
73
+ /**
74
+ * Per declaration of the graph, whether the run that counts test references reaches it.
75
+ * That run's roots are the graph's own and each test file, being the file's top level
76
+ * and every declaration it exports, which a test runner may call; `held` seeds it as a
77
+ * mark or an exemption seeds a sweep.
78
+ */
79
+ export function reachedWithTheTests(graph, held) {
80
+ const tests = graph.symbols.flatMap((symbol, at) => {
81
+ const top = symbol.kind === "file" || graph.symbols[graph.parent[at] ?? OUTSIDE]?.kind === "file";
82
+ return graph.test[at] === true && top && (symbol.kind === "file" || symbol.exported)
83
+ ? [{ at, kind: "test-runner" }]
84
+ : [];
85
+ });
86
+ return reachable({ ...graph, rooted: [...graph.rooted, ...tests] }, flagged(graph, held), {
87
+ production: false,
88
+ });
89
+ }
73
90
  /**
74
91
  * The references a loaded consumer makes that the mode counts. A production mode counts
75
92
  * none a consumer's test file made, as it counts none of the target's.
@@ -1,47 +1,59 @@
1
1
  /**
2
- * Test-support code: a file that is not a test file by name and that only test code
3
- * imports. At least one test file reaches it, directly or through other such files, and
4
- * nothing else does, so it is judged with the tests: what it references is referenced by
5
- * test code, and a declaration of it is live while test code references it.
2
+ * Test-support code: the files only tests reach. A test reaches each one through a chain
3
+ * of references, and every reference to a declaration of one, an import for its effects
4
+ * included, is written in a test file or in another such file, so it is judged with the
5
+ * tests: what it references is referenced by test code.
6
6
  */
7
7
  /** The root kind that never takes a file out of test-support code: the skip only withholds. */
8
8
  const SKIP_ROOT = "type-error";
9
9
  /**
10
- * The files of one project that are test-support code, below the target root. A file
11
- * holding a root is not one, because a root has a caller outside the import graph: a
12
- * runtime, a tool, a configuration, or a consumer of a library's published API.
13
- * `testFiles` are the files the test-file rules classified.
10
+ * The files of one project that are test-support code, below the target root: the
11
+ * largest set of files a test file reaches, none of them a test file or holding a root,
12
+ * whose every user outside the set is a test file. A root has a caller outside the
13
+ * reference graph: a runtime, a tool, a configuration, or a consumer of a library's
14
+ * published API. `testFiles` are the files the test-file rules classified.
14
15
  */
15
16
  export function testSupportFiles(symbols, references, roots, testFiles) {
16
17
  const fileOf = new Map(symbols.map((symbol) => [symbol.id, symbol.position.path]));
17
18
  const users = new Map();
19
+ const uses = new Map();
20
+ const add = (map, key, value) => {
21
+ const held = map.get(key);
22
+ if (held === undefined) {
23
+ map.set(key, new Set([value]));
24
+ }
25
+ else {
26
+ held.add(value);
27
+ }
28
+ };
18
29
  for (const reference of references) {
19
30
  const used = fileOf.get(reference.to);
20
31
  const from = reference.position.path;
21
32
  if (reference.consumer !== undefined || used === undefined || used === from) {
22
33
  continue;
23
34
  }
24
- const held = users.get(used);
25
- if (held === undefined) {
26
- users.set(used, new Set([from]));
27
- }
28
- else {
29
- held.add(from);
30
- }
35
+ add(users, used, from);
36
+ add(uses, from, used);
31
37
  }
32
38
  const tests = new Set(testFiles);
33
39
  const rooted = new Set(roots.filter((root) => root.kind !== SKIP_ROOT).flatMap((root) => fileOf.get(root.id) ?? []));
34
- const candidates = [...users.keys()].filter((path) => !tests.has(path) && !rooted.has(path));
35
- const support = new Set();
40
+ const reached = new Set();
41
+ const queue = [...tests];
42
+ for (let path = queue.pop(); path !== undefined; path = queue.pop()) {
43
+ for (const next of uses.get(path) ?? []) {
44
+ if (!reached.has(next) && !tests.has(next)) {
45
+ reached.add(next);
46
+ queue.push(next);
47
+ }
48
+ }
49
+ }
50
+ const support = new Set([...reached].filter((path) => !rooted.has(path)));
36
51
  for (let changed = true; changed;) {
37
52
  changed = false;
38
- for (const path of candidates) {
39
- if (support.has(path)) {
40
- continue;
41
- }
53
+ for (const path of support) {
42
54
  const from = users.get(path) ?? new Set();
43
- if ([...from].every((one) => tests.has(one) || support.has(one))) {
44
- support.add(path);
55
+ if (![...from].every((one) => tests.has(one) || support.has(one))) {
56
+ support.delete(path);
45
57
  changed = true;
46
58
  }
47
59
  }
@@ -4,4 +4,4 @@
4
4
  * independently of the analyzer's own version, which is the one the host answers
5
5
  * from the package the analyzer was installed from.
6
6
  */
7
- export const CONTRACT_VERSION = "5.2.0";
7
+ export const CONTRACT_VERSION = "5.3.0";
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "5.1.1",
3
+ "version": "5.2.0",
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": "5.1.1",
3
+ "version": "5.2.0",
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
@@ -22,7 +22,7 @@ import {
22
22
  declarationsNamed,
23
23
  } from "./configured-declarations.ts";
24
24
  import { decorator } from "./decorator.ts";
25
- import { dependenciesOf, projectNeeds } from "./dependencies.ts";
25
+ import { configurationNeeds, dependenciesOf, projectNeeds } from "./dependencies.ts";
26
26
  import { deprecatedDeclarations, type Deprecation } from "./deprecation.ts";
27
27
  import {
28
28
  discoverProjects,
@@ -43,7 +43,7 @@ import {
43
43
  } from "./conventions.ts";
44
44
  import { readEdgeSides } from "./edges.ts";
45
45
  import { enumGroup } from "./enum-group.ts";
46
- import { generatedFile } from "./generated-file.ts";
46
+ import { generatedFile, generatedFilesOf } from "./generated-file.ts";
47
47
  import { sourceFilesOf } from "./source-files.ts";
48
48
  import {
49
49
  evidenceRecords,
@@ -191,7 +191,7 @@ interface ProjectRead {
191
191
  readonly typeErrorSkips: readonly TypeErrorSkip[];
192
192
  /** The lines each of those skips withholds every finding on. */
193
193
  readonly skipped: readonly SkippedUnit[];
194
- /** The project's generated files, by path below the target root, each with the row naming it. */
194
+ /** The project's generated files, by path below the target root, each with the detail its retentions name. */
195
195
  readonly generated: ReadonlyMap<string, string>;
196
196
  }
197
197
 
@@ -222,6 +222,8 @@ interface ReadProjects<Answer> {
222
222
  readonly componentsUnread: readonly ComponentsUnread[];
223
223
  /** The convention rows the run applied. */
224
224
  readonly conventionsApplied: readonly AppliedConvention[];
225
+ /** The dependencies the applied rows' short names use. */
226
+ readonly conventionUses: readonly string[];
225
227
  /** The test files below the target root no project the run reads holds, parsed. */
226
228
  readonly outsideTests: readonly OutsideTest[];
227
229
  }
@@ -435,6 +437,7 @@ function readProjects<Answer>(
435
437
  ...kept.map((id) => ({ id, kind: "type-error" as const, source: "" })),
436
438
  ],
437
439
  };
440
+ const own = sourceFilesOf(project, targetRoot);
438
441
  return {
439
442
  configuration,
440
443
  configFile: project.configFile,
@@ -446,7 +449,7 @@ function readProjects<Answer>(
446
449
  .ownSourceFiles()
447
450
  .filter((file) => referenceOnly(file.fileName))
448
451
  .flatMap((file) => relativePath(absoluteRoot, file.fileName) ?? []),
449
- generated: generatedFiles(conventions, sourceFilesOf(project, targetRoot)),
452
+ generated: generatedFilesOf(own, generatedFiles(conventions, own)),
450
453
  };
451
454
  };
452
455
  for (const file of project.ownSourceFiles()) {
@@ -504,6 +507,7 @@ function readProjects<Answer>(
504
507
  unanswered: session.unanswered,
505
508
  componentsUnread: workspace?.componentsUnread ?? [],
506
509
  conventionsApplied: conventions.applied,
510
+ conventionUses: conventions.uses,
507
511
  outsideTests,
508
512
  };
509
513
  }
@@ -688,6 +692,8 @@ interface ReadRun<Extra> {
688
692
  readonly componentsUnread: readonly ComponentsUnread[];
689
693
  /** The convention rows the run applied. */
690
694
  readonly conventionsApplied: readonly AppliedConvention[];
695
+ /** The dependencies the applied rows' short names use. */
696
+ readonly conventionUses: readonly string[];
691
697
  }
692
698
 
693
699
  /**
@@ -839,6 +845,7 @@ function readRun<Extra>(
839
845
  unanswered: read.unanswered,
840
846
  componentsUnread: read.componentsUnread,
841
847
  conventionsApplied: read.conventionsApplied,
848
+ conventionUses: read.conventionUses,
842
849
  };
843
850
  }
844
851
 
@@ -1093,7 +1100,7 @@ function emitterInputOver(
1093
1100
  dependencies: dependenciesOf(
1094
1101
  host,
1095
1102
  targetRoot,
1096
- projects.map((one) => one.extra.needs),
1103
+ [...projects.map((one) => one.extra.needs), configurationNeeds(read.conventionUses)],
1097
1104
  swept.sweep.candidates.map((candidate) => candidate.id),
1098
1105
  ),
1099
1106
  implementations: mergeImplementations(projects.map((one) => one.extra.implementations)),
package/src/config.ts CHANGED
@@ -232,9 +232,12 @@ export function defaultConfig(): Config {
232
232
  ts: {
233
233
  testFiles: [
234
234
  "**/*.test.{ts,tsx,mts,cts}",
235
+ "**/*.test-d.{ts,tsx,mts,cts}",
236
+ "**/*.spec-d.{ts,tsx,mts,cts}",
235
237
  "**/*.spec.*",
236
238
  "**/__tests__/**",
237
239
  "**/__mocks__/**",
240
+ "**/test-d/**",
238
241
  ],
239
242
  entryFiles: [],
240
243
  componentExtensions: [".vue", ".svelte", ".astro"],
@@ -237,33 +237,36 @@ function relativeSpecifiers(file: SourceFile): readonly string[] {
237
237
  }
238
238
 
239
239
  /**
240
- * The module one relative specifier from a configuration file reaches: the path it names,
241
- * that path with its JavaScript extension replaced by the TypeScript counterparts, that
242
- * path with an extension appended, and a directory's `index` with one appended.
240
+ * The paths one relative specifier from a configuration file may reach, in the order they
241
+ * are tried: the path it names where that ends with a module extension, that path with its
242
+ * JavaScript extension replaced by the TypeScript counterparts, that path with an
243
+ * extension appended, and a directory's `index` with one appended. A data file such as
244
+ * `./package.json` is no module, so it is no candidate.
243
245
  */
244
- function reachedBy(host: Host, dir: string, specifier: string): string | undefined {
246
+ export function specifierCandidates(dir: string, specifier: string): readonly string[] {
245
247
  const path = resolvePath(dir, specifier);
246
- const isFile = (candidate: string): boolean => host.kindOf(candidate) === "file";
247
- // A data file such as `./package.json` is no module, so its strings use no dependency.
248
- if (isFile(path) && APPENDED.some((extension) => path.endsWith(extension))) {
249
- return path;
248
+ const candidates: string[] = [];
249
+ if (APPENDED.some((extension) => path.endsWith(extension))) {
250
+ candidates.push(path);
250
251
  }
251
252
  for (const [extension, counterparts] of COUNTERPARTS) {
252
253
  if (path.endsWith(extension)) {
253
254
  const stem = path.slice(0, -extension.length);
254
- const found = counterparts.map((one) => stem + one).find(isFile);
255
- if (found !== undefined) {
256
- return found;
257
- }
255
+ candidates.push(...counterparts.map((one) => stem + one));
258
256
  }
259
257
  }
260
- const appended = APPENDED.map((extension) => path + extension).find(isFile);
261
- if (appended !== undefined) {
262
- return appended;
263
- }
264
- return host.kindOf(path) === "directory"
265
- ? APPENDED.map((extension) => joinPath(path, `index${extension}`)).find(isFile)
266
- : undefined;
258
+ candidates.push(...APPENDED.map((extension) => path + extension));
259
+ candidates.push(...APPENDED.map((extension) => joinPath(path, `index${extension}`)));
260
+ return candidates;
261
+ }
262
+
263
+ /** The file on disk one relative specifier from a configuration file reaches, or undefined. */
264
+ export function resolveRelativeSpecifier(
265
+ host: Host,
266
+ dir: string,
267
+ specifier: string,
268
+ ): string | undefined {
269
+ return specifierCandidates(dir, specifier).find((path) => host.kindOf(path) === "file");
267
270
  }
268
271
 
269
272
  /**
@@ -311,7 +314,7 @@ function outsideModules(
311
314
  const file = parse(next.path, text);
312
315
  found.push({ file, manifestDir: next.manifestDir });
313
316
  for (const specifier of relativeSpecifiers(file)) {
314
- const reached = reachedBy(host, dirnamePath(next.path), specifier);
317
+ const reached = resolveRelativeSpecifier(host, dirnamePath(next.path), specifier);
315
318
  if (reached !== undefined) {
316
319
  pending.push({ path: reached, manifestDir: next.manifestDir });
317
320
  }
@@ -347,12 +350,13 @@ export function dependenciesNamed(text: string, declared: readonly string[]): re
347
350
  }
348
351
 
349
352
  /**
350
- * The own files one string names read against a directory: none unless it starts with
351
- * `./` or `../`, else the file it names directly or through the emit mapping, or the
352
- * file it names with an extension the module resolution appends.
353
+ * The own files one string names read against a directory: the file it names directly or
354
+ * through the emit mapping, or the file it names with an extension the module resolution
355
+ * appends. An empty string, one starting with `/` and one holding a glob character names
356
+ * none.
353
357
  */
354
358
  export function filesNamedBy(files: SourceFiles, dir: string, text: string): readonly SourceFile[] {
355
- if (!RELATIVE_PREFIXES.some((prefix) => text.startsWith(prefix)) || text.includes("*")) {
359
+ if (text === "" || text.startsWith("/") || /[*?[{]/u.test(text)) {
356
360
  return [];
357
361
  }
358
362
  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
  } as const;