@reviewflow/sdk 0.0.0-stage → 6.21.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.
- package/LICENSE +15 -0
- package/README.md +86 -2
- package/build/files-node.mjs +70 -0
- package/build/git-node.mjs +1792 -0
- package/build/index-browser.es.js +3338 -0
- package/build/index-node.mjs +3338 -0
- package/package.json +136 -4
- package/src/api/pullRequest.ts +617 -0
- package/src/config/insertCheckoutEntry.ts +54 -0
- package/src/diff/diffPaths.ts +78 -0
- package/src/diff/impacts.ts +47 -0
- package/src/diff/readingOrder.ts +428 -0
- package/src/diff/themes.ts +105 -0
- package/src/diff/types.ts +141 -0
- package/src/files/editableFile.ts +163 -0
- package/src/files.ts +13 -0
- package/src/git/groupDiffPaths.ts +134 -0
- package/src/git/manifests.ts +96 -0
- package/src/git/moduleBindings.ts +571 -0
- package/src/git/pullRequestDiff.ts +159 -0
- package/src/git/readBranchDiff.ts +359 -0
- package/src/git/readImpacts.ts +797 -0
- package/src/git/readingOrder.ts +190 -0
- package/src/git/run.ts +213 -0
- package/src/git.ts +38 -0
- package/src/github/applySuggestions.ts +399 -0
- package/src/github/editPullRequest.ts +790 -0
- package/src/github/fetchCompareDiff.ts +21 -0
- package/src/github/imageProxies.ts +43 -0
- package/src/github/lineDiff.ts +145 -0
- package/src/github/prQuery.ts +298 -0
- package/src/github/pullRequestDetail.ts +1347 -0
- package/src/github/pullRequestHead.ts +65 -0
- package/src/github/pullRequestRevisions.ts +124 -0
- package/src/github/transport.ts +171 -0
- package/src/index.ts +35 -0
- package/src/reviewflow/board.ts +333 -0
- package/src/reviewflow/changedParts.ts +80 -0
- package/src/reviewflow/checksFromReviewflow.ts +75 -0
- package/src/reviewflow/client.ts +589 -0
- package/src/reviewflow/follow.ts +345 -0
- package/src/reviewflow/status.ts +17 -0
- package/src/reviewflow/stream.ts +199 -0
- package/src/types/entities.ts +8 -0
- package/src/types/ids.ts +1 -0
- package/src/types/reviewflow.ts +32 -0
- package/src/utils/pullRequestEdit.ts +108 -0
- package/src/utils/pullRequestPart.ts +12 -0
- package/src/utils/pullRequestReaction.ts +19 -0
- package/src/utils/pullRequestSuggestion.ts +148 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
import { parseSync } from "oxc-parser";
|
|
2
|
+
import type { ParseResult, StaticExportEntry } from "oxc-parser";
|
|
3
|
+
|
|
4
|
+
/** Lines of a source, 1-based and inclusive. */
|
|
5
|
+
export interface LineRange {
|
|
6
|
+
start: number;
|
|
7
|
+
end: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ExportedBinding {
|
|
11
|
+
/** `default` for a default export */
|
|
12
|
+
name: string;
|
|
13
|
+
/** the local name it exports, null for an expression (`export default 5`) */
|
|
14
|
+
local: string | null;
|
|
15
|
+
/** the export statement; for `export { x }`, that line, not where `x` is declared */
|
|
16
|
+
lines: LineRange;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A module passing names of another one on: `export { x as y } from`
|
|
21
|
+
* (`local` x, `exported` y), `export * from` (both `*`), `export * as ns from`
|
|
22
|
+
* (`local` `*`, `exported` ns), and the barrel form `import { a } from "./x";
|
|
23
|
+
* export { a }`.
|
|
24
|
+
*/
|
|
25
|
+
export interface ReExport {
|
|
26
|
+
specifier: string;
|
|
27
|
+
/** the name in the other module, `*` for all of them */
|
|
28
|
+
local: string;
|
|
29
|
+
/** the name given to it here, `*` when every name keeps its own */
|
|
30
|
+
exported: string;
|
|
31
|
+
lines: LineRange;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ExportedBindings {
|
|
35
|
+
bindings: ExportedBinding[];
|
|
36
|
+
reExports: ReExport[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ImportedBinding {
|
|
40
|
+
specifier: string;
|
|
41
|
+
/** names in the other module, `default` included; `*` for the whole module */
|
|
42
|
+
names: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const stringLiteral = /^(["'`])(.*)\1$/s;
|
|
46
|
+
|
|
47
|
+
const lineStartsOf = (source: string): number[] => {
|
|
48
|
+
const starts = [0];
|
|
49
|
+
for (let index = source.indexOf("\n"); index !== -1;) {
|
|
50
|
+
starts.push(index + 1);
|
|
51
|
+
index = source.indexOf("\n", index + 1);
|
|
52
|
+
}
|
|
53
|
+
return starts;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** The 1-based line holding the offset: the last line start at or before it. */
|
|
57
|
+
const lineOf = (starts: readonly number[], offset: number): number => {
|
|
58
|
+
let low = 0;
|
|
59
|
+
let high = starts.length - 1;
|
|
60
|
+
while (low < high) {
|
|
61
|
+
const middle = Math.ceil((low + high) / 2);
|
|
62
|
+
if (starts[middle]! <= offset) low = middle;
|
|
63
|
+
else high = middle - 1;
|
|
64
|
+
}
|
|
65
|
+
return low + 1;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const rangeOf = (
|
|
69
|
+
starts: readonly number[],
|
|
70
|
+
{ start, end }: { start: number; end: number },
|
|
71
|
+
): LineRange => ({
|
|
72
|
+
start: lineOf(starts, start),
|
|
73
|
+
end: lineOf(starts, Math.max(start, end - 1)),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const overlaps = (left: LineRange, right: LineRange): boolean =>
|
|
77
|
+
left.start <= right.end && right.start <= left.end;
|
|
78
|
+
|
|
79
|
+
interface NameOf {
|
|
80
|
+
kind: unknown;
|
|
81
|
+
name: string | null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const kindOf = ({ kind }: NameOf): string => String(kind);
|
|
85
|
+
|
|
86
|
+
/** The name of an entry; `default` for the `export default` and the `import x` forms. */
|
|
87
|
+
const nameOf = (entry: NameOf): string | null =>
|
|
88
|
+
kindOf(entry) === "Default" ? "default" : entry.name;
|
|
89
|
+
|
|
90
|
+
interface Parsed {
|
|
91
|
+
result: ParseResult;
|
|
92
|
+
starts: number[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const parse = (path: string, source: string): Parsed => ({
|
|
96
|
+
result: parseSync(path, source),
|
|
97
|
+
starts: lineStartsOf(source),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
interface ImportedName {
|
|
101
|
+
specifier: string;
|
|
102
|
+
imported: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const importedNames = ({ result }: Parsed): Map<string, ImportedName> => {
|
|
106
|
+
const names = new Map<string, ImportedName>();
|
|
107
|
+
for (const { moduleRequest, entries } of result.module.staticImports) {
|
|
108
|
+
for (const { importName, localName } of entries) {
|
|
109
|
+
names.set(localName.value, {
|
|
110
|
+
specifier: moduleRequest.value,
|
|
111
|
+
imported:
|
|
112
|
+
kindOf(importName) === "NamespaceObject"
|
|
113
|
+
? "*"
|
|
114
|
+
: (nameOf(importName) ?? "*"),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return names;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
interface MatchedImport {
|
|
122
|
+
/** the name in the other module */
|
|
123
|
+
name: string;
|
|
124
|
+
/** the local binding of the import it is passed on from */
|
|
125
|
+
local: string | null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The import a barrel-form re-export (`import d from "./d"; export { d }`)
|
|
130
|
+
* passes on. For a default import the parser reports the local name as the
|
|
131
|
+
* imported one, where the module's export is `default`.
|
|
132
|
+
*/
|
|
133
|
+
const matchImport = (
|
|
134
|
+
{ result }: Parsed,
|
|
135
|
+
{ importName, moduleRequest }: StaticExportEntry,
|
|
136
|
+
): MatchedImport => {
|
|
137
|
+
const name = nameOf(importName) ?? "*";
|
|
138
|
+
const statement = result.module.staticImports.find(
|
|
139
|
+
({ moduleRequest: request }) => request.start === moduleRequest?.start,
|
|
140
|
+
);
|
|
141
|
+
const entries = statement?.entries ?? [];
|
|
142
|
+
const asDefault = entries.find(
|
|
143
|
+
(entry) =>
|
|
144
|
+
kindOf(entry.importName) === "Default" && entry.localName.value === name,
|
|
145
|
+
);
|
|
146
|
+
if (asDefault !== undefined) {
|
|
147
|
+
return { name: "default", local: asDefault.localName.value };
|
|
148
|
+
}
|
|
149
|
+
const named = entries.find((entry) => entry.importName.name === name);
|
|
150
|
+
return { name, local: named?.localName.value ?? null };
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
interface AstNode {
|
|
154
|
+
type: string;
|
|
155
|
+
start: number;
|
|
156
|
+
end: number;
|
|
157
|
+
[key: string]: unknown;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const isNode = (value: unknown): value is AstNode =>
|
|
161
|
+
typeof value === "object" &&
|
|
162
|
+
value !== null &&
|
|
163
|
+
typeof (value as { type?: unknown }).type === "string";
|
|
164
|
+
|
|
165
|
+
/** `export { a }`, `export { a } from "./x"`: no declaration of its own. */
|
|
166
|
+
const isExportList = (node: AstNode): boolean =>
|
|
167
|
+
node.type === "ExportNamedDeclaration" && node.declaration === null;
|
|
168
|
+
|
|
169
|
+
/** The children whose identifier is a label, not a use of a name. */
|
|
170
|
+
const labelKeys: Readonly<Record<string, string>> = {
|
|
171
|
+
MemberExpression: "property",
|
|
172
|
+
Property: "key",
|
|
173
|
+
PropertyDefinition: "key",
|
|
174
|
+
MethodDefinition: "key",
|
|
175
|
+
TSPropertySignature: "key",
|
|
176
|
+
TSMethodSignature: "key",
|
|
177
|
+
TSQualifiedName: "right",
|
|
178
|
+
JSXMemberExpression: "property",
|
|
179
|
+
JSXAttribute: "name",
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Every name a subtree mentions, shadowing ignored: a local variable named
|
|
184
|
+
* like a helper counts as a use of it, so what is reported is a superset.
|
|
185
|
+
*/
|
|
186
|
+
const collectNames = (root: unknown, names: Set<string>): void => {
|
|
187
|
+
const stack: unknown[] = [root];
|
|
188
|
+
while (stack.length > 0) {
|
|
189
|
+
const value = stack.pop();
|
|
190
|
+
if (Array.isArray(value)) {
|
|
191
|
+
stack.push(...(value as unknown[]));
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (!isNode(value)) continue;
|
|
195
|
+
if (value.type === "Identifier" || value.type === "JSXIdentifier") {
|
|
196
|
+
if (typeof value.name === "string") names.add(value.name);
|
|
197
|
+
stack.push(value.typeAnnotation);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const labelKey = labelKeys[value.type];
|
|
201
|
+
for (const [key, child] of Object.entries(value)) {
|
|
202
|
+
if (key === labelKey && value.computed !== true) continue;
|
|
203
|
+
if (typeof child === "object" && child !== null) stack.push(child);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Every name mentioned outside the imports and the export statements that
|
|
210
|
+
* only list names (`export { a }`, `export { a } from`, `export *`): what the
|
|
211
|
+
* file uses itself.
|
|
212
|
+
*/
|
|
213
|
+
const namesUsedLocally = ({ result }: Parsed): Set<string> => {
|
|
214
|
+
const names = new Set<string>();
|
|
215
|
+
for (const node of result.program.body as unknown as AstNode[]) {
|
|
216
|
+
const isListing =
|
|
217
|
+
node.type === "ImportDeclaration" ||
|
|
218
|
+
node.type === "ExportAllDeclaration" ||
|
|
219
|
+
isExportList(node);
|
|
220
|
+
if (!isListing) collectNames(node, names);
|
|
221
|
+
}
|
|
222
|
+
return names;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
interface Bindings extends ExportedBindings {
|
|
226
|
+
/** local bindings of imports that are only passed on by a re-export */
|
|
227
|
+
passedOn: Set<string>;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const readBindings = (parsed: Parsed): Bindings => {
|
|
231
|
+
const { result, starts } = parsed;
|
|
232
|
+
const imported = importedNames(parsed);
|
|
233
|
+
const bindings: ExportedBinding[] = [];
|
|
234
|
+
const reExports: ReExport[] = [];
|
|
235
|
+
const passedOn = new Set<string>();
|
|
236
|
+
for (const { start, end, entries } of result.module.staticExports) {
|
|
237
|
+
for (const entry of entries) {
|
|
238
|
+
const exported = nameOf(entry.exportName);
|
|
239
|
+
const inStatement =
|
|
240
|
+
entry.exportName.start === null ||
|
|
241
|
+
(entry.exportName.start >= start && entry.exportName.start < end);
|
|
242
|
+
const isListed = !inStatement || (entries.length > 1 && entry.end < end);
|
|
243
|
+
const lines = rangeOf(
|
|
244
|
+
starts,
|
|
245
|
+
isListed ? { start: entry.start, end: entry.end } : { start, end },
|
|
246
|
+
);
|
|
247
|
+
const importKind = kindOf(entry.importName);
|
|
248
|
+
if (entry.moduleRequest !== null && importKind !== "None") {
|
|
249
|
+
const { value: specifier } = entry.moduleRequest;
|
|
250
|
+
if (importKind === "AllButDefault") {
|
|
251
|
+
reExports.push({ specifier, local: "*", exported: "*", lines });
|
|
252
|
+
} else if (exported !== null) {
|
|
253
|
+
const matched = matchImport(parsed, entry);
|
|
254
|
+
if (matched.local !== null && !inStatement) {
|
|
255
|
+
passedOn.add(matched.local);
|
|
256
|
+
}
|
|
257
|
+
reExports.push({
|
|
258
|
+
specifier,
|
|
259
|
+
local: importKind === "All" ? "*" : matched.name,
|
|
260
|
+
exported,
|
|
261
|
+
lines,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (exported === null) continue;
|
|
267
|
+
const local = entry.localName.name;
|
|
268
|
+
const viaImport = local === null ? undefined : imported.get(local);
|
|
269
|
+
if (viaImport === undefined) {
|
|
270
|
+
bindings.push({ name: exported, local, lines });
|
|
271
|
+
} else {
|
|
272
|
+
passedOn.add(local!);
|
|
273
|
+
reExports.push({
|
|
274
|
+
specifier: viaImport.specifier,
|
|
275
|
+
local: viaImport.imported,
|
|
276
|
+
exported,
|
|
277
|
+
lines,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return { bindings, reExports, passedOn };
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* What a module exports: its own declarations, apart from what it passes on
|
|
287
|
+
* from other modules (`reExports`), which are edges to follow, not declarations.
|
|
288
|
+
* `export *` carries no name of its own. A syntax error yields whatever was
|
|
289
|
+
* parsed before it.
|
|
290
|
+
*/
|
|
291
|
+
export const exportedBindings = (
|
|
292
|
+
path: string,
|
|
293
|
+
source: string,
|
|
294
|
+
): ExportedBindings => {
|
|
295
|
+
const { bindings, reExports } = readBindings(parse(path, source));
|
|
296
|
+
return { bindings, reExports };
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
/** The specifiers of the `export *` of a module. */
|
|
300
|
+
export const starSpecifiers = (reExports: readonly ReExport[]): string[] =>
|
|
301
|
+
reExports
|
|
302
|
+
.filter(({ local, exported }) => local === "*" && exported === "*")
|
|
303
|
+
.map(({ specifier }) => specifier);
|
|
304
|
+
|
|
305
|
+
/** Every name a module exports, its re-exports by their new name; `export *` carries none. */
|
|
306
|
+
export const exportedNames = ({
|
|
307
|
+
bindings,
|
|
308
|
+
reExports,
|
|
309
|
+
}: ExportedBindings): string[] => [
|
|
310
|
+
...new Set([
|
|
311
|
+
...bindings.map(({ name }) => name),
|
|
312
|
+
...reExports.flatMap(({ exported }) =>
|
|
313
|
+
exported === "*" ? [] : [exported],
|
|
314
|
+
),
|
|
315
|
+
]),
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
const importsOf = (
|
|
319
|
+
parsed: Parsed,
|
|
320
|
+
source: string,
|
|
321
|
+
passedOn: ReadonlySet<string>,
|
|
322
|
+
): ImportedBinding[] => {
|
|
323
|
+
const { result } = parsed;
|
|
324
|
+
const { staticImports, dynamicImports } = result.module;
|
|
325
|
+
const usedLocally = namesUsedLocally(parsed);
|
|
326
|
+
const found: { start: number; specifier: string; names: string[] }[] = [
|
|
327
|
+
...staticImports.flatMap(({ start, moduleRequest, entries }) => {
|
|
328
|
+
const used = entries.filter(
|
|
329
|
+
({ localName }) =>
|
|
330
|
+
!passedOn.has(localName.value) || usedLocally.has(localName.value),
|
|
331
|
+
);
|
|
332
|
+
if (entries.length > 0 && used.length === 0) return [];
|
|
333
|
+
const names =
|
|
334
|
+
entries.length === 0
|
|
335
|
+
? ["*"]
|
|
336
|
+
: used.map(({ importName }) =>
|
|
337
|
+
kindOf(importName) === "NamespaceObject"
|
|
338
|
+
? "*"
|
|
339
|
+
: (nameOf(importName) ?? "*"),
|
|
340
|
+
);
|
|
341
|
+
return [{ start, specifier: moduleRequest.value, names }];
|
|
342
|
+
}),
|
|
343
|
+
...dynamicImports.flatMap(({ start, moduleRequest }) => {
|
|
344
|
+
const literal = stringLiteral.exec(
|
|
345
|
+
source.slice(moduleRequest.start, moduleRequest.end),
|
|
346
|
+
);
|
|
347
|
+
const specifier = literal?.[2];
|
|
348
|
+
return specifier === undefined || specifier.includes("${")
|
|
349
|
+
? []
|
|
350
|
+
: [{ start, specifier, names: ["*"] }];
|
|
351
|
+
}),
|
|
352
|
+
];
|
|
353
|
+
const bySpecifier = new Map<string, Set<string>>();
|
|
354
|
+
for (const { specifier, names } of found.toSorted(
|
|
355
|
+
(left, right) => left.start - right.start,
|
|
356
|
+
)) {
|
|
357
|
+
const merged = bySpecifier.get(specifier) ?? new Set<string>();
|
|
358
|
+
for (const name of names) merged.add(name);
|
|
359
|
+
bySpecifier.set(specifier, merged);
|
|
360
|
+
}
|
|
361
|
+
return [...bySpecifier].map(([specifier, names]) => ({
|
|
362
|
+
specifier,
|
|
363
|
+
names: [...names],
|
|
364
|
+
}));
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* What a module imports, per specifier, in source order: `import type`
|
|
369
|
+
* included, a dynamic `import()` only when its argument is a plain string.
|
|
370
|
+
* A namespace, a dynamic and a side-effect import use the whole module (`*`).
|
|
371
|
+
* A re-export is not an import: see `exportedBindings`. Neither is an import
|
|
372
|
+
* only passed on by `export { a }` (the barrel form); one the file also uses
|
|
373
|
+
* outside its export lists stays an import. Uses are read by name, shadowing
|
|
374
|
+
* ignored: a local variable named like the import counts as a use.
|
|
375
|
+
*/
|
|
376
|
+
export const importedBindings = (
|
|
377
|
+
path: string,
|
|
378
|
+
source: string,
|
|
379
|
+
): ImportedBinding[] => {
|
|
380
|
+
const parsed = parse(path, source);
|
|
381
|
+
return importsOf(parsed, source, readBindings(parsed).passedOn);
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
export interface ModuleBindings extends ExportedBindings {
|
|
385
|
+
imports: ImportedBinding[];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** `exportedBindings` and `importedBindings` of one parse. */
|
|
389
|
+
export const moduleBindings = (
|
|
390
|
+
path: string,
|
|
391
|
+
source: string,
|
|
392
|
+
): ModuleBindings => {
|
|
393
|
+
const parsed = parse(path, source);
|
|
394
|
+
const { bindings, reExports, passedOn } = readBindings(parsed);
|
|
395
|
+
return { bindings, reExports, imports: importsOf(parsed, source, passedOn) };
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
/** The names a pattern binds: `a`, `{ a, b: [c] }`, `[a = 1, ...rest]`. */
|
|
399
|
+
const patternNames = (pattern: unknown, names: string[]): void => {
|
|
400
|
+
if (!isNode(pattern)) return;
|
|
401
|
+
switch (pattern.type) {
|
|
402
|
+
case "Identifier":
|
|
403
|
+
names.push(String(pattern.name));
|
|
404
|
+
break;
|
|
405
|
+
case "ObjectPattern":
|
|
406
|
+
for (const property of pattern.properties as unknown[]) {
|
|
407
|
+
if (isNode(property)) {
|
|
408
|
+
patternNames(property.value ?? property.argument, names);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
break;
|
|
412
|
+
case "ArrayPattern":
|
|
413
|
+
for (const element of pattern.elements as unknown[]) {
|
|
414
|
+
patternNames(element, names);
|
|
415
|
+
}
|
|
416
|
+
break;
|
|
417
|
+
case "AssignmentPattern":
|
|
418
|
+
patternNames(pattern.left, names);
|
|
419
|
+
break;
|
|
420
|
+
case "RestElement":
|
|
421
|
+
patternNames(pattern.argument, names);
|
|
422
|
+
break;
|
|
423
|
+
default:
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const declaredNamesOf = (statement: AstNode): string[] => {
|
|
428
|
+
const names: string[] = [];
|
|
429
|
+
if (statement.type === "VariableDeclaration") {
|
|
430
|
+
for (const declarator of statement.declarations as unknown[]) {
|
|
431
|
+
if (isNode(declarator)) patternNames(declarator.id, names);
|
|
432
|
+
}
|
|
433
|
+
} else if (
|
|
434
|
+
statement.type === "FunctionDeclaration" ||
|
|
435
|
+
statement.type === "ClassDeclaration" ||
|
|
436
|
+
statement.type.startsWith("TS")
|
|
437
|
+
) {
|
|
438
|
+
patternNames(statement.id, names);
|
|
439
|
+
}
|
|
440
|
+
return names;
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
interface Statement {
|
|
444
|
+
lines: LineRange;
|
|
445
|
+
/** `isExportList`: its names have lines of their own */
|
|
446
|
+
isList: boolean;
|
|
447
|
+
/** declared by a statement that is not an export */
|
|
448
|
+
declared: string[];
|
|
449
|
+
/** every name the statement mentions */
|
|
450
|
+
mentioned: Set<string>;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const statementsOf = ({ result, starts }: Parsed): Statement[] =>
|
|
454
|
+
(result.program.body as unknown as AstNode[]).map((node) => {
|
|
455
|
+
const mentioned = new Set<string>();
|
|
456
|
+
collectNames(node, mentioned);
|
|
457
|
+
return {
|
|
458
|
+
lines: rangeOf(starts, node),
|
|
459
|
+
isList: isExportList(node),
|
|
460
|
+
declared: declaredNamesOf(node),
|
|
461
|
+
mentioned,
|
|
462
|
+
};
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
export interface TouchedExportsParams {
|
|
466
|
+
path: string;
|
|
467
|
+
/** the file as it is after the change */
|
|
468
|
+
source: string;
|
|
469
|
+
/** the lines of `source` the change touches; a deletion touches the line it sits before */
|
|
470
|
+
touchedLines: readonly LineRange[];
|
|
471
|
+
/** the names the file exported before the change */
|
|
472
|
+
baseExports: readonly string[];
|
|
473
|
+
/** the specifiers of the `export *` the file had before the change */
|
|
474
|
+
baseStars?: readonly string[];
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export interface TouchedExports {
|
|
478
|
+
/** exported now, whose declaration the change touches */
|
|
479
|
+
touched: string[];
|
|
480
|
+
/** exported before the change, not any more */
|
|
481
|
+
removed: string[];
|
|
482
|
+
/** the specifiers of the `export *` on a touched line: which names they pass on is told by the other module */
|
|
483
|
+
touchedStars: string[];
|
|
484
|
+
/** the specifiers of `baseStars` the file no longer has */
|
|
485
|
+
removedStars: string[];
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* The exports a change touches: those declared on a touched line; those
|
|
490
|
+
* declared by a statement mentioning, directly or through another one, a
|
|
491
|
+
* touched declaration that is not exported (a helper); a touched re-export
|
|
492
|
+
* line's; and the names of `baseExports` no longer exported.
|
|
493
|
+
*
|
|
494
|
+
* Mentions are read by name, shadowing ignored: a superset of the real uses.
|
|
495
|
+
* A helper reached through something built at runtime is not followed. An
|
|
496
|
+
* `export *` touches no name here, as which names it passes on is told by the
|
|
497
|
+
* other module: its specifier is returned in `touchedStars` or `removedStars`.
|
|
498
|
+
*/
|
|
499
|
+
export const touchedExports = ({
|
|
500
|
+
path,
|
|
501
|
+
source,
|
|
502
|
+
touchedLines,
|
|
503
|
+
baseExports,
|
|
504
|
+
baseStars = [],
|
|
505
|
+
}: TouchedExportsParams): TouchedExports => {
|
|
506
|
+
const parsed = parse(path, source);
|
|
507
|
+
const { bindings, reExports } = readBindings(parsed);
|
|
508
|
+
const statements = statementsOf(parsed);
|
|
509
|
+
const isTouched = (lines: LineRange): boolean =>
|
|
510
|
+
touchedLines.some((touched) => overlaps(touched, lines));
|
|
511
|
+
|
|
512
|
+
const exportedLocals = new Set(
|
|
513
|
+
bindings.flatMap(({ local }) => (local === null ? [] : [local])),
|
|
514
|
+
);
|
|
515
|
+
const helpers = statements.filter(
|
|
516
|
+
({ declared }) =>
|
|
517
|
+
declared.length > 0 && !declared.some((name) => exportedLocals.has(name)),
|
|
518
|
+
);
|
|
519
|
+
const touchedHelperNames = new Set(
|
|
520
|
+
helpers
|
|
521
|
+
.filter(({ lines }) => isTouched(lines))
|
|
522
|
+
.flatMap(({ declared }) => declared),
|
|
523
|
+
);
|
|
524
|
+
for (let grown = true; grown;) {
|
|
525
|
+
grown = false;
|
|
526
|
+
for (const { declared, mentioned } of helpers) {
|
|
527
|
+
if (declared.every((name) => touchedHelperNames.has(name))) continue;
|
|
528
|
+
if (![...touchedHelperNames].some((name) => mentioned.has(name))) {
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
for (const name of declared) touchedHelperNames.add(name);
|
|
532
|
+
grown = true;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const isBindingTouched = ({ local, lines }: ExportedBinding): boolean => {
|
|
537
|
+
const declaring = [
|
|
538
|
+
...statements.filter(
|
|
539
|
+
(statement) => !statement.isList && overlaps(statement.lines, lines),
|
|
540
|
+
),
|
|
541
|
+
...(local === null
|
|
542
|
+
? []
|
|
543
|
+
: statements.filter(({ declared }) => declared.includes(local))),
|
|
544
|
+
];
|
|
545
|
+
return (
|
|
546
|
+
isTouched(lines) ||
|
|
547
|
+
declaring.some(
|
|
548
|
+
({ lines: declaringLines, mentioned }) =>
|
|
549
|
+
isTouched(declaringLines) ||
|
|
550
|
+
[...touchedHelperNames].some((name) => mentioned.has(name)),
|
|
551
|
+
)
|
|
552
|
+
);
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
const touched = new Set([
|
|
556
|
+
...bindings.filter(isBindingTouched).map(({ name }) => name),
|
|
557
|
+
...reExports
|
|
558
|
+
.filter(({ exported, lines }) => exported !== "*" && isTouched(lines))
|
|
559
|
+
.map(({ exported }) => exported),
|
|
560
|
+
]);
|
|
561
|
+
const now = new Set(exportedNames({ bindings, reExports }));
|
|
562
|
+
const stars = starSpecifiers(reExports);
|
|
563
|
+
return {
|
|
564
|
+
touched: [...touched],
|
|
565
|
+
removed: baseExports.filter((name) => !now.has(name)),
|
|
566
|
+
touchedStars: starSpecifiers(
|
|
567
|
+
reExports.filter(({ lines }) => isTouched(lines)),
|
|
568
|
+
),
|
|
569
|
+
removedStars: baseStars.filter((specifier) => !stars.includes(specifier)),
|
|
570
|
+
};
|
|
571
|
+
};
|