@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.
- package/README.md +1 -1
- package/dist/src/analysis.js +145 -39
- package/dist/src/configuration-files.js +108 -4
- package/dist/src/conformance-record.js +3 -9
- package/dist/src/contextual-uses.js +247 -0
- package/dist/src/convention-rows.js +6 -0
- package/dist/src/conventions.js +123 -14
- package/dist/src/dependencies.js +8 -2
- package/dist/src/destructuring.js +6 -6
- package/dist/src/entry-points.js +8 -5
- package/dist/src/exempt-classes.js +6 -1
- package/dist/src/exempt.js +35 -16
- package/dist/src/file-facts.js +23 -20
- package/dist/src/findings/completion.js +27 -3
- package/dist/src/findings/interfaces.js +6 -2
- package/dist/src/findings/reads-and-writes.js +53 -9
- package/dist/src/findings/unused-declarations.js +40 -1
- package/dist/src/generated-file.js +24 -0
- package/dist/src/graph.js +1 -1
- package/dist/src/interface-satisfaction.js +2 -68
- package/dist/src/inventory.js +17 -4
- package/dist/src/kinds.js +4 -3
- package/dist/src/manifest.js +29 -4
- package/dist/src/outside-tests.js +139 -0
- package/dist/src/overrides.js +104 -0
- package/dist/src/paths.js +17 -0
- package/dist/src/query.js +15 -0
- package/dist/src/references.js +229 -11
- package/dist/src/resolve.js +12 -11
- package/dist/src/roots.js +121 -37
- package/dist/src/session.js +4 -3
- package/dist/src/stores.js +3 -0
- package/dist/src/template-field.js +60 -3
- package/dist/src/value-flow.js +74 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +194 -49
- package/src/configuration-files.ts +137 -2
- package/src/conformance-record.ts +3 -10
- package/src/contextual-uses.ts +358 -0
- package/src/convention-rows.ts +8 -0
- package/src/conventions.ts +161 -13
- package/src/dependencies.ts +14 -1
- package/src/destructuring.ts +16 -6
- package/src/entry-points.ts +16 -3
- package/src/exempt-classes.ts +6 -1
- package/src/exempt.ts +67 -16
- package/src/file-facts.ts +26 -20
- package/src/findings/completion.ts +31 -3
- package/src/findings/interfaces.ts +9 -2
- package/src/findings/reads-and-writes.ts +70 -9
- package/src/findings/unused-declarations.ts +51 -1
- package/src/generated-file.ts +27 -0
- package/src/graph.ts +4 -4
- package/src/interface-satisfaction.ts +1 -92
- package/src/inventory.ts +19 -4
- package/src/kinds.ts +5 -3
- package/src/manifest.ts +40 -4
- package/src/outside-tests.ts +191 -0
- package/src/overrides.ts +136 -0
- package/src/paths.ts +18 -0
- package/src/query.ts +26 -0
- package/src/references.ts +294 -22
- package/src/resolve.ts +14 -13
- package/src/roots.ts +152 -44
- package/src/session.ts +4 -2
- package/src/stores.ts +9 -0
- package/src/template-field.ts +65 -3
- package/src/value-flow.ts +104 -0
- 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 {
|
|
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.
|
|
5
|
+
corpusVersion: "2.2.0",
|
|
6
6
|
result: "pass",
|
|
7
|
-
digest: "sha256:
|
|
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
|
+
}
|
package/src/convention-rows.ts
CHANGED
|
@@ -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",
|