jev-agent-tools 0.1.3

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 (95) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/LICENSE +21 -0
  3. package/README.md +119 -0
  4. package/docs/design.md +187 -0
  5. package/docs/tools/jev_ask.md +92 -0
  6. package/docs/tools/jev_ask_files.md +80 -0
  7. package/docs/tools/jev_check_diff.md +77 -0
  8. package/docs/tools/jev_find_files.md +59 -0
  9. package/docs/tools/jev_locate_in_file.md +51 -0
  10. package/docs/tools/jev_select_tests.md +59 -0
  11. package/package.json +85 -0
  12. package/rules/jev-ask.md +4 -0
  13. package/src/adapters/analysis-context.ts +100 -0
  14. package/src/adapters/ask-files.ts +236 -0
  15. package/src/adapters/ask-proof.ts +202 -0
  16. package/src/adapters/ask-syntax.ts +463 -0
  17. package/src/adapters/command.ts +240 -0
  18. package/src/adapters/docs.ts +222 -0
  19. package/src/adapters/files.ts +411 -0
  20. package/src/adapters/find.ts +151 -0
  21. package/src/adapters/git-base.ts +32 -0
  22. package/src/adapters/git-inventory.ts +94 -0
  23. package/src/adapters/git.ts +525 -0
  24. package/src/adapters/locate-file.ts +197 -0
  25. package/src/adapters/output-lines.ts +50 -0
  26. package/src/adapters/risk-callers.ts +525 -0
  27. package/src/adapters/runner-version.ts +102 -0
  28. package/src/adapters/syntax.ts +229 -0
  29. package/src/adapters/test-inventory.ts +168 -0
  30. package/src/adapters/usage.ts +21 -0
  31. package/src/adapters/utf8.ts +57 -0
  32. package/src/constants.ts +109 -0
  33. package/src/core/ask-closure.ts +419 -0
  34. package/src/core/ask-proof.ts +32 -0
  35. package/src/core/ask-references.ts +249 -0
  36. package/src/core/asks.ts +616 -0
  37. package/src/core/batches.ts +83 -0
  38. package/src/core/command-output.ts +249 -0
  39. package/src/core/diff.ts +226 -0
  40. package/src/core/docs.ts +399 -0
  41. package/src/core/find.ts +157 -0
  42. package/src/core/git.ts +5 -0
  43. package/src/core/import-boundaries.ts +102 -0
  44. package/src/core/imports.ts +691 -0
  45. package/src/core/integrity.ts +64 -0
  46. package/src/core/lexical.ts +130 -0
  47. package/src/core/locate.ts +213 -0
  48. package/src/core/output.ts +264 -0
  49. package/src/core/pointer.ts +51 -0
  50. package/src/core/risk-callers.ts +1270 -0
  51. package/src/core/runner-version.ts +66 -0
  52. package/src/core/sections.ts +269 -0
  53. package/src/core/state.ts +53 -0
  54. package/src/core/syntax.ts +8 -0
  55. package/src/core/test-commands.ts +430 -0
  56. package/src/core/test-coverage.ts +103 -0
  57. package/src/core/test-discovery.ts +1695 -0
  58. package/src/core/test-evidence.ts +649 -0
  59. package/src/core/test-state.ts +99 -0
  60. package/src/core/truncate.ts +14 -0
  61. package/src/core/units.ts +531 -0
  62. package/src/describe.ts +26 -0
  63. package/src/guide.ts +42 -0
  64. package/src/host.ts +22 -0
  65. package/src/index.ts +40 -0
  66. package/src/jev/client.ts +505 -0
  67. package/src/jev/pool.ts +60 -0
  68. package/src/jev/types.ts +60 -0
  69. package/src/presets/docs.ts +85 -0
  70. package/src/presets/risk.ts +263 -0
  71. package/src/presets/spec.ts +111 -0
  72. package/src/presets/witnesses.ts +313 -0
  73. package/src/render.ts +45 -0
  74. package/src/result.ts +4 -0
  75. package/src/run-end.ts +157 -0
  76. package/src/runtime.ts +16 -0
  77. package/src/session.ts +106 -0
  78. package/src/texts/ask-files.ts +2 -0
  79. package/src/texts/ask.ts +4 -0
  80. package/src/texts/check-diff.ts +24 -0
  81. package/src/texts/configuration.ts +2 -0
  82. package/src/texts/find.ts +14 -0
  83. package/src/texts/guide.ts +16 -0
  84. package/src/texts/locate.ts +10 -0
  85. package/src/texts/run-end.ts +13 -0
  86. package/src/texts/select-tests.ts +3 -0
  87. package/src/tools/ask-files.ts +263 -0
  88. package/src/tools/ask-schema.ts +116 -0
  89. package/src/tools/ask.ts +925 -0
  90. package/src/tools/check-diff.ts +510 -0
  91. package/src/tools/docs-check.ts +399 -0
  92. package/src/tools/find.ts +529 -0
  93. package/src/tools/locate.ts +369 -0
  94. package/src/tools/select-tests.ts +746 -0
  95. package/src/tools/spec-check.ts +210 -0
@@ -0,0 +1,202 @@
1
+ import { relative, resolve } from "node:path";
2
+ import { STATE_MAX_CHARS, TIMEOUT_MS } from "../constants.ts";
3
+ import type { GitExec } from "../core/git.ts";
4
+ import type { ImportSource } from "../core/imports.ts";
5
+ import type { Result } from "../result.ts";
6
+ import { collectFiles } from "./files.ts";
7
+ import { verifyGitUtf8 } from "./utf8.ts";
8
+
9
+ export interface AskRepository {
10
+ root: string;
11
+ known: ReadonlySet<string>;
12
+ baseSha?: string;
13
+ read(path: string): Promise<Result<ImportSource>>;
14
+ readBefore(path: string): Promise<Result<{ text: string | null }>>;
15
+ }
16
+
17
+ export async function collectAskRepository(
18
+ exec: GitExec,
19
+ cwd: string,
20
+ base: string | undefined,
21
+ signal?: AbortSignal,
22
+ ): Promise<Result<AskRepository>> {
23
+ try {
24
+ const options = { cwd, timeout: TIMEOUT_MS, signal };
25
+ const location = await exec(
26
+ "git",
27
+ ["rev-parse", "--show-toplevel"],
28
+ options,
29
+ );
30
+ if (location.code !== 0 || location.killed)
31
+ return {
32
+ ok: false,
33
+ error: location.stderr || "Cannot resolve repository root.",
34
+ };
35
+ const root = location.stdout.trimEnd();
36
+ const gitOptions = { ...options, cwd: root };
37
+ const [listed, ignored, revision] = await Promise.all([
38
+ exec(
39
+ "git",
40
+ ["ls-files", "--cached", "--others", "--exclude-standard", "-z"],
41
+ gitOptions,
42
+ ),
43
+ exec(
44
+ "git",
45
+ ["ls-files", "--cached", "--ignored", "--exclude-standard", "-z"],
46
+ gitOptions,
47
+ ),
48
+ base === undefined
49
+ ? undefined
50
+ : exec(
51
+ "git",
52
+ ["rev-parse", "--verify", "--end-of-options", `${base}^{commit}`],
53
+ gitOptions,
54
+ ),
55
+ ]);
56
+ if (listed.code !== 0 || listed.killed)
57
+ return {
58
+ ok: false,
59
+ error: listed.stderr || "Cannot list repository paths.",
60
+ };
61
+ if (revision && (revision.code !== 0 || revision.killed))
62
+ return { ok: false, error: revision.stderr || "Cannot resolve base." };
63
+ if (ignored.code !== 0 || ignored.killed)
64
+ return {
65
+ ok: false,
66
+ error: ignored.stderr || "Cannot list ignored repository paths.",
67
+ };
68
+ const known = new Set(listed.stdout.split("\0").filter(Boolean));
69
+ for (const path of ignored.stdout.split("\0")) known.delete(path);
70
+ const baseSha = revision?.stdout.trim();
71
+ const basePaths = baseSha
72
+ ? await exec(
73
+ "git",
74
+ ["ls-tree", "-r", "--name-only", "-z", baseSha],
75
+ gitOptions,
76
+ )
77
+ : undefined;
78
+ if (basePaths && (basePaths.code !== 0 || basePaths.killed))
79
+ return {
80
+ ok: false,
81
+ error: basePaths.stderr || "Cannot list base paths.",
82
+ };
83
+ const beforeKnown = new Set(basePaths?.stdout.split("\0").filter(Boolean));
84
+ const reads = new Map<string, Promise<Result<ImportSource>>>();
85
+ const beforeReads = new Map<
86
+ string,
87
+ Promise<Result<{ text: string | null }>>
88
+ >();
89
+ return {
90
+ ok: true,
91
+ root,
92
+ known,
93
+ baseSha,
94
+ read(path) {
95
+ let pending = reads.get(path);
96
+ if (!pending) {
97
+ pending = (async () => {
98
+ if (!known.has(path))
99
+ return {
100
+ ok: false as const,
101
+ error: `${path}: not in repository inventory`,
102
+ };
103
+ const loaded = await collectFiles(root, [path], signal, {
104
+ allowEmpty: true,
105
+ exec,
106
+ inventory: known,
107
+ });
108
+ return loaded.ok
109
+ ? { ok: true as const, path, text: loaded.files[path] ?? "" }
110
+ : loaded;
111
+ })();
112
+ reads.set(path, pending);
113
+ }
114
+ return pending;
115
+ },
116
+ readBefore(path) {
117
+ let pending = beforeReads.get(path);
118
+ if (!pending) {
119
+ pending = (async () => {
120
+ if (!baseSha || !beforeKnown.has(path))
121
+ return { ok: true as const, text: null };
122
+ try {
123
+ const object = await exec(
124
+ "git",
125
+ [
126
+ "rev-parse",
127
+ "--verify",
128
+ "--end-of-options",
129
+ `${baseSha}:${path}`,
130
+ ],
131
+ gitOptions,
132
+ );
133
+ if (object.code !== 0 || object.killed)
134
+ return {
135
+ ok: false as const,
136
+ error: `before evidence unavailable for ${path}`,
137
+ };
138
+ const id = object.stdout.trim();
139
+ const sizeResult = await exec(
140
+ "git",
141
+ ["cat-file", "-s", id],
142
+ gitOptions,
143
+ );
144
+ const size = Number(sizeResult.stdout);
145
+ if (
146
+ sizeResult.code !== 0 ||
147
+ sizeResult.killed ||
148
+ !Number.isFinite(size) ||
149
+ size > STATE_MAX_CHARS * 4
150
+ )
151
+ return {
152
+ ok: false as const,
153
+ error: `before evidence unavailable for ${path}`,
154
+ };
155
+ const loaded = await exec(
156
+ "git",
157
+ [
158
+ "show",
159
+ "--no-ext-diff",
160
+ "--no-textconv",
161
+ "--end-of-options",
162
+ id,
163
+ ],
164
+ gitOptions,
165
+ );
166
+ if (
167
+ loaded.code !== 0 ||
168
+ loaded.killed ||
169
+ loaded.stdout.length > STATE_MAX_CHARS
170
+ )
171
+ return {
172
+ ok: false as const,
173
+ error: `before evidence unavailable for ${path}`,
174
+ };
175
+ const decoded = verifyGitUtf8(loaded.stdout, id, size);
176
+ return decoded.ok
177
+ ? decoded
178
+ : { ok: false as const, error: `${path}: ${decoded.error}` };
179
+ } catch (error) {
180
+ return { ok: false as const, error: String(error) };
181
+ }
182
+ })();
183
+ beforeReads.set(path, pending);
184
+ }
185
+ return pending;
186
+ },
187
+ };
188
+ } catch (error) {
189
+ return {
190
+ ok: false,
191
+ error: `Cannot collect repository proof: ${String(error)}`,
192
+ };
193
+ }
194
+ }
195
+
196
+ export function repositoryPath(
197
+ root: string,
198
+ cwd: string,
199
+ path: string,
200
+ ): string {
201
+ return relative(root, resolve(cwd, path)).split("\\").join("/");
202
+ }
@@ -0,0 +1,463 @@
1
+ import type { SgNode } from "@ast-grep/napi";
2
+ import type {
3
+ ProofBinding,
4
+ ProofDeclaration,
5
+ ProofSyntax,
6
+ } from "../core/ask-proof.ts";
7
+ import type { ImportSource } from "../core/imports.ts";
8
+ import type { Limitation } from "../core/output.ts";
9
+ import type { SyntaxRootParser } from "../core/syntax.ts";
10
+ import { loadSyntaxRootParser } from "./syntax.ts";
11
+
12
+ function nodes(root: SgNode, kinds: readonly string[]): SgNode[] {
13
+ const result: SgNode[] = [];
14
+ for (const kind of kinds) {
15
+ try {
16
+ result.push(...root.findAll({ rule: { kind } }));
17
+ } catch {
18
+ /* Node kinds differ across the supported language grammars. */
19
+ }
20
+ }
21
+ return result
22
+ .map((node) => ({ node, index: node.range().start.index }))
23
+ .sort((a, b) => a.index - b.index)
24
+ .map((entry) => entry.node);
25
+ }
26
+ function names(root: SgNode, includeTypes = true): string[] {
27
+ const result = new Set<string>();
28
+ const excluded = nodes(root, [
29
+ "import_statement",
30
+ "import_from_statement",
31
+ "export_clause",
32
+ ]).map((node) => node.range());
33
+ const typeScopes: { name: string; start: number; end: number }[] = [];
34
+ if (includeTypes) {
35
+ for (const binder of nodes(root, [
36
+ "type_parameter",
37
+ "infer_type",
38
+ "mapped_type_clause",
39
+ ])) {
40
+ const name =
41
+ binder.field("name") ??
42
+ binder.children().find((node) => node.kind() === "type_identifier");
43
+ let scope = binder.parent();
44
+ if (binder.kind() === "type_parameter") scope = scope?.parent() ?? null;
45
+ if (binder.kind() === "infer_type") {
46
+ while (scope && scope.kind() !== "conditional_type")
47
+ scope = scope.parent();
48
+ const own = name?.range();
49
+ if (name && own)
50
+ typeScopes.push({
51
+ name: name.text(),
52
+ start: own.start.index,
53
+ end: own.end.index,
54
+ });
55
+ scope = scope?.field("consequence") ?? null;
56
+ }
57
+ if (name && scope) {
58
+ const range = scope.range();
59
+ typeScopes.push({
60
+ name: name.text(),
61
+ start: range.start.index,
62
+ end: range.end.index,
63
+ });
64
+ }
65
+ }
66
+ }
67
+ for (const node of nodes(root, [
68
+ "identifier",
69
+ "type_identifier",
70
+ "nested_type_identifier",
71
+ "member_expression",
72
+ "attribute",
73
+ ])) {
74
+ const position = node.range().start.index;
75
+ if (node.kind() === "type_identifier") {
76
+ if (
77
+ !includeTypes ||
78
+ node.parent()?.kind() === "nested_type_identifier" ||
79
+ typeScopes.some(
80
+ (scope) =>
81
+ scope.name === node.text() &&
82
+ position >= scope.start &&
83
+ position < scope.end,
84
+ )
85
+ )
86
+ continue;
87
+ }
88
+ if (!includeTypes && node.kind() === "nested_type_identifier") continue;
89
+ if (
90
+ excluded.some(
91
+ (range) => position >= range.start.index && position < range.end.index,
92
+ )
93
+ )
94
+ continue;
95
+ if (node.kind() === "identifier" || node.kind() === "type_identifier")
96
+ result.add(node.text());
97
+ else {
98
+ const owner = node.field("object") ?? node.field("module");
99
+ const property =
100
+ node.field("property") ?? node.field("attribute") ?? node.field("name");
101
+ if (owner && property) result.add(`${owner.text()}.${property.text()}`);
102
+ }
103
+ }
104
+ return [...result];
105
+ }
106
+ function literal(node: SgNode | null | undefined): string | undefined {
107
+ if (!node || !["string", "string_literal"].includes(String(node.kind())))
108
+ return undefined;
109
+ const fragments = nodes(node, ["string_fragment", "string_content"]);
110
+ if (nodes(node, ["interpolation", "escape_sequence"]).length)
111
+ return undefined;
112
+ return fragments.map((fragment) => fragment.text()).join("");
113
+ }
114
+
115
+ /** Collect AST metadata only; repository resolution remains in the shared I8 graph. */
116
+ export async function collectProofSyntax(
117
+ files: readonly ImportSource[],
118
+ before?: Readonly<Record<string, string | null>>,
119
+ sourceParser?: SyntaxRootParser,
120
+ ): Promise<ProofSyntax> {
121
+ const declarations: ProofDeclaration[] = [];
122
+ const bindings: ProofBinding[] = [];
123
+ const limits: Limitation[] = [];
124
+ const uses = new Map<string, readonly string[]>();
125
+ const parser = sourceParser ?? (await loadSyntaxRootParser());
126
+ for (const file of files) {
127
+ if (!parser?.supports(file.path)) {
128
+ limits.push({
129
+ fact: `${file.path} : unsupported language${/\.[cm]?[jt]sx?$|\.py$/.test(file.path) ? ", optional parser unavailable" : ""}`,
130
+ next: "Install the language parser or provide the required declarations.",
131
+ });
132
+ continue;
133
+ }
134
+ const python = file.path.endsWith(".py");
135
+ const parsed = parser.parseRaw(file.path, file.text);
136
+ if (!parsed.ok) {
137
+ limits.push({
138
+ fact: `${file.path}: parse_failed`,
139
+ next: "read the complete source and check its syntax",
140
+ });
141
+ continue;
142
+ }
143
+ const root = parsed.root;
144
+ const cachedParser = Object.create(parser) as SyntaxRootParser;
145
+ cachedParser.parseRaw = () => parsed;
146
+ const syntax = cachedParser.declarations(file.path, file.text, {
147
+ descendAbove: Infinity,
148
+ });
149
+ for (const limit of syntax.limits) {
150
+ if (limit.kind === "parse_partial" && limit.declaration) continue;
151
+ limits.push({
152
+ fact: `${file.path} : ${limit.kind}`,
153
+ next: "check the complete supplied source",
154
+ });
155
+ }
156
+ if (syntax.limits.some((limit) => limit.kind === "parse_failed")) continue;
157
+ const usedNames = names(root);
158
+ const valueNames = names(root, false);
159
+ uses.set(file.path, usedNames);
160
+ const collectDeclarations = (tree: SgNode) => {
161
+ const result: { name: string; text: string; node: SgNode }[] = [];
162
+ for (const outer of tree.children()) {
163
+ const envelope = outer;
164
+ const node = ["export_statement", "decorated_definition"].includes(
165
+ String(outer.kind()),
166
+ )
167
+ ? (outer
168
+ .children()
169
+ .find((child) =>
170
+ [
171
+ "function_declaration",
172
+ "generator_function_declaration",
173
+ "class_declaration",
174
+ "lexical_declaration",
175
+ "variable_declaration",
176
+ "function_definition",
177
+ "class_definition",
178
+ "type_alias_declaration",
179
+ "interface_declaration",
180
+ "enum_declaration",
181
+ ].includes(String(child.kind())),
182
+ ) ?? outer)
183
+ : outer;
184
+ if (
185
+ [
186
+ "function_declaration",
187
+ "generator_function_declaration",
188
+ "class_declaration",
189
+ "function_definition",
190
+ "class_definition",
191
+ "type_alias_declaration",
192
+ "interface_declaration",
193
+ "enum_declaration",
194
+ ].includes(String(node.kind()))
195
+ ) {
196
+ const name = node.field("name")?.text();
197
+ if (name)
198
+ result.push({ name, text: envelope.text(), node: envelope });
199
+ } else if (
200
+ ["lexical_declaration", "variable_declaration"].includes(
201
+ String(node.kind()),
202
+ )
203
+ ) {
204
+ for (const variable of node
205
+ .children()
206
+ .filter((child) => child.kind() === "variable_declarator")) {
207
+ const name = variable.field("name")?.text();
208
+ if (name)
209
+ result.push({ name, text: envelope.text(), node: envelope });
210
+ }
211
+ } else if (python && node.kind() === "expression_statement") {
212
+ const assignment = node
213
+ .children()
214
+ .find((child) => child.kind() === "assignment");
215
+ const name = assignment?.field("left")?.text();
216
+ if (name)
217
+ result.push({ name, text: envelope.text(), node: envelope });
218
+ }
219
+ }
220
+ for (const declaration of result) {
221
+ if (!declaration.node.find({ rule: { kind: "ERROR" } })) continue;
222
+ const fact = `${file.path} : parse_partial (${declaration.name})`;
223
+ if (!limits.some((limit) => limit.fact === fact))
224
+ limits.push({
225
+ fact,
226
+ next: "check the complete supplied source declaration",
227
+ });
228
+ }
229
+ return result;
230
+ };
231
+ const old =
232
+ before && Object.hasOwn(before, file.path)
233
+ ? before[file.path]
234
+ : undefined;
235
+ const oldRoot =
236
+ typeof old === "string" ? parser.parseRaw(file.path, old) : undefined;
237
+ const oldDeclarations = oldRoot?.ok
238
+ ? collectDeclarations(oldRoot.root)
239
+ : [];
240
+ const currentDeclarations = collectDeclarations(root);
241
+ for (const declaration of currentDeclarations) {
242
+ const tree = declaration.node;
243
+ const decorators = nodes(tree, ["decorator"]);
244
+ declarations.push({
245
+ path: file.path,
246
+ name: declaration.name,
247
+ text: declaration.text,
248
+ ...(old !== undefined
249
+ ? {
250
+ before:
251
+ oldDeclarations.find((item) => item.name === declaration.name)
252
+ ?.text ?? null,
253
+ }
254
+ : {}),
255
+ usedNames: names(tree),
256
+ fixture:
257
+ python &&
258
+ decorators.some((node) =>
259
+ nodes(node, ["identifier"]).some(
260
+ (item) => item.text() === "fixture",
261
+ ),
262
+ ),
263
+ autouse:
264
+ python &&
265
+ decorators.some((node) =>
266
+ nodes(node, ["keyword_argument"]).some(
267
+ (item) =>
268
+ item.field("name")?.text() === "autouse" &&
269
+ item.field("value")?.text() === "True",
270
+ ),
271
+ ),
272
+ });
273
+ }
274
+ for (const declaration of oldDeclarations) {
275
+ if (!currentDeclarations.some((item) => item.name === declaration.name))
276
+ declarations.push({
277
+ path: file.path,
278
+ name: declaration.name,
279
+ text: "",
280
+ before: declaration.text,
281
+ usedNames: names(declaration.node),
282
+ deleted: true,
283
+ });
284
+ }
285
+ const add = (
286
+ local: string,
287
+ imported: string,
288
+ specifier: string,
289
+ reexport: boolean,
290
+ data = false,
291
+ ) =>
292
+ bindings.push({
293
+ path: file.path,
294
+ local,
295
+ imported,
296
+ specifier,
297
+ reexport,
298
+ used:
299
+ data ||
300
+ usedNames.includes(local) ||
301
+ usedNames.some((name) => name.startsWith(`${local}.`)),
302
+ ...(data ? { data: true } : {}),
303
+ typeOnly:
304
+ !data &&
305
+ !valueNames.some(
306
+ (name) => name === local || name.startsWith(`${local}.`),
307
+ ),
308
+ valueMembers: valueNames.filter((name) => name.startsWith(`${local}.`)),
309
+ });
310
+ for (const node of nodes(root, [
311
+ "import_statement",
312
+ "import_from_statement",
313
+ "export_statement",
314
+ ])) {
315
+ if (python) {
316
+ const children = node.children();
317
+ const module =
318
+ node.kind() === "import_from_statement"
319
+ ? children.find((child) =>
320
+ ["relative_import", "dotted_name"].includes(
321
+ String(child.kind()),
322
+ ),
323
+ )
324
+ : undefined;
325
+ for (const child of children) {
326
+ if (child === module) continue;
327
+ if (child.kind() === "aliased_import") {
328
+ const original =
329
+ child.field("name")?.text() ??
330
+ child
331
+ .children()
332
+ .find((item) => item.kind() === "dotted_name")
333
+ ?.text();
334
+ const alias =
335
+ child.field("alias")?.text() ??
336
+ child
337
+ .children()
338
+ .filter((item) => item.kind() === "identifier")
339
+ .at(-1)
340
+ ?.text();
341
+ if (original)
342
+ add(
343
+ alias ?? original,
344
+ module ? original : "*",
345
+ module?.text() ?? original,
346
+ file.path.endsWith("__init__.py"),
347
+ );
348
+ } else if (child.kind() === "dotted_name")
349
+ add(
350
+ child.text().split(".")[0] ?? child.text(),
351
+ module ? child.text() : "*",
352
+ module?.text() ?? child.text(),
353
+ file.path.endsWith("__init__.py"),
354
+ );
355
+ else if (child.kind() === "wildcard_import" && module)
356
+ add("*", "*", module.text(), file.path.endsWith("__init__.py"));
357
+ }
358
+ } else {
359
+ const specifier = literal(
360
+ node.field("source") ??
361
+ node.children().find((child) => child.kind() === "string"),
362
+ );
363
+ const reexport = node.kind() === "export_statement";
364
+ const specs = nodes(node, ["import_specifier", "export_specifier"]);
365
+ if (specifier) {
366
+ for (const spec of specs) {
367
+ const ids = spec
368
+ .children()
369
+ .filter((child) => child.kind() === "identifier");
370
+ const imported = spec.field("name")?.text() ?? ids[0]?.text();
371
+ const alias = spec.field("alias")?.text() ?? ids.at(-1)?.text();
372
+ if (imported) add(alias ?? imported, imported, specifier, reexport);
373
+ }
374
+ const clause = node
375
+ .children()
376
+ .find((child) => child.kind() === "import_clause");
377
+ const defaultName = clause
378
+ ?.children()
379
+ .find((child) => child.kind() === "identifier");
380
+ if (defaultName) add(defaultName.text(), "default", specifier, false);
381
+ const namespace = nodes(node, [
382
+ "namespace_import",
383
+ "namespace_export",
384
+ ])[0];
385
+ if (namespace)
386
+ add(
387
+ namespace
388
+ .children()
389
+ .find((child) => child.kind() === "identifier")
390
+ ?.text() ?? "*",
391
+ "*",
392
+ specifier,
393
+ reexport,
394
+ );
395
+ else if (
396
+ reexport &&
397
+ !specs.length &&
398
+ node.children().some((child) => child.text() === "*")
399
+ )
400
+ add("*", "*", specifier, true);
401
+ } else if (reexport) {
402
+ if (node.children().some((child) => child.kind() === "default")) {
403
+ const declaration = node
404
+ .children()
405
+ .find((child) => child.field("name") !== null);
406
+ const name = declaration?.field("name")?.text();
407
+ if (name) add("default", name, "", true);
408
+ }
409
+ for (const spec of specs) {
410
+ const ids = spec
411
+ .children()
412
+ .filter((child) => child.kind() === "identifier");
413
+ if (ids[0])
414
+ add(ids.at(-1)?.text() ?? ids[0].text(), ids[0].text(), "", true);
415
+ }
416
+ }
417
+ }
418
+ }
419
+ const readAliases = bindings
420
+ .filter(
421
+ (binding) =>
422
+ binding.path === file.path && binding.imported === "readFileSync",
423
+ )
424
+ .map((binding) => binding.local);
425
+ for (const call of nodes(root, python ? ["call"] : ["call_expression"])) {
426
+ const fn = call.field("function");
427
+ const fnName = fn?.text() ?? "";
428
+ if (
429
+ fnName === "readFileSync" ||
430
+ fnName.endsWith(".readFileSync") ||
431
+ readAliases.includes(fnName)
432
+ ) {
433
+ const argument = (
434
+ call.field("arguments") ??
435
+ call.children().find((child) => child.kind() === "arguments")
436
+ )
437
+ ?.children()
438
+ .find((child) => child.kind() === "string");
439
+ const specifier = literal(argument);
440
+ if (specifier !== undefined)
441
+ add(specifier, "*", specifier, false, true);
442
+ else
443
+ limits.push({
444
+ fact: `${file.path} : unresolved dynamic readFileSync call`,
445
+ next: "Provide the data file explicitly.",
446
+ });
447
+ }
448
+ if (["import", "__import__", "importlib.import_module"].includes(fnName))
449
+ limits.push({
450
+ fact: `${file.path} : unresolved dynamic import ${call.text()}`,
451
+ next: "Provide the dependency explicitly.",
452
+ });
453
+ }
454
+ }
455
+ return {
456
+ declarations,
457
+ bindings,
458
+ limits,
459
+ sources: files,
460
+ uses,
461
+ ...(before ? { before } : {}),
462
+ };
463
+ }