@cplieger/deadset-ts 5.0.0 → 5.1.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 (55) hide show
  1. package/dist/src/analysis.js +77 -10
  2. package/dist/src/configuration-files.js +107 -3
  3. package/dist/src/conformance-record.js +3 -9
  4. package/dist/src/contextual-uses.js +240 -0
  5. package/dist/src/convention-rows.js +6 -0
  6. package/dist/src/conventions.js +123 -14
  7. package/dist/src/dependencies.js +8 -2
  8. package/dist/src/destructuring.js +6 -6
  9. package/dist/src/entry-points.js +8 -5
  10. package/dist/src/exempt-classes.js +6 -1
  11. package/dist/src/exempt.js +21 -17
  12. package/dist/src/findings/completion.js +27 -3
  13. package/dist/src/findings/interfaces.js +6 -2
  14. package/dist/src/findings/reads-and-writes.js +53 -9
  15. package/dist/src/findings/unused-declarations.js +40 -1
  16. package/dist/src/generated-file.js +24 -0
  17. package/dist/src/interface-satisfaction.js +2 -68
  18. package/dist/src/inventory.js +4 -1
  19. package/dist/src/kinds.js +4 -3
  20. package/dist/src/query.js +15 -0
  21. package/dist/src/references.js +199 -10
  22. package/dist/src/resolve.js +12 -11
  23. package/dist/src/roots.js +1 -1
  24. package/dist/src/stores.js +3 -0
  25. package/dist/src/value-flow.js +74 -0
  26. package/dist/src/version.js +1 -1
  27. package/jsr.json +1 -1
  28. package/package.json +2 -2
  29. package/src/analysis.ts +92 -13
  30. package/src/configuration-files.ts +136 -1
  31. package/src/conformance-record.ts +3 -10
  32. package/src/contextual-uses.ts +354 -0
  33. package/src/convention-rows.ts +8 -0
  34. package/src/conventions.ts +161 -13
  35. package/src/dependencies.ts +14 -1
  36. package/src/destructuring.ts +16 -6
  37. package/src/entry-points.ts +16 -3
  38. package/src/exempt-classes.ts +6 -1
  39. package/src/exempt.ts +48 -17
  40. package/src/file-facts.ts +2 -0
  41. package/src/findings/completion.ts +31 -3
  42. package/src/findings/interfaces.ts +9 -2
  43. package/src/findings/reads-and-writes.ts +70 -9
  44. package/src/findings/unused-declarations.ts +51 -1
  45. package/src/generated-file.ts +27 -0
  46. package/src/interface-satisfaction.ts +1 -92
  47. package/src/inventory.ts +4 -1
  48. package/src/kinds.ts +5 -3
  49. package/src/query.ts +26 -0
  50. package/src/references.ts +254 -21
  51. package/src/resolve.ts +14 -13
  52. package/src/roots.ts +11 -1
  53. package/src/stores.ts +9 -0
  54. package/src/value-flow.ts +104 -0
  55. package/src/version.ts +1 -1
@@ -0,0 +1,354 @@
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
+ /** How many element and signature steps the structural walk takes below a value's own type. */
103
+ const MAX_DEPTH = 3;
104
+
105
+ /** One member of an object type, by the inventory's declarations and the checker's declaration handles. */
106
+ interface Member {
107
+ readonly ids: readonly string[];
108
+ readonly handles: ReadonlySet<string>;
109
+ }
110
+
111
+ /**
112
+ * The contextual uses of one project's literals and values, asked in batches: one
113
+ * contextual type per literal and per value whose type carries a member of the target,
114
+ * one type per value, and each distinct type's properties once.
115
+ */
116
+ export function contextualUses<Brand>(
117
+ project: ProjectView<Brand>,
118
+ held: Inventory,
119
+ chains: AliasChains,
120
+ literals: readonly LiteralAt[],
121
+ flows: readonly FlowAt[],
122
+ cap: number,
123
+ properties: PropertyTables,
124
+ ): ContextualUses {
125
+ const queries = project.queries;
126
+ const kinds = new Map(held.symbols.map((symbol) => [symbol.id, symbol.kind]));
127
+ let batches = 0;
128
+ let lookups = 0;
129
+ const batchesOf = (count: number): number => Math.ceil(count / cap);
130
+
131
+ const parts = new Map<number, readonly Type[]>();
132
+ /** The object types one type is: itself, or its constituents at any depth. */
133
+ const partsOf = (type: Type): readonly Type[] => {
134
+ const known = parts.get(type.id);
135
+ if (known !== undefined) {
136
+ return known;
137
+ }
138
+ let found: readonly Type[] = [];
139
+ if (type.isUnionType() || type.isIntersectionType()) {
140
+ lookups += 1;
141
+ const constituents = queries.constituents(type);
142
+ found = isAnswered(constituents) ? constituents.flatMap(partsOf) : [];
143
+ } else if (type.isObjectType()) {
144
+ found = [type];
145
+ }
146
+ parts.set(type.id, found);
147
+ return found;
148
+ };
149
+
150
+ const tables = new Map<number, ReadonlyMap<string, Member>>();
151
+ /** The members one object type has, by name. */
152
+ const membersOf = (type: Type): ReadonlyMap<string, Member> => {
153
+ const known = tables.get(type.id);
154
+ if (known !== undefined) {
155
+ return known;
156
+ }
157
+ const table = new Map<string, Member>();
158
+ const read = properties.of(type);
159
+ lookups += read.asked ? 1 : 0;
160
+ for (const property of isAnswered(read.properties) ? read.properties : []) {
161
+ table.set(property.name, {
162
+ ids: chains
163
+ .declarationsOf(property)
164
+ .filter((id) => MEMBER_KINDS.has(kinds.get(id) ?? "file")),
165
+ handles: new Set(
166
+ property.declarations.map((handle) => `${handle.path}#${String(handle.index)}`),
167
+ ),
168
+ });
169
+ }
170
+ tables.set(type.id, table);
171
+ return table;
172
+ };
173
+
174
+ const writes: ContextualUse[] = [];
175
+ batches += batchesOf(literals.length);
176
+ const contexts = queries.contextualTypes(
177
+ literals.map((one) => one.literal),
178
+ cap,
179
+ );
180
+ literals.forEach(({ literal, from }, index) => {
181
+ const context = contexts[index];
182
+ if (context === undefined || !isAnswered(context)) {
183
+ return;
184
+ }
185
+ const tablesOf = partsOf(context).map(membersOf);
186
+ for (const property of literal.properties) {
187
+ const name = propertyName(property);
188
+ if (name === undefined) {
189
+ continue;
190
+ }
191
+ const ids = [...new Set(tablesOf.flatMap((table) => table.get(name.text)?.ids ?? []))];
192
+ if (ids.length > 0) {
193
+ writes.push({ at: name.node, from, ids });
194
+ }
195
+ }
196
+ });
197
+
198
+ const carried = flows.filter((flow) => !isObjectLiteralExpression(flow.value));
199
+ batches += batchesOf(carried.length);
200
+ const types = queries.typesAt(
201
+ carried.map((flow) => flow.value),
202
+ cap,
203
+ );
204
+ /** The members a position may read structurally: those of object types, not of classes. */
205
+ const readable = (member: Member): readonly string[] =>
206
+ member.ids.filter((id) => kinds.get(id) === "type-member");
207
+ const carries = (part: Type): boolean =>
208
+ [...membersOf(part).values()].some((member) => readable(member).length > 0);
209
+
210
+ const elements = new Map<number, Type | undefined>();
211
+ /** The element type of an array type, read once per type. */
212
+ const elementOf = (type: Type): Type | undefined => {
213
+ if (elements.has(type.id)) {
214
+ return elements.get(type.id);
215
+ }
216
+ lookups += 1;
217
+ const isArray = queries.isArray(type);
218
+ let element: Type | undefined;
219
+ if (isAnswered(isArray) && isArray) {
220
+ lookups += 1;
221
+ const held = queries.typeArguments(type);
222
+ element = isAnswered(held) ? held[0] : undefined;
223
+ }
224
+ elements.set(type.id, element);
225
+ return element;
226
+ };
227
+
228
+ const calls = new Map<number, Called | undefined>();
229
+ /** The parameter and return types of a type's one call signature, read once per type. */
230
+ const callOf = (type: Type): Called | undefined => {
231
+ if (calls.has(type.id)) {
232
+ return calls.get(type.id);
233
+ }
234
+ lookups += 1;
235
+ const signatures = queries.signaturesOf(type, SignatureKind.Call);
236
+ let called: Called | undefined;
237
+ const [signature, ...more] = isAnswered(signatures) ? signatures : [];
238
+ if (signature !== undefined && more.length === 0) {
239
+ const parameters = signature.parameters.map((_parameter, index) =>
240
+ queries.parameterType(signature, index),
241
+ );
242
+ const result = queries.returnTypeOf(signature);
243
+ lookups += parameters.length + 1;
244
+ called = {
245
+ parameters: parameters.map((one) => (isAnswered(one) ? one : undefined)),
246
+ result: isAnswered(result) ? result : undefined,
247
+ };
248
+ }
249
+ calls.set(type.id, called);
250
+ return called;
251
+ };
252
+
253
+ /**
254
+ * Each object type a value of `source` is checked as against the object types of
255
+ * `target`: the value's own, and through array elements and through a function's
256
+ * parameters, checked the other way, and its result. A source that is one of the
257
+ * target's own object types matches it as it stands.
258
+ */
259
+ const compared = (
260
+ source: Type | undefined,
261
+ target: Type | undefined,
262
+ depth: number,
263
+ into: Checked[],
264
+ seen: Set<string>,
265
+ ): void => {
266
+ const key = `${String(source?.id)}>${String(target?.id)}`;
267
+ if (source === undefined || target === undefined || source.id === target.id) {
268
+ return;
269
+ }
270
+ if (depth > MAX_DEPTH || seen.has(key)) {
271
+ return;
272
+ }
273
+ seen.add(key);
274
+ const targets = partsOf(target);
275
+ const sources = partsOf(source).filter((one) => !targets.some((part) => part.id === one.id));
276
+ for (const one of sources) {
277
+ if (carries(one)) {
278
+ into.push({ source: one, targets });
279
+ }
280
+ const element = elementOf(one);
281
+ const called = element === undefined ? callOf(one) : undefined;
282
+ for (const part of targets) {
283
+ if (element !== undefined) {
284
+ compared(element, elementOf(part), depth + 1, into, seen);
285
+ continue;
286
+ }
287
+ const other = called === undefined ? undefined : callOf(part);
288
+ if (called === undefined || other === undefined) {
289
+ continue;
290
+ }
291
+ other.parameters.forEach((parameter, index) => {
292
+ compared(parameter, called.parameters[index], depth + 1, into, seen);
293
+ });
294
+ compared(called.result, other.result, depth + 1, into, seen);
295
+ }
296
+ }
297
+ };
298
+
299
+ /** Whether a value of one type may carry a member a position reads, at any depth the walk takes. */
300
+ const mayCarry = (type: Type, depth: number): boolean =>
301
+ depth <= MAX_DEPTH &&
302
+ partsOf(type).some((part) => {
303
+ if (carries(part)) {
304
+ return true;
305
+ }
306
+ const element = elementOf(part);
307
+ if (element !== undefined) {
308
+ return mayCarry(element, depth + 1);
309
+ }
310
+ return callOf(part) !== undefined;
311
+ });
312
+
313
+ const sourced = carried.flatMap((flow, index) => {
314
+ const type = types[index];
315
+ return type === undefined || !isAnswered(type) || !mayCarry(type, 0) ? [] : [{ flow, type }];
316
+ });
317
+ batches += batchesOf(sourced.length);
318
+ const positions = queries.contextualTypes(
319
+ sourced.map((one) => one.flow.value),
320
+ cap,
321
+ );
322
+ const reads: ContextualUse[] = [];
323
+ sourced.forEach(({ flow, type }, index) => {
324
+ const position = positions[index];
325
+ if (position === undefined || !isAnswered(position)) {
326
+ return;
327
+ }
328
+ const checked: Checked[] = [];
329
+ compared(type, position, 0, checked, new Set());
330
+ const ids = new Set<string>();
331
+ for (const { source, targets } of checked) {
332
+ const tables = targets.map(membersOf);
333
+ for (const [name, member] of membersOf(source)) {
334
+ const read = tables.some((table) => {
335
+ const declared = table.get(name);
336
+ return (
337
+ declared !== undefined &&
338
+ declared.handles.size > 0 &&
339
+ ![...declared.handles].some((handle) => member.handles.has(handle))
340
+ );
341
+ });
342
+ if (read) {
343
+ for (const id of readable(member)) {
344
+ ids.add(id);
345
+ }
346
+ }
347
+ }
348
+ }
349
+ if (ids.size > 0) {
350
+ reads.push({ at: flow.value, from: flow.from, ids: [...ids] });
351
+ }
352
+ });
353
+ return { writes, reads, batches, lookups };
354
+ }
@@ -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",
@@ -22,6 +22,8 @@ import {
22
22
  isNamespaceImport,
23
23
  isNoSubstitutionTemplateLiteral,
24
24
  isNonNullExpression,
25
+ isNumericLiteral,
26
+ isObjectBindingPattern,
25
27
  isObjectLiteralExpression,
26
28
  isParenthesizedExpression,
27
29
  isPropertyAccessExpression,
@@ -33,7 +35,9 @@ import {
33
35
  isVariableStatement,
34
36
  SyntaxKind,
35
37
  type AsExpression,
38
+ type Identifier,
36
39
  type Node,
40
+ type ObjectBindingPattern,
37
41
  type NonNullExpression,
38
42
  type ObjectLiteralExpression,
39
43
  type ParenthesizedExpression,
@@ -81,6 +85,14 @@ interface ConventionGlob {
81
85
  readonly excludes: readonly RegExp[];
82
86
  }
83
87
 
88
+ /** One directory an applied row names as generated, read against its manifest's directory. */
89
+ interface GeneratedDirectory {
90
+ readonly dir: string;
91
+ readonly row: string;
92
+ /** The directory below the manifest's directory, `/`-separated. */
93
+ readonly below: string;
94
+ }
95
+
84
96
  /** A setup failure a row met, at the directory of the manifest that declared it. */
85
97
  interface ConventionFailure {
86
98
  readonly dir: string;
@@ -92,6 +104,7 @@ export interface Conventions {
92
104
  /** The rows applied, ordered by name, then manifest, then their compact encoding. */
93
105
  readonly applied: readonly AppliedConvention[];
94
106
  readonly globs: readonly ConventionGlob[];
107
+ readonly generated: readonly GeneratedDirectory[];
95
108
  readonly failures: readonly ConventionFailure[];
96
109
  }
97
110
 
@@ -105,9 +118,11 @@ interface ConventionEntry {
105
118
  /** Parses one file's text with no program. */
106
119
  type ParseSource = (fileName: string, text: string) => SourceFile;
107
120
 
121
+ /** The sections an installed package is required by, so its absence fails the run. */
122
+ const REQUIRED_SECTIONS = ["dependencies", "devDependencies"] as const;
123
+
108
124
  const DEPENDENCY_SECTIONS = [
109
- "dependencies",
110
- "devDependencies",
125
+ ...REQUIRED_SECTIONS,
111
126
  "peerDependencies",
112
127
  "optionalDependencies",
113
128
  ] as const;
@@ -124,11 +139,15 @@ function readJSON(host: Host, path: string): unknown {
124
139
  }
125
140
  }
126
141
 
127
- function declares(manifest: unknown, name: string): boolean {
142
+ function declares(
143
+ manifest: unknown,
144
+ name: string,
145
+ sections: readonly string[] = DEPENDENCY_SECTIONS,
146
+ ): boolean {
128
147
  if (!isRecord(manifest)) {
129
148
  return false;
130
149
  }
131
- return DEPENDENCY_SECTIONS.some((section) => {
150
+ return sections.some((section) => {
132
151
  const held = manifest[section];
133
152
  return isRecord(held) && Object.hasOwn(held, name);
134
153
  });
@@ -174,21 +193,33 @@ function unwrapped(node: Node): Node {
174
193
  return at;
175
194
  }
176
195
 
177
- /** The key one property is written with, where it is written as a name or a literal. */
196
+ /** The key one property is written with, where it is written as a name or as literal text. */
178
197
  function keyOf(property: Node): string | undefined {
179
198
  if (!isPropertyAssignment(property) && !isShorthandPropertyAssignment(property)) {
180
199
  return undefined;
181
200
  }
182
201
  const { name } = property;
183
- if (isComputedPropertyName(name)) {
184
- return undefined;
185
- }
186
- if (isIdentifier(name) || isStringLiteral(name) || isNoSubstitutionTemplateLiteral(name)) {
187
- return name.text;
202
+ const key = isComputedPropertyName(name) ? name.expression : name;
203
+ if (
204
+ (isIdentifier(key) && key === name) ||
205
+ isStringLiteral(key) ||
206
+ isNoSubstitutionTemplateLiteral(key)
207
+ ) {
208
+ return key.text;
188
209
  }
189
210
  return undefined;
190
211
  }
191
212
 
213
+ /** Whether one member's computed name is no literal, so it may set any key. */
214
+ function computesKey(member: Node): boolean {
215
+ const name = (member as { readonly name?: Node }).name;
216
+ if (name === undefined || !isComputedPropertyName(name)) {
217
+ return false;
218
+ }
219
+ const key = name.expression;
220
+ return !(isStringLiteral(key) || isNumericLiteral(key) || isNoSubstitutionTemplateLiteral(key));
221
+ }
222
+
192
223
  /** The values one file's top-level variable declarations bind, by name. */
193
224
  function declaredValues(file: SourceFile): ReadonlyMap<string, Node> {
194
225
  const values = new Map<string, Node>();
@@ -247,6 +278,79 @@ function defaultExports(file: SourceFile): readonly Node[] {
247
278
  return found;
248
279
  }
249
280
 
281
+ /** The declarations whose name is a binding of the scope they are written in. */
282
+ const BINDING_KINDS: ReadonlySet<SyntaxKind> = new Set([
283
+ SyntaxKind.VariableDeclaration,
284
+ SyntaxKind.Parameter,
285
+ SyntaxKind.BindingElement,
286
+ SyntaxKind.FunctionDeclaration,
287
+ SyntaxKind.ClassDeclaration,
288
+ SyntaxKind.ImportClause,
289
+ SyntaxKind.ImportSpecifier,
290
+ SyntaxKind.NamespaceImport,
291
+ SyntaxKind.ImportEqualsDeclaration,
292
+ ]);
293
+
294
+ /** Whether one file declares a binding named `require` at any depth, which shadows the global. */
295
+ function declaresRequire(file: SourceFile): boolean {
296
+ let found = false;
297
+ const visit = (node: Node): void => {
298
+ const name = (node as { readonly name?: Node }).name;
299
+ if (
300
+ name !== undefined &&
301
+ isIdentifier(name) &&
302
+ name.text === "require" &&
303
+ BINDING_KINDS.has(node.kind)
304
+ ) {
305
+ found = true;
306
+ }
307
+ if (!found) {
308
+ node.forEachChild(visit);
309
+ }
310
+ };
311
+ file.forEachChild(visit);
312
+ return found;
313
+ }
314
+
315
+ /**
316
+ * The top-level `const`, `let` and `var` declarations that bind a `require` of one module,
317
+ * written with the module's name as its one string-literal argument, in a file that
318
+ * declares no binding named `require`: each binds as an import of the module does.
319
+ */
320
+ function requireBindings(
321
+ file: SourceFile,
322
+ module: string,
323
+ ): readonly (Identifier | ObjectBindingPattern)[] {
324
+ const found: (Identifier | ObjectBindingPattern)[] = [];
325
+ for (const statement of file.statements) {
326
+ if (!isVariableStatement(statement)) {
327
+ continue;
328
+ }
329
+ for (const declaration of statement.declarationList.declarations) {
330
+ const value =
331
+ declaration.initializer === undefined ? undefined : unwrapped(declaration.initializer);
332
+ const [argument, ...rest] =
333
+ value !== undefined && isCallExpression(value) ? value.arguments : [];
334
+ if (
335
+ value === undefined ||
336
+ !isCallExpression(value) ||
337
+ !isIdentifier(value.expression) ||
338
+ value.expression.text !== "require" ||
339
+ argument === undefined ||
340
+ rest.length > 0 ||
341
+ !isStringLiteral(argument) ||
342
+ argument.text !== module
343
+ ) {
344
+ continue;
345
+ }
346
+ if (isIdentifier(declaration.name) || isObjectBindingPattern(declaration.name)) {
347
+ found.push(declaration.name);
348
+ }
349
+ }
350
+ }
351
+ return found.length > 0 && declaresRequire(file) ? [] : found;
352
+ }
353
+
250
354
  /**
251
355
  * The first argument of every call one file makes to one module's export, through a
252
356
  * named or a namespace import; `undefined` for a call with no argument.
@@ -254,6 +358,26 @@ function defaultExports(file: SourceFile): readonly Node[] {
254
358
  function optionsArguments(file: SourceFile, call: OptionsCall): readonly (Node | undefined)[] {
255
359
  const names = new Set<string>();
256
360
  const namespaces = new Set<string>();
361
+ for (const name of requireBindings(file, call.module)) {
362
+ if (isIdentifier(name)) {
363
+ namespaces.add(name.text);
364
+ continue;
365
+ }
366
+ for (const element of name.elements) {
367
+ const local = element.name;
368
+ const exported = element.propertyName ?? local;
369
+ if (
370
+ element.dotDotDotToken === undefined &&
371
+ local !== undefined &&
372
+ isIdentifier(local) &&
373
+ exported !== undefined &&
374
+ (isIdentifier(exported) || isStringLiteral(exported)) &&
375
+ exported.text === call.export
376
+ ) {
377
+ names.add(local.text);
378
+ }
379
+ }
380
+ }
257
381
  for (const statement of file.statements) {
258
382
  const bindings = isImportDeclaration(statement)
259
383
  ? statement.importClause?.namedBindings
@@ -333,7 +457,7 @@ function readPath(
333
457
  ): Node | undefined {
334
458
  const [key, ...rest] = keys;
335
459
  for (const member of object.properties) {
336
- if (isSpreadAssignment(member)) {
460
+ if (isSpreadAssignment(member) || computesKey(member)) {
337
461
  return member;
338
462
  }
339
463
  if (keyOf(member) !== key) {
@@ -518,6 +642,7 @@ export function readConventions(
518
642
  ): Conventions {
519
643
  const applied: AppliedConvention[] = [];
520
644
  const globs: ConventionGlob[] = [];
645
+ const generated: GeneratedDirectory[] = [];
521
646
  const failures: ConventionFailure[] = [];
522
647
  for (const manifest of manifests) {
523
648
  const declared = readJSON(host, joinPath(manifest.dir, "package.json"));
@@ -532,7 +657,7 @@ export function readConventions(
532
657
  }
533
658
  const version = versions.get(row.package);
534
659
  if (version === undefined) {
535
- if (missing.has(row.package)) {
660
+ if (missing.has(row.package) || !declares(declared, row.package, REQUIRED_SECTIONS)) {
536
661
  continue;
537
662
  }
538
663
  missing.add(row.package);
@@ -580,6 +705,9 @@ export function readConventions(
580
705
  const excludes = [
581
706
  ...new Set((row.excludes ?? []).flatMap((one) => expand(one, resolved))),
582
707
  ].map(globExpression);
708
+ for (const below of new Set((row.generated ?? []).flatMap((one) => expand(one, resolved)))) {
709
+ generated.push({ dir: manifest.dir, row: row.name, below });
710
+ }
583
711
  for (const glob of new Set(row.entries.flatMap((entry) => expand(entry, resolved)))) {
584
712
  globs.push({
585
713
  dir: manifest.dir,
@@ -597,7 +725,27 @@ export function readConventions(
597
725
  compare(a.manifest, b.manifest) ||
598
726
  compare(JSON.stringify(a), JSON.stringify(b)),
599
727
  );
600
- return { applied: unique, globs, failures };
728
+ return { applied: unique, globs, generated, failures };
729
+ }
730
+
731
+ /**
732
+ * Every own file of one project below a directory an applied row names as generated, by
733
+ * its path below the target root, with the name of the first such row.
734
+ */
735
+ export function generatedFiles(
736
+ conventions: Conventions,
737
+ files: SourceFiles,
738
+ ): ReadonlyMap<string, string> {
739
+ const found = new Map<string, string>();
740
+ for (const { dir, row, below } of conventions.generated) {
741
+ const root = joinPath(dir, below);
742
+ for (const [path, file] of files.byPath) {
743
+ if (!found.has(path) && relativePath(root, file.fileName) !== undefined) {
744
+ found.set(path, row);
745
+ }
746
+ }
747
+ }
748
+ return found;
601
749
  }
602
750
 
603
751
  /** Every own file of one project an applied row's globs match, once per row. */