@cplieger/deadset-ts 5.0.0 → 5.1.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 (71) hide show
  1. package/README.md +1 -1
  2. package/dist/src/analysis.js +145 -39
  3. package/dist/src/configuration-files.js +108 -4
  4. package/dist/src/conformance-record.js +3 -9
  5. package/dist/src/contextual-uses.js +247 -0
  6. package/dist/src/convention-rows.js +6 -0
  7. package/dist/src/conventions.js +123 -14
  8. package/dist/src/dependencies.js +8 -2
  9. package/dist/src/destructuring.js +6 -6
  10. package/dist/src/entry-points.js +8 -5
  11. package/dist/src/exempt-classes.js +6 -1
  12. package/dist/src/exempt.js +35 -16
  13. package/dist/src/file-facts.js +23 -20
  14. package/dist/src/findings/completion.js +27 -3
  15. package/dist/src/findings/interfaces.js +6 -2
  16. package/dist/src/findings/reads-and-writes.js +53 -9
  17. package/dist/src/findings/unused-declarations.js +40 -1
  18. package/dist/src/generated-file.js +24 -0
  19. package/dist/src/graph.js +1 -1
  20. package/dist/src/interface-satisfaction.js +2 -68
  21. package/dist/src/inventory.js +17 -4
  22. package/dist/src/kinds.js +4 -3
  23. package/dist/src/manifest.js +29 -4
  24. package/dist/src/outside-tests.js +139 -0
  25. package/dist/src/overrides.js +104 -0
  26. package/dist/src/paths.js +17 -0
  27. package/dist/src/query.js +15 -0
  28. package/dist/src/references.js +229 -11
  29. package/dist/src/resolve.js +12 -11
  30. package/dist/src/roots.js +121 -37
  31. package/dist/src/session.js +4 -3
  32. package/dist/src/stores.js +3 -0
  33. package/dist/src/template-field.js +60 -3
  34. package/dist/src/value-flow.js +74 -0
  35. package/dist/src/version.js +1 -1
  36. package/jsr.json +1 -1
  37. package/package.json +2 -2
  38. package/src/analysis.ts +194 -49
  39. package/src/configuration-files.ts +137 -2
  40. package/src/conformance-record.ts +3 -10
  41. package/src/contextual-uses.ts +358 -0
  42. package/src/convention-rows.ts +8 -0
  43. package/src/conventions.ts +161 -13
  44. package/src/dependencies.ts +14 -1
  45. package/src/destructuring.ts +16 -6
  46. package/src/entry-points.ts +16 -3
  47. package/src/exempt-classes.ts +6 -1
  48. package/src/exempt.ts +67 -16
  49. package/src/file-facts.ts +26 -20
  50. package/src/findings/completion.ts +31 -3
  51. package/src/findings/interfaces.ts +9 -2
  52. package/src/findings/reads-and-writes.ts +70 -9
  53. package/src/findings/unused-declarations.ts +51 -1
  54. package/src/generated-file.ts +27 -0
  55. package/src/graph.ts +4 -4
  56. package/src/interface-satisfaction.ts +1 -92
  57. package/src/inventory.ts +19 -4
  58. package/src/kinds.ts +5 -3
  59. package/src/manifest.ts +40 -4
  60. package/src/outside-tests.ts +191 -0
  61. package/src/overrides.ts +136 -0
  62. package/src/paths.ts +18 -0
  63. package/src/query.ts +26 -0
  64. package/src/references.ts +294 -22
  65. package/src/resolve.ts +14 -13
  66. package/src/roots.ts +152 -44
  67. package/src/session.ts +4 -2
  68. package/src/stores.ts +9 -0
  69. package/src/template-field.ts +65 -3
  70. package/src/value-flow.ts +104 -0
  71. package/src/version.ts +1 -1
@@ -8,6 +8,8 @@
8
8
  */
9
9
 
10
10
  import {
11
+ isExportDeclaration,
12
+ isImportDeclaration,
11
13
  isNoSubstitutionTemplateLiteral,
12
14
  isStringLiteral,
13
15
  type Node,
@@ -43,8 +45,19 @@ const DEPENDENCY_MEMBERS = [
43
45
  /** The prefixes a string names a file with, relative to the file that writes it. */
44
46
  const RELATIVE_PREFIXES = ["./", "../"] as const;
45
47
 
48
+ /** The JavaScript extensions a relative specifier from a configuration file stands for TypeScript under. */
49
+ const COUNTERPARTS: readonly (readonly [string, readonly string[]])[] = [
50
+ [".js", [".ts", ".tsx"]],
51
+ [".jsx", [".tsx", ".ts"]],
52
+ [".mjs", [".mts"]],
53
+ [".cjs", [".cts"]],
54
+ ];
55
+
56
+ /** The extensions appended to a relative specifier from a configuration file, in order. */
57
+ const APPENDED = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"] as const;
58
+
46
59
  /** The extensions the program's module resolution appends to a specifier written without one. */
47
- const RESOLVED_EXTENSIONS = [
60
+ export const RESOLVED_EXTENSIONS = [
48
61
  ".ts",
49
62
  ".tsx",
50
63
  ".d.ts",
@@ -83,10 +96,27 @@ interface ConfigurationDocument {
83
96
  readonly strings: readonly string[];
84
97
  }
85
98
 
99
+ /** Parses one file the program does not hold, with no type information. */
100
+ export type ParseFile = (fileName: string, text: string) => SourceFile;
101
+
102
+ /** One file no program holds that a configuration module outside the program leads to. */
103
+ export interface OutsideModule {
104
+ /** The file, parsed alone. */
105
+ readonly file: SourceFile;
106
+ /** The directory of the configuration module the file was reached from, which holds its manifest. */
107
+ readonly manifestDir: string;
108
+ }
109
+
86
110
  /** The configuration files one project holds and sits beside. */
87
111
  interface ConfigurationFiles {
88
112
  /** Every own file that is a configuration module, with the form its name has. */
89
113
  readonly modules: readonly { readonly file: SourceFile; readonly form: string }[];
114
+ /**
115
+ * Every configuration module in a directory of the project that the program does not
116
+ * hold, and every file such a file imports or re-exports by a relative specifier that
117
+ * the program does not hold either, read where a parser is given.
118
+ */
119
+ readonly outside: readonly OutsideModule[];
90
120
  /** Every JSON configuration file in a directory of the project's files or above one. */
91
121
  readonly documents: readonly ConfigurationDocument[];
92
122
  }
@@ -128,6 +158,7 @@ export function configurationFiles(
128
158
  host: Host,
129
159
  targetRoot: string,
130
160
  files: SourceFiles,
161
+ parse?: ParseFile,
131
162
  ): ConfigurationFiles {
132
163
  const root = resolvePath(host.workingDirectory(), targetRoot);
133
164
  const manifests = new Map<string, boolean>();
@@ -182,7 +213,111 @@ export function configurationFiles(
182
213
  documents.push({ path, strings: documentStrings(value) });
183
214
  }
184
215
  }
185
- return { modules, documents };
216
+ return {
217
+ modules,
218
+ documents,
219
+ outside:
220
+ parse === undefined ? [] : outsideModules(host, [...dirs], files, holdsManifest, parse),
221
+ };
222
+ }
223
+
224
+ /** The relative specifiers one file's imports and re-exports name. */
225
+ function relativeSpecifiers(file: SourceFile): readonly string[] {
226
+ return file.statements.flatMap((statement) => {
227
+ const specifier =
228
+ isImportDeclaration(statement) || isExportDeclaration(statement)
229
+ ? statement.moduleSpecifier
230
+ : undefined;
231
+ return specifier !== undefined &&
232
+ isStringLiteral(specifier) &&
233
+ RELATIVE_PREFIXES.some((prefix) => specifier.text.startsWith(prefix))
234
+ ? [specifier.text]
235
+ : [];
236
+ });
237
+ }
238
+
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.
243
+ */
244
+ function reachedBy(host: Host, dir: string, specifier: string): string | undefined {
245
+ 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;
250
+ }
251
+ for (const [extension, counterparts] of COUNTERPARTS) {
252
+ if (path.endsWith(extension)) {
253
+ 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
+ }
258
+ }
259
+ }
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;
267
+ }
268
+
269
+ /**
270
+ * The configuration modules of the directories that hold a manifest which the program
271
+ * does not hold, and the files they import by a relative specifier, until no further
272
+ * file joins. A file that cannot be read is skipped.
273
+ */
274
+ function outsideModules(
275
+ host: Host,
276
+ dirs: readonly string[],
277
+ files: SourceFiles,
278
+ holdsManifest: (dir: string) => boolean,
279
+ parse: ParseFile,
280
+ ): readonly OutsideModule[] {
281
+ const pending: { path: string; manifestDir: string }[] = [];
282
+ for (const dir of [...dirs].sort()) {
283
+ if (!holdsManifest(dir)) {
284
+ continue;
285
+ }
286
+ let names: readonly string[];
287
+ try {
288
+ names = host
289
+ .readDirectory(dir)
290
+ .filter((entry) => !entry.directory && configurationModuleForm(entry.name) !== undefined)
291
+ .map((entry) => entry.name)
292
+ .sort();
293
+ } catch {
294
+ continue;
295
+ }
296
+ pending.push(...names.map((name) => ({ path: joinPath(dir, name), manifestDir: dir })));
297
+ }
298
+ const found: OutsideModule[] = [];
299
+ const seen = new Set<string>();
300
+ for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
301
+ if (seen.has(next.path) || files.byName.has(next.path)) {
302
+ continue;
303
+ }
304
+ seen.add(next.path);
305
+ let text: string;
306
+ try {
307
+ text = host.readFile(next.path);
308
+ } catch {
309
+ continue;
310
+ }
311
+ const file = parse(next.path, text);
312
+ found.push({ file, manifestDir: next.manifestDir });
313
+ for (const specifier of relativeSpecifiers(file)) {
314
+ const reached = reachedBy(host, dirnamePath(next.path), specifier);
315
+ if (reached !== undefined) {
316
+ pending.push({ path: reached, manifestDir: next.manifestDir });
317
+ }
318
+ }
319
+ }
320
+ return found;
186
321
  }
187
322
 
188
323
  /** Every dependency the manifest in one directory declares, or none where it holds none to read. */
@@ -2,16 +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.1.1",
5
+ corpusVersion: "2.2.0",
6
6
  result: "pass",
7
- digest: "sha256:e5e456bf3a706f1fa9f906ae71e10ea98a78da52d7d99a5ed59dca8a72bbe927",
7
+ digest: "sha256:ce7c9087ed7b2baf3de9fa2df436b500706f4f9e94fd9e0fed633054305f06a4",
8
8
  },
9
- gaps: [
10
- {
11
- fixture: "private-member-unread",
12
- capability: "reflective-lookup",
13
- reason:
14
- "The reference pass resolves a string-literal element access to the member it names, so the member is live by that reference and the retained listing names no class for it.",
15
- },
16
- ],
9
+ gaps: [],
17
10
  } as const;
@@ -0,0 +1,358 @@
1
+ /**
2
+ * The uses contextual types make of the target's members, which no name node resolves to.
3
+ * An object-literal property writes the member of its name on each constituent of the
4
+ * literal's contextual type that declares one. A value reaching a position typed by another
5
+ * object type reads each of its members that type declares, through array elements and
6
+ * function parameters and results too; a class instance there is the
7
+ * interface-satisfaction class's evidence instead.
8
+ */
9
+
10
+ import {
11
+ isComputedPropertyName,
12
+ isGetAccessorDeclaration,
13
+ isIdentifier,
14
+ isMethodDeclaration,
15
+ isNoSubstitutionTemplateLiteral,
16
+ isNumericLiteral,
17
+ isObjectLiteralExpression,
18
+ isPropertyAssignment,
19
+ isSetAccessorDeclaration,
20
+ isShorthandPropertyAssignment,
21
+ isStringLiteral,
22
+ type Expression,
23
+ type Node,
24
+ type ObjectLiteralExpression,
25
+ } from "@typescript/native/unstable/ast";
26
+ import { SignatureKind, type Type } from "@typescript/native/unstable/sync";
27
+ import type { AliasChains } from "./alias-chain.ts";
28
+ import type { Inventory, SymbolKind } from "./inventory.ts";
29
+ import { isAnswered, type PropertyTables } from "./query.ts";
30
+ import type { ProjectView } from "./session.ts";
31
+
32
+ /** The member kinds a contextual type writes and reads: data members and the members of object types. */
33
+ const MEMBER_KINDS: ReadonlySet<SymbolKind> = new Set(["type-member", "class-member"]);
34
+
35
+ /** One object literal a walk found, and the declaration it is written inside. */
36
+ export interface LiteralAt {
37
+ readonly literal: ObjectLiteralExpression;
38
+ readonly from: string;
39
+ }
40
+
41
+ /** One value handed to a position typed by its context, and the declaration it is written inside. */
42
+ export interface FlowAt {
43
+ readonly value: Expression;
44
+ readonly from: string;
45
+ }
46
+
47
+ /** One use the contextual types make: the node it is written at and the members it names. */
48
+ export interface ContextualUse {
49
+ readonly at: Node;
50
+ readonly from: string;
51
+ readonly ids: readonly string[];
52
+ }
53
+
54
+ /** What the contextual types of one project's literals and values use, and what asking cost. */
55
+ export interface ContextualUses {
56
+ /** Each literal property that writes a member, by the property's node. */
57
+ readonly writes: readonly ContextualUse[];
58
+ /** Each value whose members a position typed by another object type reads. */
59
+ readonly reads: readonly ContextualUse[];
60
+ /** The batched lookups: contextual types and value types, one per capped run. */
61
+ readonly batches: number;
62
+ /** The per-type lookups: each distinct type's constituents, properties and declaring symbol. */
63
+ readonly lookups: number;
64
+ }
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 {
68
+ if (
69
+ !isPropertyAssignment(property) &&
70
+ !isShorthandPropertyAssignment(property) &&
71
+ !isMethodDeclaration(property) &&
72
+ !isGetAccessorDeclaration(property) &&
73
+ !isSetAccessorDeclaration(property)
74
+ ) {
75
+ return undefined;
76
+ }
77
+ const name = property.name;
78
+ if (isIdentifier(name) || isStringLiteral(name) || isNumericLiteral(name)) {
79
+ return { node: name, text: name.text };
80
+ }
81
+ if (isComputedPropertyName(name)) {
82
+ const key = name.expression;
83
+ if (isStringLiteral(key) || isNumericLiteral(key) || isNoSubstitutionTemplateLiteral(key)) {
84
+ return { node: name, text: key.text };
85
+ }
86
+ }
87
+ return undefined;
88
+ }
89
+
90
+ /** The parameter and result types of one call signature, each where the checker answered. */
91
+ interface Called {
92
+ readonly parameters: readonly (Type | undefined)[];
93
+ readonly result: Type | undefined;
94
+ }
95
+
96
+ /** One object type a value is checked as, and the position's object types it is checked against. */
97
+ interface Checked {
98
+ readonly source: Type;
99
+ readonly targets: readonly Type[];
100
+ }
101
+
102
+ /** One member of an object type, by the inventory's declarations and the checker's declaration handles. */
103
+ interface Member {
104
+ readonly ids: readonly string[];
105
+ readonly handles: ReadonlySet<string>;
106
+ }
107
+
108
+ /**
109
+ * The contextual uses of one project's literals and values, asked in batches: one
110
+ * contextual type per literal and per value whose type carries a member of the target,
111
+ * one type per value, and each distinct type's properties once.
112
+ */
113
+ export function contextualUses<Brand>(
114
+ project: ProjectView<Brand>,
115
+ held: Inventory,
116
+ chains: AliasChains,
117
+ literals: readonly LiteralAt[],
118
+ flows: readonly FlowAt[],
119
+ cap: number,
120
+ properties: PropertyTables,
121
+ ): ContextualUses {
122
+ const queries = project.queries;
123
+ const kinds = new Map(held.symbols.map((symbol) => [symbol.id, symbol.kind]));
124
+ let batches = 0;
125
+ let lookups = 0;
126
+ const batchesOf = (count: number): number => Math.ceil(count / cap);
127
+
128
+ const parts = new Map<number, readonly Type[]>();
129
+ /** The object types one type is: itself, or its constituents at any depth. */
130
+ const partsOf = (type: Type): readonly Type[] => {
131
+ const known = parts.get(type.id);
132
+ if (known !== undefined) {
133
+ return known;
134
+ }
135
+ let found: readonly Type[] = [];
136
+ if (type.isUnionType() || type.isIntersectionType()) {
137
+ lookups += 1;
138
+ const constituents = queries.constituents(type);
139
+ found = isAnswered(constituents) ? constituents.flatMap(partsOf) : [];
140
+ } else if (type.isObjectType()) {
141
+ found = [type];
142
+ }
143
+ parts.set(type.id, found);
144
+ return found;
145
+ };
146
+
147
+ const tables = new Map<number, ReadonlyMap<string, Member>>();
148
+ /** The members one object type has, by name. */
149
+ const membersOf = (type: Type): ReadonlyMap<string, Member> => {
150
+ const known = tables.get(type.id);
151
+ if (known !== undefined) {
152
+ return known;
153
+ }
154
+ const table = new Map<string, Member>();
155
+ const read = properties.of(type);
156
+ lookups += read.asked ? 1 : 0;
157
+ for (const property of isAnswered(read.properties) ? read.properties : []) {
158
+ table.set(property.name, {
159
+ ids: chains
160
+ .declarationsOf(property)
161
+ .filter((id) => MEMBER_KINDS.has(kinds.get(id) ?? "file")),
162
+ handles: new Set(
163
+ property.declarations.map((handle) => `${handle.path}#${String(handle.index)}`),
164
+ ),
165
+ });
166
+ }
167
+ tables.set(type.id, table);
168
+ return table;
169
+ };
170
+
171
+ const writes: ContextualUse[] = [];
172
+ batches += batchesOf(literals.length);
173
+ const contexts = queries.contextualTypes(
174
+ literals.map((one) => one.literal),
175
+ cap,
176
+ );
177
+ literals.forEach(({ literal, from }, index) => {
178
+ const context = contexts[index];
179
+ if (context === undefined || !isAnswered(context)) {
180
+ return;
181
+ }
182
+ const tablesOf = partsOf(context).map(membersOf);
183
+ for (const property of literal.properties) {
184
+ const name = propertyName(property);
185
+ if (name === undefined) {
186
+ continue;
187
+ }
188
+ const ids = [...new Set(tablesOf.flatMap((table) => table.get(name.text)?.ids ?? []))];
189
+ if (ids.length > 0) {
190
+ writes.push({ at: name.node, from, ids });
191
+ }
192
+ }
193
+ });
194
+
195
+ const carried = flows.filter((flow) => !isObjectLiteralExpression(flow.value));
196
+ batches += batchesOf(carried.length);
197
+ const types = queries.typesAt(
198
+ carried.map((flow) => flow.value),
199
+ cap,
200
+ );
201
+ /** The members a position may read structurally: those of object types, not of classes. */
202
+ const readable = (member: Member): readonly string[] =>
203
+ member.ids.filter((id) => kinds.get(id) === "type-member");
204
+ const carries = (part: Type): boolean =>
205
+ [...membersOf(part).values()].some((member) => readable(member).length > 0);
206
+
207
+ const elements = new Map<number, Type | undefined>();
208
+ /** The element type of an array type, read once per type. */
209
+ const elementOf = (type: Type): Type | undefined => {
210
+ if (elements.has(type.id)) {
211
+ return elements.get(type.id);
212
+ }
213
+ lookups += 1;
214
+ const isArray = queries.isArray(type);
215
+ let element: Type | undefined;
216
+ if (isAnswered(isArray) && isArray) {
217
+ lookups += 1;
218
+ const held = queries.typeArguments(type);
219
+ element = isAnswered(held) ? held[0] : undefined;
220
+ }
221
+ elements.set(type.id, element);
222
+ return element;
223
+ };
224
+
225
+ const calls = new Map<number, Called | undefined>();
226
+ /** The parameter and return types of a type's one call signature, read once per type. */
227
+ const callOf = (type: Type): Called | undefined => {
228
+ if (calls.has(type.id)) {
229
+ return calls.get(type.id);
230
+ }
231
+ lookups += 1;
232
+ const signatures = queries.signaturesOf(type, SignatureKind.Call);
233
+ let called: Called | undefined;
234
+ const [signature, ...more] = isAnswered(signatures) ? signatures : [];
235
+ if (signature !== undefined && more.length === 0) {
236
+ const parameters = signature.parameters.map((_parameter, index) =>
237
+ queries.parameterType(signature, index),
238
+ );
239
+ const result = queries.returnTypeOf(signature);
240
+ lookups += parameters.length + 1;
241
+ called = {
242
+ parameters: parameters.map((one) => (isAnswered(one) ? one : undefined)),
243
+ result: isAnswered(result) ? result : undefined,
244
+ };
245
+ }
246
+ calls.set(type.id, called);
247
+ return called;
248
+ };
249
+
250
+ /**
251
+ * Each object type a value of `source` is checked as against the object types of
252
+ * `target`: the value's own, and through array elements and through a function's
253
+ * parameters, checked the other way, and its result. A source that is one of the
254
+ * target's own object types matches it as it stands. Each pair is compared once, which
255
+ * ends the walk over a recursive type.
256
+ */
257
+ const compared = (
258
+ source: Type | undefined,
259
+ target: Type | undefined,
260
+ into: Checked[],
261
+ seen: Set<string>,
262
+ ): void => {
263
+ const key = `${String(source?.id)}>${String(target?.id)}`;
264
+ if (source === undefined || target === undefined || source.id === target.id) {
265
+ return;
266
+ }
267
+ if (seen.has(key)) {
268
+ return;
269
+ }
270
+ seen.add(key);
271
+ const targets = partsOf(target);
272
+ const sources = partsOf(source).filter((one) => !targets.some((part) => part.id === one.id));
273
+ for (const one of sources) {
274
+ if (carries(one)) {
275
+ into.push({ source: one, targets });
276
+ }
277
+ const element = elementOf(one);
278
+ const called = element === undefined ? callOf(one) : undefined;
279
+ for (const part of targets) {
280
+ if (element !== undefined) {
281
+ compared(element, elementOf(part), into, seen);
282
+ continue;
283
+ }
284
+ const other = called === undefined ? undefined : callOf(part);
285
+ if (called === undefined || other === undefined) {
286
+ continue;
287
+ }
288
+ other.parameters.forEach((parameter, index) => {
289
+ compared(parameter, called.parameters[index], into, seen);
290
+ });
291
+ compared(called.result, other.result, into, seen);
292
+ }
293
+ }
294
+ };
295
+
296
+ /**
297
+ * Whether a value of one type may carry a member a position reads, at any depth the
298
+ * walk takes. A type met again on the way adds nothing its first visit did not.
299
+ */
300
+ const mayCarry = (type: Type, seen: Set<number> = new Set()): boolean => {
301
+ if (seen.has(type.id)) {
302
+ return false;
303
+ }
304
+ seen.add(type.id);
305
+ return partsOf(type).some((part) => {
306
+ if (carries(part)) {
307
+ return true;
308
+ }
309
+ const element = elementOf(part);
310
+ if (element !== undefined) {
311
+ return mayCarry(element, seen);
312
+ }
313
+ return callOf(part) !== undefined;
314
+ });
315
+ };
316
+
317
+ const sourced = carried.flatMap((flow, index) => {
318
+ const type = types[index];
319
+ return type === undefined || !isAnswered(type) || !mayCarry(type) ? [] : [{ flow, type }];
320
+ });
321
+ batches += batchesOf(sourced.length);
322
+ const positions = queries.contextualTypes(
323
+ sourced.map((one) => one.flow.value),
324
+ cap,
325
+ );
326
+ const reads: ContextualUse[] = [];
327
+ sourced.forEach(({ flow, type }, index) => {
328
+ const position = positions[index];
329
+ if (position === undefined || !isAnswered(position)) {
330
+ return;
331
+ }
332
+ const checked: Checked[] = [];
333
+ compared(type, position, checked, new Set());
334
+ const ids = new Set<string>();
335
+ for (const { source, targets } of checked) {
336
+ const tables = targets.map(membersOf);
337
+ for (const [name, member] of membersOf(source)) {
338
+ const read = tables.some((table) => {
339
+ const declared = table.get(name);
340
+ return (
341
+ declared !== undefined &&
342
+ declared.handles.size > 0 &&
343
+ ![...declared.handles].some((handle) => member.handles.has(handle))
344
+ );
345
+ });
346
+ if (read) {
347
+ for (const id of readable(member)) {
348
+ ids.add(id);
349
+ }
350
+ }
351
+ }
352
+ }
353
+ if (ids.size > 0) {
354
+ reads.push({ at: flow.value, from: flow.from, ids: [...ids] });
355
+ }
356
+ });
357
+ return { writes, reads, batches, lookups };
358
+ }
@@ -52,6 +52,8 @@ export interface ConventionRow {
52
52
  readonly entries: readonly string[];
53
53
  /** The globs of the files the row does not root, though an entry glob names them. */
54
54
  readonly excludes?: readonly string[];
55
+ /** The directories the framework generates, each of whose files is a generated file. */
56
+ readonly generated?: readonly string[];
55
57
  /** The directories configuration moves, in the order their placeholders depend on each other. */
56
58
  readonly moves: readonly DirectoryMove[];
57
59
  }
@@ -102,6 +104,7 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
102
104
  `<src>/content/config.${MODULE}`,
103
105
  ],
104
106
  excludes: ["<src>/pages/**/_*", "<src>/pages/**/_*/**"],
107
+ generated: [".astro"],
105
108
  moves: [
106
109
  {
107
110
  id: "src",
@@ -128,6 +131,7 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
128
131
  name: "next",
129
132
  package: "next",
130
133
  range: ">=13.4.0 <17.0.0",
134
+ generated: [".next"],
131
135
  entries: [
132
136
  `{,src/}app/**/{layout,page,loading,not-found,error,route,template,default,forbidden,unauthorized}.${SCRIPT}`,
133
137
  `{,src/}app/global-{error,not-found}.${SCRIPT}`,
@@ -144,6 +148,7 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
144
148
  name: "nuxt",
145
149
  package: "nuxt",
146
150
  range: ">=3.0.0 <5.0.0",
151
+ generated: [".nuxt"],
147
152
  entries: [
148
153
  "<srcDir>/{app,error}.{vue,jsx,tsx}",
149
154
  `<srcDir>/app.config.${MODULE}`,
@@ -238,6 +243,7 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
238
243
  package: "@react-router/dev",
239
244
  range: ">=7.0.0 <9.0.0",
240
245
  entries: APP_DIRECTORY_ENTRIES,
246
+ generated: [".react-router"],
241
247
  moves: [
242
248
  {
243
249
  id: "app",
@@ -316,6 +322,7 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
316
322
  package: "@sveltejs/kit",
317
323
  range: ">=2.0.0 <3.0.0",
318
324
  entries: [...SVELTEKIT_ENTRIES, "<params>/*.{js,ts}"],
325
+ generated: [".svelte-kit"],
319
326
  moves: [
320
327
  {
321
328
  id: "src",
@@ -380,6 +387,7 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
380
387
  package: "@sveltejs/kit",
381
388
  range: ">=3.0.0 <4.0.0",
382
389
  entries: [...SVELTEKIT_ENTRIES, "<params>.{js,ts}"],
390
+ generated: [".svelte-kit"],
383
391
  moves: [
384
392
  {
385
393
  id: "src",