@cplieger/deadset-ts 6.1.0 → 7.0.0-dev.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 (47) hide show
  1. package/dist/src/analysis.js +39 -16
  2. package/dist/src/conformance-record.js +2 -2
  3. package/dist/src/contextual-uses.js +27 -3
  4. package/dist/src/convention-rows.js +42 -0
  5. package/dist/src/conventions.js +48 -0
  6. package/dist/src/dependencies.js +109 -38
  7. package/dist/src/findings/completion.js +6 -7
  8. package/dist/src/findings/dependencies-and-module-machinery.js +6 -26
  9. package/dist/src/html-entries.js +29 -14
  10. package/dist/src/indexed-access.js +371 -81
  11. package/dist/src/intra-function-parts.js +57 -11
  12. package/dist/src/kinds.js +6 -6
  13. package/dist/src/references.js +60 -21
  14. package/dist/src/roots.js +96 -6
  15. package/dist/src/session.js +6 -1
  16. package/dist/src/source-files.js +1 -0
  17. package/dist/src/sweep.js +29 -17
  18. package/dist/src/symbol-keys.js +114 -0
  19. package/dist/src/test-joins.js +101 -0
  20. package/dist/src/version.js +1 -1
  21. package/dist/src/workflow-steps.js +21 -15
  22. package/dist/src/workspace-programs.js +1 -1
  23. package/jsr.json +1 -1
  24. package/package.json +1 -1
  25. package/src/analysis.ts +45 -17
  26. package/src/conformance-record.ts +2 -2
  27. package/src/contextual-uses.ts +36 -4
  28. package/src/convention-rows.ts +57 -0
  29. package/src/conventions.ts +59 -0
  30. package/src/dependencies.ts +134 -46
  31. package/src/finding.ts +2 -0
  32. package/src/findings/completion.ts +7 -7
  33. package/src/findings/dependencies-and-module-machinery.ts +9 -28
  34. package/src/html-entries.ts +33 -15
  35. package/src/indexed-access.ts +468 -102
  36. package/src/intra-function-parts.ts +84 -12
  37. package/src/kinds.ts +7 -6
  38. package/src/references.ts +86 -27
  39. package/src/roots.ts +129 -6
  40. package/src/session.ts +7 -1
  41. package/src/source-files.ts +3 -0
  42. package/src/sweep.ts +29 -17
  43. package/src/symbol-keys.ts +172 -0
  44. package/src/test-joins.ts +120 -0
  45. package/src/version.ts +1 -1
  46. package/src/workflow-steps.ts +23 -16
  47. package/src/workspace-programs.ts +1 -1
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The members a unique symbol keys, used through element accesses. `value[key]`, where
3
+ * `key` is a constant of a `unique symbol` type, reads the member of the receiver's type
4
+ * whose computed name is that same constant, and a store through the access writes it.
5
+ * No name node resolves to such a member: the key's name resolves to the constant.
6
+ */
7
+ import { isComputedPropertyName, isIdentifier, isNoSubstitutionTemplateLiteral, isNumericLiteral, isParenthesizedExpression, isPropertyAccessExpression, isStringLiteral, } from "@typescript/native/unstable/ast";
8
+ import { isAnswered } from "./query.js";
9
+ /** The identifier an element access's argument names its key by, where it is a name. */
10
+ export function keyNameOf(argument) {
11
+ let at = argument;
12
+ while (isParenthesizedExpression(at)) {
13
+ at = at.expression;
14
+ }
15
+ if (isIdentifier(at)) {
16
+ return at;
17
+ }
18
+ return isPropertyAccessExpression(at) && isIdentifier(at.name) ? at.name : undefined;
19
+ }
20
+ /**
21
+ * The last identifier of each computed member name the inventory holds, `mark` for
22
+ * `[mark]` and `iterator` for `[Symbol.iterator]`: an access whose key is spelled by no
23
+ * such identifier selects no member a unique symbol keys, and is not asked about.
24
+ */
25
+ export function computedKeyNames(symbols) {
26
+ const names = new Set();
27
+ for (const symbol of symbols) {
28
+ const key = /\[([^\]]+)\]$/u.exec(symbol.name)?.[1];
29
+ const last = key?.split(".").pop()?.trim();
30
+ if (last !== undefined && /^[\p{ID_Start}$_][\p{ID_Continue}$]*$/u.test(last)) {
31
+ names.add(last);
32
+ }
33
+ }
34
+ return names;
35
+ }
36
+ /** Whether a computed name's expression is literal text, which names a member by its text. */
37
+ function isLiteralKey(expression) {
38
+ return (isStringLiteral(expression) ||
39
+ isNumericLiteral(expression) ||
40
+ isNoSubstitutionTemplateLiteral(expression));
41
+ }
42
+ /**
43
+ * The members each keyed access selects. The receivers' types are asked in one batch,
44
+ * each object type's properties once, and the key of every property declared under a
45
+ * computed name in one batch more; a property is selected where its key and the
46
+ * access's key are one symbol.
47
+ */
48
+ export function keyedUses(project, chains, resolved, properties, cap) {
49
+ if (resolved.length === 0) {
50
+ return [];
51
+ }
52
+ const queries = project.queries;
53
+ const ownPaths = project.ownPaths();
54
+ const types = queries.typesAt(resolved.map((one) => one.access.access.expression), cap);
55
+ const parts = new Map();
56
+ const partsOf = (type) => {
57
+ const known = parts.get(type.id);
58
+ if (known !== undefined) {
59
+ return known;
60
+ }
61
+ let found = [];
62
+ if (type.isUnionType() || type.isIntersectionType()) {
63
+ const constituents = queries.constituents(type);
64
+ found = isAnswered(constituents) ? constituents.flatMap(partsOf) : [];
65
+ }
66
+ else if (type.isObjectType()) {
67
+ found = [type];
68
+ }
69
+ parts.set(type.id, found);
70
+ return found;
71
+ };
72
+ /** Each receiver's properties declared in the target under a computed name, with that name's expression. */
73
+ const candidates = new Map();
74
+ const keyNodes = [];
75
+ const candidatesOf = (type) => {
76
+ const known = candidates.get(type.id);
77
+ if (known !== undefined) {
78
+ return known;
79
+ }
80
+ const found = [];
81
+ const read = properties.of(type);
82
+ for (const property of isAnswered(read.properties) ? read.properties : []) {
83
+ const handle = property.declarations.find((one) => ownPaths.has(one.path));
84
+ const node = handle === undefined ? undefined : project.declarationAt(handle)?.node;
85
+ const name = node?.name;
86
+ if (name !== undefined && isComputedPropertyName(name) && !isLiteralKey(name.expression)) {
87
+ found.push({ property, key: name.expression });
88
+ keyNodes.push(name.expression);
89
+ }
90
+ }
91
+ candidates.set(type.id, found);
92
+ return found;
93
+ };
94
+ const perAccess = resolved.map((_one, index) => {
95
+ const type = types[index];
96
+ return type === undefined || !isAnswered(type) ? [] : partsOf(type).flatMap(candidatesOf);
97
+ });
98
+ const keyOf = new Map();
99
+ const asked = [...new Set(keyNodes)];
100
+ (asked.length === 0
101
+ ? []
102
+ : project.symbolsAt(asked.map((node) => project.handle(node)), cap)).forEach((symbol, index) => {
103
+ const node = asked[index];
104
+ if (node !== undefined && symbol !== undefined && isAnswered(symbol)) {
105
+ keyOf.set(node, symbol.id);
106
+ }
107
+ });
108
+ return resolved.flatMap((one, index) => {
109
+ const ids = (perAccess[index] ?? [])
110
+ .filter((candidate) => keyOf.get(candidate.key) === one.symbol.id)
111
+ .flatMap((candidate) => chains.declarationsOf(candidate.property));
112
+ return ids.length === 0 ? [] : [{ access: one.access, ids: [...new Set(ids)] }];
113
+ });
114
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * The test files a derived matrix holds although no configuration's file list names them.
3
+ * The test runner compiles such a file with the options of the nearest compiler
4
+ * configuration at or above it, so the file joins that configuration's program as a file
5
+ * its list names, unless a `package.json` below that configuration's directory makes the
6
+ * file another package's, which its own test runner compiles.
7
+ */
8
+ import { sourcePaths } from "./file-facts.js";
9
+ import { dirnamePath, joinPath, relativePath } from "./paths.js";
10
+ import { writtenPath } from "./workspace-programs.js";
11
+ const PREFERRED = "/tsconfig.json";
12
+ /** The configurations of one directory in the order a file joins them: `tsconfig.json` first. */
13
+ function byPreference(a, b) {
14
+ const first = a.endsWith(PREFERRED) ? 0 : 1;
15
+ const second = b.endsWith(PREFERRED) ? 0 : 1;
16
+ return first - second || (a < b ? -1 : a > b ? 1 : 0);
17
+ }
18
+ /**
19
+ * Per configuration of `configFiles`, the absolute paths of the test files that join it:
20
+ * each file below `root` a pattern of `patterns` names, that no configuration's file list
21
+ * holds, joins the configuration nearest at or above its directory when no `package.json`
22
+ * sits between them. A configuration the compiler cannot read lists nothing and is joined
23
+ * by nothing.
24
+ */
25
+ export function joinedTestFiles(engine, host, root, configFiles, patterns) {
26
+ const listed = new Set();
27
+ const byDir = new Map();
28
+ for (const configFile of configFiles) {
29
+ let fileNames;
30
+ try {
31
+ fileNames = engine.parseConfigFile(configFile).fileNames;
32
+ }
33
+ catch {
34
+ continue;
35
+ }
36
+ fileNames.forEach((name) => listed.add(name));
37
+ const dir = dirnamePath(configFile);
38
+ byDir.set(dir, [...(byDir.get(dir) ?? []), configFile].sort(byPreference));
39
+ }
40
+ const joins = new Map();
41
+ for (const path of sourcePaths(host, root)) {
42
+ const file = joinPath(root, path);
43
+ if (listed.has(file) || !patterns.some((pattern) => pattern.test(path))) {
44
+ continue;
45
+ }
46
+ for (let dir = dirnamePath(file);; dir = dirnamePath(dir)) {
47
+ const [nearest] = byDir.get(dir) ?? [];
48
+ if (nearest !== undefined) {
49
+ joins.set(nearest, [...(joins.get(nearest) ?? []), file]);
50
+ break;
51
+ }
52
+ if (host.kindOf(joinPath(dir, "package.json")) === "file" ||
53
+ dir === root ||
54
+ relativePath(root, dir) === undefined) {
55
+ break;
56
+ }
57
+ }
58
+ }
59
+ return joins;
60
+ }
61
+ /**
62
+ * How the snapshot reads each configuration a test file joins: the program `programs`
63
+ * describes with the files added to its root files, or a configuration only the snapshot
64
+ * holds that extends the one `programs` writes, or the configuration itself, and lists
65
+ * every file the configuration lists and the joined ones.
66
+ */
67
+ export function withJoinedTests(engine, host, programs, joins) {
68
+ const found = new Map(programs);
69
+ for (const [configFile, files] of joins) {
70
+ const program = programs.get(configFile);
71
+ if (program?.kind === "program") {
72
+ found.set(configFile, {
73
+ kind: "program",
74
+ params: {
75
+ ...program.params,
76
+ rootFiles: [...(program.params.rootFiles ?? []), ...files],
77
+ },
78
+ });
79
+ continue;
80
+ }
81
+ let listed;
82
+ try {
83
+ listed =
84
+ program?.kind === "configuration"
85
+ ? engine.parseConfigJson(JSON.parse(program.text), program.file).fileNames
86
+ : engine.parseConfigFile(configFile).fileNames;
87
+ }
88
+ catch {
89
+ continue;
90
+ }
91
+ const value = program?.kind === "configuration"
92
+ ? { ...JSON.parse(program.text), files: [...listed, ...files] }
93
+ : { extends: configFile, files: [...listed, ...files] };
94
+ found.set(configFile, {
95
+ kind: "configuration",
96
+ file: program?.kind === "configuration" ? program.file : writtenPath(host, configFile),
97
+ text: `${JSON.stringify({ ...value, include: [] }, null, 2)}\n`,
98
+ });
99
+ }
100
+ return found;
101
+ }
@@ -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 = "6.1.0";
7
+ export const CONTRACT_VERSION = "7.0.0";
@@ -50,12 +50,8 @@ export function runCommands(text) {
50
50
  }
51
51
  return found;
52
52
  }
53
- /**
54
- * The own files the `run:` steps of every workflow file in the target root's
55
- * `.github/workflows` name: a token equal to a file's path below the target root, with or
56
- * without a leading `./`, whatever directory the step runs in.
57
- */
58
- export function workflowEntries(host, targetRoot, files) {
53
+ /** Every token the `run:` steps of the workflow files in the target root's `.github/workflows` hold. */
54
+ export function workflowTokens(host, targetRoot) {
59
55
  const dir = joinPath(resolvePath(host.workingDirectory(), targetRoot), WORKFLOWS);
60
56
  const found = [];
61
57
  for (const path of workflowFiles(host, dir)) {
@@ -67,15 +63,25 @@ export function workflowEntries(host, targetRoot, files) {
67
63
  continue;
68
64
  }
69
65
  for (const command of runCommands(text)) {
70
- for (const token of scriptTokens(command)) {
71
- if (COMPUTED.test(token)) {
72
- continue;
73
- }
74
- const file = files.byPath.get(token.startsWith("./") ? token.slice(2) : token);
75
- if (file !== undefined) {
76
- found.push({ file, source: token });
77
- }
78
- }
66
+ found.push(...scriptTokens(command));
67
+ }
68
+ }
69
+ return found;
70
+ }
71
+ /**
72
+ * The own files the `run:` steps of every workflow file in the target root's
73
+ * `.github/workflows` name: a token equal to a file's path below the target root, with or
74
+ * without a leading `./`, whatever directory the step runs in.
75
+ */
76
+ export function workflowEntries(host, targetRoot, files) {
77
+ const found = [];
78
+ for (const token of workflowTokens(host, targetRoot)) {
79
+ if (COMPUTED.test(token)) {
80
+ continue;
81
+ }
82
+ const file = files.byPath.get(token.startsWith("./") ? token.slice(2) : token);
83
+ if (file !== undefined) {
84
+ found.push({ file, source: token });
79
85
  }
80
86
  }
81
87
  return found;
@@ -48,7 +48,7 @@ function unstatable(reading) {
48
48
  return undefined;
49
49
  }
50
50
  /** The path beside `configFile` its written configuration takes, which no file has. */
51
- function writtenPath(host, configFile) {
51
+ export function writtenPath(host, configFile) {
52
52
  const stem = configFile.replace(/\.json$/u, "");
53
53
  for (let counter = 0;; counter += 1) {
54
54
  const path = `${stem}.deadset-ts${counter === 0 ? "" : `-${String(counter)}`}.json`;
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "6.1.0",
3
+ "version": "7.0.0-dev.1",
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": "6.1.0",
3
+ "version": "7.0.0-dev.1",
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
@@ -84,7 +84,7 @@ import {
84
84
  type Matrix,
85
85
  type SweepResult,
86
86
  } from "./matrix.ts";
87
- import { joinPath, normalizePath, relativePath, resolvePath } from "./paths.ts";
87
+ import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "./paths.ts";
88
88
  import type { UnansweredQuestion } from "./query.ts";
89
89
  import { byPosition, isComponentFile, type Position } from "./position.ts";
90
90
  import type { TypeErrorSkip, UnansweredCount } from "./report.ts";
@@ -102,6 +102,7 @@ import { roots, unmatchedEverywhere, type RootKind, type Roots } from "./roots.t
102
102
  import type { Scope } from "./scope.ts";
103
103
  import { serializationContract } from "./serialization-contract.ts";
104
104
  import { outsideTestReferences, testFilesAt, type OutsideTest } from "./outside-tests.ts";
105
+ import { joinedTestFiles, withJoinedTests } from "./test-joins.ts";
105
106
  import { supportReferences, testSupportFiles } from "./test-support.ts";
106
107
  import { diagnosticsOf, ownedAs, runSession, type Engine, type ProjectView } from "./session.ts";
107
108
  import { accessorsOf, storesOf } from "./stores.ts";
@@ -308,6 +309,9 @@ function readProjects<Answer>(
308
309
  testFiles: config.ts.testFiles,
309
310
  parse: (fileName: string, text: string) => engine.parseSourceFile(fileName, text),
310
311
  publishedAPI: config.targetKind === "library",
312
+ publishedMembers: new Set(
313
+ (workspace?.entries ?? []).filter((entry) => entry.published).map((entry) => entry.dir),
314
+ ),
311
315
  };
312
316
  const notBuilt: NotBuilt[] = [...discovered.notBuilt];
313
317
  const dropped = (configFile: string, error: string): void => {
@@ -332,6 +336,10 @@ function readProjects<Answer>(
332
336
  unbuildable.push(...met);
333
337
  }
334
338
  };
339
+ const testPatterns = config.ts.testFiles.map(globExpression);
340
+ const joins = discovered.derived
341
+ ? joinedTestFiles(engine, host, absoluteRoot, discovered.configFiles, testPatterns)
342
+ : new Map<string, readonly string[]>();
335
343
  const session = runSession(
336
344
  engine,
337
345
  [...discovered.configFiles, ...consumerOf.keys()],
@@ -460,14 +468,15 @@ function readProjects<Answer>(
460
468
  return stage(project, { configuration, read });
461
469
  },
462
470
  discovered.derived ? "record" : "refuse",
463
- workspace?.programs,
471
+ joins.size === 0
472
+ ? workspace?.programs
473
+ : withJoinedTests(engine, host, workspace?.programs ?? new Map(), joins),
464
474
  () => {
465
- const patterns = config.ts.testFiles.map(globExpression);
466
475
  outsideTests = testFilesAt(
467
476
  host,
468
477
  targetRoot,
469
478
  sourcePaths(host, targetRoot).filter(
470
- (path) => !heldFiles.has(path) && patterns.some((pattern) => pattern.test(path)),
479
+ (path) => !heldFiles.has(path) && testPatterns.some((pattern) => pattern.test(path)),
471
480
  ),
472
481
  options.parse,
473
482
  );
@@ -1082,27 +1091,44 @@ function readEmitterRun(
1082
1091
  }
1083
1092
 
1084
1093
  /**
1085
- * The workspace members whose manifest the run reads whole enough to judge its
1086
- * dependencies: a built project holds a file below the member's directory, and no derived
1087
- * configuration at or below it was dropped. A file no built project holds may import any
1088
- * dependency the member declares.
1094
+ * The workspace members whose dependencies the run judges: `read` holds those a built
1095
+ * project holds a file below, `unbuilt` the others whose own directory, the nearest
1096
+ * manifest's above the configuration, holds a configuration the run could not build. A
1097
+ * member no configuration holds a file of is in neither.
1089
1098
  */
1090
- function membersWhollyRead(
1099
+ function judgedMembers(
1091
1100
  host: Host,
1092
1101
  targetRoot: string,
1093
1102
  read: ReadRun<EmitterExtra>,
1094
- ): readonly string[] {
1103
+ ): { readonly read: readonly string[]; readonly unbuilt: readonly string[] } {
1095
1104
  const root = resolvePath(host.workingDirectory(), targetRoot);
1096
1105
  const held = new Set(read.matrix.union.symbols.map((symbol) => symbol.position.path));
1097
- const dropped = read.notBuilt.map((one) => relativePath(root, one.configFile) ?? "");
1098
- return read.memberDirs.filter((dir) => {
1106
+ const owners = new Set(
1107
+ read.notBuilt.map((one) => nearestManifestDir(host, root, one.configFile)),
1108
+ );
1109
+ const judged = { read: [] as string[], unbuilt: [] as string[] };
1110
+ for (const dir of read.memberDirs) {
1099
1111
  const below = relativePath(root, dir);
1100
1112
  if (below === undefined) {
1101
- return false;
1113
+ continue;
1102
1114
  }
1103
- const inside = (path: string): boolean => path.startsWith(`${below}/`);
1104
- return [...held].some(inside) && !dropped.some(inside);
1105
- });
1115
+ if ([...held].some((path) => path.startsWith(`${below}/`))) {
1116
+ judged.read.push(dir);
1117
+ } else if (owners.has(dir)) {
1118
+ judged.unbuilt.push(dir);
1119
+ }
1120
+ }
1121
+ return judged;
1122
+ }
1123
+
1124
+ /** The directory of the nearest `package.json` at or above a file, inside `root`, else `root`. */
1125
+ function nearestManifestDir(host: Host, root: string, file: string): string {
1126
+ for (let dir = dirnamePath(file); relativePath(root, dir) !== undefined; dir = dirnamePath(dir)) {
1127
+ if (host.kindOf(joinPath(dir, "package.json")) === "file") {
1128
+ return dir;
1129
+ }
1130
+ }
1131
+ return root;
1106
1132
  }
1107
1133
 
1108
1134
  /**
@@ -1120,6 +1146,7 @@ function emitterInputOver(
1120
1146
  const targetRoot = scope.target.path;
1121
1147
  const swept = sweptOf(read, input);
1122
1148
  const { projects } = read;
1149
+ const members = judgedMembers(host, targetRoot, read);
1123
1150
  return {
1124
1151
  config,
1125
1152
  swept,
@@ -1137,7 +1164,8 @@ function emitterInputOver(
1137
1164
  targetRoot,
1138
1165
  [...projects.map((one) => one.extra.needs), configurationNeeds(read.conventionUses)],
1139
1166
  swept.sweep.candidates.map((candidate) => candidate.id),
1140
- membersWhollyRead(host, targetRoot, read),
1167
+ members.read,
1168
+ members.unbuilt,
1141
1169
  ),
1142
1170
  implementations: mergeImplementations(projects.map((one) => one.extra.implementations)),
1143
1171
  files: {
@@ -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: "4.1.0",
5
+ corpusVersion: "5.0.0",
6
6
  result: "pass",
7
- digest: "sha256:167f770cdff2776fa7ab28b7ff6121c4d7bdb01d83c5ba0057d9303281614008",
7
+ digest: "sha256:26c486907df44fb8eaf3f749f60a6010ac94e7cfeeda70c4aa406445193ab80a",
8
8
  },
9
9
  gaps: [],
10
10
  } as const;
@@ -63,8 +63,13 @@ export interface ContextualUses {
63
63
  readonly lookups: number;
64
64
  }
65
65
 
66
- /** The name one literal property is written under, where the name is literal text. */
67
- function propertyName(property: Node): { node: Node; text: string } | undefined {
66
+ /**
67
+ * The name one literal property is written under: its literal text, or the computed name
68
+ * node whose key the checker names, a unique symbol among them.
69
+ */
70
+ function propertyName(
71
+ property: Node,
72
+ ): { node: Node; text: string } | { node: Node; text: undefined } | undefined {
68
73
  if (
69
74
  !isPropertyAssignment(property) &&
70
75
  !isShorthandPropertyAssignment(property) &&
@@ -83,6 +88,7 @@ function propertyName(property: Node): { node: Node; text: string } | undefined
83
88
  if (isStringLiteral(key) || isNumericLiteral(key) || isNoSubstitutionTemplateLiteral(key)) {
84
89
  return { node: name, text: key.text };
85
90
  }
91
+ return { node: name, text: undefined };
86
92
  }
87
93
  return undefined;
88
94
  }
@@ -183,6 +189,31 @@ export function contextualUses<Brand>(
183
189
  literals.map((one) => one.literal),
184
190
  cap,
185
191
  );
192
+ // A computed name that is not literal text names the property the literal declares
193
+ // under the checker's own key, which is how a type declares a member a unique symbol
194
+ // keys: asked once, for the literals that have a contextual type.
195
+ const computed = literals.flatMap(({ literal }, index) =>
196
+ contexts[index] === undefined || !isAnswered(contexts[index])
197
+ ? []
198
+ : literal.properties.flatMap((property) => {
199
+ const name = propertyName(property);
200
+ return name !== undefined && name.text === undefined ? [name.node] : [];
201
+ }),
202
+ );
203
+ batches += batchesOf(computed.length);
204
+ const keys = new Map<Node, string>();
205
+ (computed.length === 0
206
+ ? []
207
+ : project.symbolsAt(
208
+ computed.map((node) => project.handle(node)),
209
+ cap,
210
+ )
211
+ ).forEach((symbol, index) => {
212
+ const node = computed[index];
213
+ if (node !== undefined && symbol !== undefined && isAnswered(symbol)) {
214
+ keys.set(node, symbol.name);
215
+ }
216
+ });
186
217
  literals.forEach(({ literal, from }, index) => {
187
218
  const context = contexts[index];
188
219
  if (context === undefined || !isAnswered(context)) {
@@ -191,10 +222,11 @@ export function contextualUses<Brand>(
191
222
  const tablesOf = partsOf(context).map(membersOf);
192
223
  for (const property of literal.properties) {
193
224
  const name = propertyName(property);
194
- if (name === undefined) {
225
+ const text = name?.text ?? (name === undefined ? undefined : keys.get(name.node));
226
+ if (name === undefined || text === undefined) {
195
227
  continue;
196
228
  }
197
- const ids = [...new Set(tablesOf.flatMap((table) => table.get(name.text)?.ids ?? []))];
229
+ const ids = [...new Set(tablesOf.flatMap((table) => table.get(text)?.ids ?? []))];
198
230
  if (ids.length > 0) {
199
231
  writes.push({ at: name.node, from, ids });
200
232
  }
@@ -45,6 +45,14 @@ export interface HelperReading extends MoveReading {
45
45
  readonly package: string;
46
46
  }
47
47
 
48
+ /** One member of a configuration file whose strings name the packages its tool loads. */
49
+ export interface PackageKey {
50
+ /** The member's name, matched at any depth of the objects the row reads. */
51
+ readonly key: string;
52
+ /** Globs naming the configuration files, matched in the manifest's directory alone. */
53
+ readonly files: readonly string[];
54
+ }
55
+
48
56
  /** One directory a row's globs name that a configuration property moves. */
49
57
  export interface DirectoryMove {
50
58
  /** The placeholder the row's globs and later moves write as `<id>`. */
@@ -83,6 +91,11 @@ export interface ConventionRow {
83
91
  * string there names no file the tool runs or loads.
84
92
  */
85
93
  readonly selections?: readonly MoveReading[];
94
+ /**
95
+ * The members whose strings name a package the tool loads: the package itself, a
96
+ * subpath of it, or the package followed by `:` and a name.
97
+ */
98
+ readonly packageKeys?: readonly PackageKey[];
86
99
  /** The members of a `package.json` the tool reads its configuration from. */
87
100
  readonly manifestKeys?: readonly string[];
88
101
  /** The settings that make the tool's output import a runtime helper package. */
@@ -131,8 +144,40 @@ const STRYKER_CONFIG = "{stryker,.stryker}.{conf,config}.{json,js,mjs,cjs}";
131
144
  const STRYKER_MODULE = "{stryker,.stryker}.{conf,config}.{js,mjs,cjs}";
132
145
  const VITEST_CONFIG = `{vitest,vite}.config.${ANY_MODULE}`;
133
146
 
147
+ /** The entry files of an Angular CLI project, at the workspace root or below `projects`. */
148
+ const ANGULAR_ENTRIES = [
149
+ "src/{main,main.server,server,polyfills,test}.ts",
150
+ "server.ts",
151
+ "projects/*/src/{main,main.server,server,polyfills,test,public-api}.ts",
152
+ "projects/*/server.ts",
153
+ ];
154
+
155
+ /** The `angular.json` members whose strings name the packages a build loads. */
156
+ const ANGULAR_PACKAGE_KEYS: readonly PackageKey[] = [
157
+ "builder",
158
+ "polyfills",
159
+ "styles",
160
+ "scripts",
161
+ ].map((key) => ({ key, files: ["angular.json"] }));
162
+
134
163
  /** Every row, ordered by name and then by range. */
135
164
  export const CONVENTION_ROWS: readonly ConventionRow[] = [
165
+ {
166
+ name: "angular",
167
+ package: "@angular/build",
168
+ range: ">=17.0.0 <22.0.0",
169
+ entries: ANGULAR_ENTRIES,
170
+ moves: [],
171
+ packageKeys: ANGULAR_PACKAGE_KEYS,
172
+ },
173
+ {
174
+ name: "angular-devkit",
175
+ package: "@angular-devkit/build-angular",
176
+ range: ">=12.0.0 <22.0.0",
177
+ entries: ANGULAR_ENTRIES,
178
+ moves: [],
179
+ packageKeys: ANGULAR_PACKAGE_KEYS,
180
+ },
136
181
  {
137
182
  name: "astro",
138
183
  package: "astro",
@@ -564,6 +609,18 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
564
609
  },
565
610
  ],
566
611
  },
612
+ {
613
+ name: "vitepress",
614
+ package: "vitepress",
615
+ range: ">=1.0.0 <2.0.0-0 || >=2.0.0-alpha.0 <3.0.0",
616
+ entries: [
617
+ `.vitepress/config.${ANY_MODULE}`,
618
+ `.vitepress/theme/index.${ANY_MODULE}`,
619
+ `**/*.data.${ANY_MODULE}`,
620
+ "**/*.vue",
621
+ ],
622
+ moves: [],
623
+ },
567
624
  {
568
625
  name: "vitest",
569
626
  package: "vitest",