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.
- package/CHANGELOG.md +66 -0
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/docs/design.md +187 -0
- package/docs/tools/jev_ask.md +92 -0
- package/docs/tools/jev_ask_files.md +80 -0
- package/docs/tools/jev_check_diff.md +77 -0
- package/docs/tools/jev_find_files.md +59 -0
- package/docs/tools/jev_locate_in_file.md +51 -0
- package/docs/tools/jev_select_tests.md +59 -0
- package/package.json +85 -0
- package/rules/jev-ask.md +4 -0
- package/src/adapters/analysis-context.ts +100 -0
- package/src/adapters/ask-files.ts +236 -0
- package/src/adapters/ask-proof.ts +202 -0
- package/src/adapters/ask-syntax.ts +463 -0
- package/src/adapters/command.ts +240 -0
- package/src/adapters/docs.ts +222 -0
- package/src/adapters/files.ts +411 -0
- package/src/adapters/find.ts +151 -0
- package/src/adapters/git-base.ts +32 -0
- package/src/adapters/git-inventory.ts +94 -0
- package/src/adapters/git.ts +525 -0
- package/src/adapters/locate-file.ts +197 -0
- package/src/adapters/output-lines.ts +50 -0
- package/src/adapters/risk-callers.ts +525 -0
- package/src/adapters/runner-version.ts +102 -0
- package/src/adapters/syntax.ts +229 -0
- package/src/adapters/test-inventory.ts +168 -0
- package/src/adapters/usage.ts +21 -0
- package/src/adapters/utf8.ts +57 -0
- package/src/constants.ts +109 -0
- package/src/core/ask-closure.ts +419 -0
- package/src/core/ask-proof.ts +32 -0
- package/src/core/ask-references.ts +249 -0
- package/src/core/asks.ts +616 -0
- package/src/core/batches.ts +83 -0
- package/src/core/command-output.ts +249 -0
- package/src/core/diff.ts +226 -0
- package/src/core/docs.ts +399 -0
- package/src/core/find.ts +157 -0
- package/src/core/git.ts +5 -0
- package/src/core/import-boundaries.ts +102 -0
- package/src/core/imports.ts +691 -0
- package/src/core/integrity.ts +64 -0
- package/src/core/lexical.ts +130 -0
- package/src/core/locate.ts +213 -0
- package/src/core/output.ts +264 -0
- package/src/core/pointer.ts +51 -0
- package/src/core/risk-callers.ts +1270 -0
- package/src/core/runner-version.ts +66 -0
- package/src/core/sections.ts +269 -0
- package/src/core/state.ts +53 -0
- package/src/core/syntax.ts +8 -0
- package/src/core/test-commands.ts +430 -0
- package/src/core/test-coverage.ts +103 -0
- package/src/core/test-discovery.ts +1695 -0
- package/src/core/test-evidence.ts +649 -0
- package/src/core/test-state.ts +99 -0
- package/src/core/truncate.ts +14 -0
- package/src/core/units.ts +531 -0
- package/src/describe.ts +26 -0
- package/src/guide.ts +42 -0
- package/src/host.ts +22 -0
- package/src/index.ts +40 -0
- package/src/jev/client.ts +505 -0
- package/src/jev/pool.ts +60 -0
- package/src/jev/types.ts +60 -0
- package/src/presets/docs.ts +85 -0
- package/src/presets/risk.ts +263 -0
- package/src/presets/spec.ts +111 -0
- package/src/presets/witnesses.ts +313 -0
- package/src/render.ts +45 -0
- package/src/result.ts +4 -0
- package/src/run-end.ts +157 -0
- package/src/runtime.ts +16 -0
- package/src/session.ts +106 -0
- package/src/texts/ask-files.ts +2 -0
- package/src/texts/ask.ts +4 -0
- package/src/texts/check-diff.ts +24 -0
- package/src/texts/configuration.ts +2 -0
- package/src/texts/find.ts +14 -0
- package/src/texts/guide.ts +16 -0
- package/src/texts/locate.ts +10 -0
- package/src/texts/run-end.ts +13 -0
- package/src/texts/select-tests.ts +3 -0
- package/src/tools/ask-files.ts +263 -0
- package/src/tools/ask-schema.ts +116 -0
- package/src/tools/ask.ts +925 -0
- package/src/tools/check-diff.ts +510 -0
- package/src/tools/docs-check.ts +399 -0
- package/src/tools/find.ts +529 -0
- package/src/tools/locate.ts +369 -0
- package/src/tools/select-tests.ts +746 -0
- 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
|
+
}
|