@haystackeditor/cli 0.25.1 → 0.26.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/README.md +39 -455
- package/dist/capture/app-config.js +25 -1
- package/dist/commands/capture-brief.js +41 -32
- package/dist/commands/feedback.js +66 -0
- package/dist/commands/init-telemetry.js +53 -7
- package/dist/commands/init.js +7 -2
- package/dist/commands/lockfile-pin.js +307 -0
- package/dist/commands/verify.js +68 -13
- package/dist/index.js +27 -923
- package/dist/schema.js +4 -10
- package/dist/utils/haystack-api.js +0 -36
- package/package.json +1 -5
- package/schemas/feedback.v1.json +13 -0
- package/schemas/pre-verify.v2.json +240 -0
- package/schemas/verify-raw.v1.json +1132 -0
- package/schemas/verify.v2.json +655 -0
- package/dist/assets/hooks/agent-context/detect.ts +0 -316
- package/dist/assets/hooks/agent-context/format.ts +0 -100
- package/dist/assets/hooks/agent-context/index.ts +0 -41
- package/dist/assets/hooks/agent-context/parsers/claude.ts +0 -262
- package/dist/assets/hooks/agent-context/parsers/codex.ts +0 -416
- package/dist/assets/hooks/agent-context/parsers/gemini.ts +0 -155
- package/dist/assets/hooks/agent-context/parsers/opencode.ts +0 -174
- package/dist/assets/hooks/agent-context/tsconfig.json +0 -14
- package/dist/assets/hooks/agent-context/types.ts +0 -58
- package/dist/assets/hooks/llm-rules-template.md +0 -59
- package/dist/assets/hooks/package-lock.json +0 -598
- package/dist/assets/hooks/package.json +0 -12
- package/dist/assets/hooks/scripts/commit-msg.sh +0 -5
- package/dist/assets/hooks/scripts/post-commit.sh +0 -5
- package/dist/assets/hooks/scripts/pre-commit.sh +0 -175
- package/dist/assets/hooks/scripts/pre-push.sh +0 -25
- package/dist/assets/hooks/scripts/prepare-commit-msg.sh +0 -5
- package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +0 -528
- package/dist/assets/hooks/truncation-checker/index.ts +0 -595
- package/dist/assets/hooks/truncation-checker/tsconfig.json +0 -13
- package/dist/assets/skills/map-cloud-verifier-universe/SKILL.md +0 -2051
- package/dist/assets/skills/map-cloud-verifier-universe/agents/openai.yaml +0 -4
- package/dist/assets/skills/map-cloud-verifier-universe/references/output-contract.md +0 -3411
- package/dist/assets/skills/map-your-system.md +0 -143
- package/dist/assets/skills/submit.md +0 -200
- package/dist/commands/ask.js +0 -20
- package/dist/commands/cloud-verifier-behaviors.js +0 -218
- package/dist/commands/cloud-verifier-data-store-census.js +0 -539
- package/dist/commands/cloud-verifier-data-store-drift.js +0 -158
- package/dist/commands/cloud-verifier-identity-census.js +0 -4060
- package/dist/commands/cloud-verifier-materialization.js +0 -704
- package/dist/commands/cloud-verifier-pascal-selector-census.js +0 -1382
- package/dist/commands/cloud-verifier-python-manifest-selector-census.js +0 -2015
- package/dist/commands/cloud-verifier-specialized-operational-census.js +0 -11432
- package/dist/commands/cloud-verifier-universe.js +0 -10178
- package/dist/commands/config.js +0 -549
- package/dist/commands/design-verify.js +0 -311
- package/dist/commands/dismiss.js +0 -159
- package/dist/commands/hooks.js +0 -226
- package/dist/commands/inbox.js +0 -137
- package/dist/commands/mcp.js +0 -201
- package/dist/commands/policy.js +0 -371
- package/dist/commands/pr-status.js +0 -207
- package/dist/commands/pr.js +0 -105
- package/dist/commands/prepare-universe-review.js +0 -1092
- package/dist/commands/production-source-deny-policy.js +0 -100
- package/dist/commands/request-review.js +0 -74
- package/dist/commands/review.js +0 -191
- package/dist/commands/rules.js +0 -98
- package/dist/commands/scaffold-provisional-universe.js +0 -806
- package/dist/commands/setup.js +0 -1170
- package/dist/commands/skills.js +0 -447
- package/dist/commands/submit.js +0 -745
- package/dist/commands/system-map.js +0 -228
- package/dist/commands/triage.js +0 -598
- package/dist/commands/webhooks.js +0 -241
- package/dist/states.js +0 -46
- package/dist/tools/detect.js +0 -832
- package/dist/triage/astra.js +0 -202
- package/dist/triage/prompts.js +0 -188
- package/dist/triage/runner.js +0 -200
- package/dist/triage/types.js +0 -7
- package/dist/utils/action-output.js +0 -26
- package/dist/utils/analysis-api.js +0 -416
- package/dist/utils/design-verifier-api.js +0 -294
- package/dist/utils/design-verifier-history.js +0 -79
- package/dist/utils/design-verifier-result.js +0 -424
- package/dist/utils/github-api.js +0 -324
- package/dist/utils/pending-state.js +0 -86
- package/dist/utils/pr-ref.js +0 -56
- package/dist/utils/prompter.js +0 -328
- package/schemas/action.v1.json +0 -22
- package/schemas/ask.v1.json +0 -40
- package/schemas/inbox.v1.json +0 -27
- package/schemas/pr-status.v1.json +0 -61
- package/schemas/pr.v1.json +0 -97
- package/schemas/pr.v3.json +0 -45
- package/schemas/setup.v1.json +0 -75
- package/schemas/submit.v1.json +0 -90
- package/schemas/triage.v1.json +0 -103
- package/schemas/triage.v2.json +0 -64
|
@@ -1,2015 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { readFileSync, statSync } from 'node:fs';
|
|
3
|
-
import { basename, dirname, extname, join, posix } from 'node:path';
|
|
4
|
-
const MAX_SOURCE_BYTES = 4 * 1024 * 1024;
|
|
5
|
-
const PYTHON_EXTENSIONS = new Set(['.py', '.pyi']);
|
|
6
|
-
const TEST_COMPONENTS = new Set([
|
|
7
|
-
'__fixtures__', '__tests__', 'benchmark', 'benchmarks', 'example', 'examples',
|
|
8
|
-
'fixture', 'fixtures', 'mock', 'mocks', 'sample', 'samples', 'spec', 'specs',
|
|
9
|
-
'test', 'testapp', 'testapps', 'tests',
|
|
10
|
-
]);
|
|
11
|
-
const STRUCTURED_DEPENDENCY_COMPONENTS = new Set([
|
|
12
|
-
'.eggs', 'dist-packages', 'node_modules', 'site-packages', '__pypackages__',
|
|
13
|
-
]);
|
|
14
|
-
const NEGATIVE_PIPELINE_PARTS = new Set([
|
|
15
|
-
'complete', 'completion', 'completions', 'exhaustive', 'exhaustiveness',
|
|
16
|
-
'rank', 'ranking', 'search', 'sort', 'sorted', 'validate', 'validation',
|
|
17
|
-
]);
|
|
18
|
-
const MANIFEST_NAMESPACE_KEYS = ['namespace', 'id', 'subdir', 'python', 'name'];
|
|
19
|
-
function sha256(value) {
|
|
20
|
-
return createHash('sha256').update(value).digest('hex');
|
|
21
|
-
}
|
|
22
|
-
function stableId(kind, identityKey) {
|
|
23
|
-
const slug = kind.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
24
|
-
return `census-${slug}-${sha256(`${kind}\0${identityKey}`).slice(0, 20)}`;
|
|
25
|
-
}
|
|
26
|
-
function recordStableId(kind, path, owner, identity) {
|
|
27
|
-
return stableId(kind, `${path}#${owner}:${identity}`);
|
|
28
|
-
}
|
|
29
|
-
function normalizePath(value) {
|
|
30
|
-
return value.replaceAll('\\', '/').replace(/^\.\//, '').replace(/\/{2,}/g, '/');
|
|
31
|
-
}
|
|
32
|
-
function pathComponents(path) {
|
|
33
|
-
return normalizePath(path).toLowerCase().split('/');
|
|
34
|
-
}
|
|
35
|
-
function isTestPath(path) {
|
|
36
|
-
const components = pathComponents(path);
|
|
37
|
-
const name = basename(path).toLowerCase();
|
|
38
|
-
return components.slice(0, -1).some((component) => TEST_COMPONENTS.has(component))
|
|
39
|
-
|| /(?:^|[._-])(test|tests|spec|fixture|mock)(?:[._-]|$)/.test(name)
|
|
40
|
-
|| /^(?:test_|conftest\.)/.test(name);
|
|
41
|
-
}
|
|
42
|
-
function isDependencyPath(path) {
|
|
43
|
-
const components = pathComponents(path);
|
|
44
|
-
return components.slice(0, -1).some((component) => STRUCTURED_DEPENDENCY_COMPONENTS.has(component))
|
|
45
|
-
|| components.some((component) => /\.(?:dist|egg)-info$/.test(component));
|
|
46
|
-
}
|
|
47
|
-
function structuredDependencyPackagePrefixes(paths, sources) {
|
|
48
|
-
const versionedPackagesByContainer = new Map();
|
|
49
|
-
const projectManifestNames = new Set(['pyproject.toml', 'setup.cfg', 'setup.py']);
|
|
50
|
-
for (const path of paths) {
|
|
51
|
-
if (basename(path).toLowerCase() !== '__init__.py')
|
|
52
|
-
continue;
|
|
53
|
-
const source = sources.get(path);
|
|
54
|
-
if (!source)
|
|
55
|
-
continue;
|
|
56
|
-
const hasVersionDeclaration = pythonStatements(path, source).some((statement) => /^(?:from\s+\.[A-Za-z_][A-Za-z0-9_.]*\s+import\s+.*(?:\bVERSION\b|__version__)|(?:VERSION|__version__)\s*=)/.test(statement.text));
|
|
57
|
-
if (!hasVersionDeclaration)
|
|
58
|
-
continue;
|
|
59
|
-
const packageRoot = dirname(path);
|
|
60
|
-
if (paths.some((candidate) => dirname(candidate) === packageRoot
|
|
61
|
-
&& projectManifestNames.has(basename(candidate).toLowerCase())))
|
|
62
|
-
continue;
|
|
63
|
-
const container = dirname(packageRoot);
|
|
64
|
-
if (sources.has(`${container}/__init__.py`))
|
|
65
|
-
continue;
|
|
66
|
-
const packages = versionedPackagesByContainer.get(container) ?? new Set();
|
|
67
|
-
packages.add(packageRoot);
|
|
68
|
-
versionedPackagesByContainer.set(container, packages);
|
|
69
|
-
}
|
|
70
|
-
const result = new Set();
|
|
71
|
-
for (const packages of versionedPackagesByContainer.values()) {
|
|
72
|
-
// Several independently versioned, manifest-free packages under an
|
|
73
|
-
// unversioned container form a structured bundled-distribution boundary.
|
|
74
|
-
if (packages.size < 3)
|
|
75
|
-
continue;
|
|
76
|
-
packages.forEach((packageRoot) => result.add(packageRoot));
|
|
77
|
-
}
|
|
78
|
-
return result;
|
|
79
|
-
}
|
|
80
|
-
function isStructuredDependencyPath(path, prefixes) {
|
|
81
|
-
return isDependencyPath(path)
|
|
82
|
-
|| [...prefixes].some((prefix) => path === prefix || path.startsWith(`${prefix}/`));
|
|
83
|
-
}
|
|
84
|
-
function readText(root, path) {
|
|
85
|
-
try {
|
|
86
|
-
const absolute = join(root, path);
|
|
87
|
-
const stat = statSync(absolute);
|
|
88
|
-
if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES)
|
|
89
|
-
return null;
|
|
90
|
-
const bytes = readFileSync(absolute);
|
|
91
|
-
if (bytes.includes(0))
|
|
92
|
-
return null;
|
|
93
|
-
return bytes.toString('utf8');
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
function indentation(line) {
|
|
100
|
-
const prefix = line.match(/^\s*/)?.[0] ?? '';
|
|
101
|
-
return [...prefix].reduce((total, char) => total + (char === '\t' ? 8 : 1), 0);
|
|
102
|
-
}
|
|
103
|
-
function stripPythonComment(value) {
|
|
104
|
-
let quote = null;
|
|
105
|
-
let triple = false;
|
|
106
|
-
let escaped = false;
|
|
107
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
108
|
-
const char = value[index];
|
|
109
|
-
if (escaped) {
|
|
110
|
-
escaped = false;
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
if (char === '\\') {
|
|
114
|
-
escaped = true;
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
if (quote) {
|
|
118
|
-
if (triple && value.slice(index, index + 3) === quote.repeat(3)) {
|
|
119
|
-
index += 2;
|
|
120
|
-
quote = null;
|
|
121
|
-
triple = false;
|
|
122
|
-
}
|
|
123
|
-
else if (!triple && char === quote) {
|
|
124
|
-
quote = null;
|
|
125
|
-
}
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
if ((char === "'" || char === '"') && value.slice(index, index + 3) === char.repeat(3)) {
|
|
129
|
-
quote = char;
|
|
130
|
-
triple = true;
|
|
131
|
-
index += 2;
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
if (char === "'" || char === '"') {
|
|
135
|
-
quote = char;
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
if (char === '#')
|
|
139
|
-
return value.slice(0, index);
|
|
140
|
-
}
|
|
141
|
-
return value;
|
|
142
|
-
}
|
|
143
|
-
function bracketDelta(value) {
|
|
144
|
-
let result = 0;
|
|
145
|
-
let quote = null;
|
|
146
|
-
let escaped = false;
|
|
147
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
148
|
-
const char = value[index];
|
|
149
|
-
if (escaped) {
|
|
150
|
-
escaped = false;
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
if (char === '\\') {
|
|
154
|
-
escaped = true;
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
if (quote) {
|
|
158
|
-
if (char === quote)
|
|
159
|
-
quote = null;
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
if (char === "'" || char === '"') {
|
|
163
|
-
quote = char;
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
if (char === '(' || char === '[' || char === '{')
|
|
167
|
-
result += 1;
|
|
168
|
-
if (char === ')' || char === ']' || char === '}')
|
|
169
|
-
result -= 1;
|
|
170
|
-
}
|
|
171
|
-
return result;
|
|
172
|
-
}
|
|
173
|
-
function canonicalExpression(value) {
|
|
174
|
-
let result = '';
|
|
175
|
-
let quote = null;
|
|
176
|
-
let escaped = false;
|
|
177
|
-
for (const char of value.trim()) {
|
|
178
|
-
if (escaped) {
|
|
179
|
-
result += char;
|
|
180
|
-
escaped = false;
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
if (char === '\\') {
|
|
184
|
-
result += char;
|
|
185
|
-
escaped = true;
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
if (quote) {
|
|
189
|
-
result += char;
|
|
190
|
-
if (char === quote)
|
|
191
|
-
quote = null;
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
if (char === "'" || char === '"') {
|
|
195
|
-
quote = char;
|
|
196
|
-
result += char;
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
if (!/\s/.test(char))
|
|
200
|
-
result += char;
|
|
201
|
-
}
|
|
202
|
-
return result.replace(/;$/, '');
|
|
203
|
-
}
|
|
204
|
-
function maskMultilinePythonStrings(lines) {
|
|
205
|
-
let triple = null;
|
|
206
|
-
return lines.map((line) => {
|
|
207
|
-
let result = '';
|
|
208
|
-
let quote = null;
|
|
209
|
-
let escapedCharacter = false;
|
|
210
|
-
for (let index = 0; index < line.length; index += 1) {
|
|
211
|
-
if (triple) {
|
|
212
|
-
const close = line.indexOf(triple, index);
|
|
213
|
-
if (close < 0)
|
|
214
|
-
break;
|
|
215
|
-
index = close + 2;
|
|
216
|
-
triple = null;
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
const char = line[index];
|
|
220
|
-
if (escapedCharacter) {
|
|
221
|
-
result += char;
|
|
222
|
-
escapedCharacter = false;
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
|
-
if (char === '\\') {
|
|
226
|
-
result += char;
|
|
227
|
-
escapedCharacter = true;
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
if (quote) {
|
|
231
|
-
result += char;
|
|
232
|
-
if (char === quote)
|
|
233
|
-
quote = null;
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
if ((char === "'" || char === '"') && line.slice(index, index + 3) === char.repeat(3)) {
|
|
237
|
-
triple = char.repeat(3);
|
|
238
|
-
result += char.repeat(2);
|
|
239
|
-
index += 2;
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
if (char === "'" || char === '"')
|
|
243
|
-
quote = char;
|
|
244
|
-
result += char;
|
|
245
|
-
}
|
|
246
|
-
return result;
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
function pythonStatements(path, source) {
|
|
250
|
-
const originalLines = source.split(/\r?\n/);
|
|
251
|
-
const lines = maskMultilinePythonStrings(originalLines);
|
|
252
|
-
const statements = [];
|
|
253
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
254
|
-
const start = index;
|
|
255
|
-
const pieces = [];
|
|
256
|
-
let depth = 0;
|
|
257
|
-
do {
|
|
258
|
-
const withoutComment = stripPythonComment(lines[index]);
|
|
259
|
-
pieces.push(withoutComment.trim());
|
|
260
|
-
depth += bracketDelta(withoutComment);
|
|
261
|
-
if (depth <= 0 && !withoutComment.trimEnd().endsWith('\\'))
|
|
262
|
-
break;
|
|
263
|
-
index += 1;
|
|
264
|
-
} while (index < lines.length && index - start < 10_000);
|
|
265
|
-
const text = pieces.filter(Boolean).join(' ').replace(/\\\s+/g, ' ').trim();
|
|
266
|
-
if (!text)
|
|
267
|
-
continue;
|
|
268
|
-
statements.push({
|
|
269
|
-
path,
|
|
270
|
-
startLine: start + 1,
|
|
271
|
-
endLine: index + 1,
|
|
272
|
-
indent: indentation(lines[start]),
|
|
273
|
-
text,
|
|
274
|
-
anchor: originalLines[start],
|
|
275
|
-
owner: basename(path),
|
|
276
|
-
scope: 'module',
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
const owners = [];
|
|
280
|
-
for (const statement of statements) {
|
|
281
|
-
while (owners.length > 0 && statement.indent <= owners.at(-1).indent)
|
|
282
|
-
owners.pop();
|
|
283
|
-
const declaration = statement.text.match(/^(?:async\s+def|def|class)\s+([A-Za-z_][A-Za-z0-9_]*)\b/);
|
|
284
|
-
if (declaration)
|
|
285
|
-
owners.push({
|
|
286
|
-
indent: statement.indent,
|
|
287
|
-
name: declaration[1],
|
|
288
|
-
scope: /^class\s/.test(statement.text) ? 'class' : 'function',
|
|
289
|
-
});
|
|
290
|
-
statement.owner = owners.length > 0
|
|
291
|
-
? owners.map((owner) => owner.name).join('.')
|
|
292
|
-
: basename(path);
|
|
293
|
-
statement.scope = owners.at(-1)?.scope ?? 'module';
|
|
294
|
-
}
|
|
295
|
-
return statements;
|
|
296
|
-
}
|
|
297
|
-
function splitTopLevel(value, delimiter) {
|
|
298
|
-
const result = [];
|
|
299
|
-
let start = 0;
|
|
300
|
-
let depth = 0;
|
|
301
|
-
let quote = null;
|
|
302
|
-
let escaped = false;
|
|
303
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
304
|
-
const char = value[index];
|
|
305
|
-
if (escaped) {
|
|
306
|
-
escaped = false;
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
if (char === '\\') {
|
|
310
|
-
escaped = true;
|
|
311
|
-
continue;
|
|
312
|
-
}
|
|
313
|
-
if (quote) {
|
|
314
|
-
if (char === quote)
|
|
315
|
-
quote = null;
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
if (char === "'" || char === '"') {
|
|
319
|
-
quote = char;
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
if (char === '(' || char === '[' || char === '{')
|
|
323
|
-
depth += 1;
|
|
324
|
-
if (char === ')' || char === ']' || char === '}')
|
|
325
|
-
depth -= 1;
|
|
326
|
-
if (depth === 0 && value.slice(index, index + delimiter.length) === delimiter) {
|
|
327
|
-
result.push(value.slice(start, index).trim());
|
|
328
|
-
start = index + delimiter.length;
|
|
329
|
-
index += delimiter.length - 1;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
result.push(value.slice(start).trim());
|
|
333
|
-
return result;
|
|
334
|
-
}
|
|
335
|
-
function topLevelIndex(value, sought) {
|
|
336
|
-
let depth = 0;
|
|
337
|
-
let quote = null;
|
|
338
|
-
let escaped = false;
|
|
339
|
-
for (let index = 0; index <= value.length - sought.length; index += 1) {
|
|
340
|
-
const char = value[index];
|
|
341
|
-
if (escaped) {
|
|
342
|
-
escaped = false;
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
|
-
if (char === '\\') {
|
|
346
|
-
escaped = true;
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
if (quote) {
|
|
350
|
-
if (char === quote)
|
|
351
|
-
quote = null;
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
if (char === "'" || char === '"') {
|
|
355
|
-
quote = char;
|
|
356
|
-
continue;
|
|
357
|
-
}
|
|
358
|
-
if (char === '(' || char === '[' || char === '{')
|
|
359
|
-
depth += 1;
|
|
360
|
-
if (char === ')' || char === ']' || char === '}')
|
|
361
|
-
depth -= 1;
|
|
362
|
-
if (depth === 0 && value.slice(index, index + sought.length) === sought)
|
|
363
|
-
return index;
|
|
364
|
-
}
|
|
365
|
-
return -1;
|
|
366
|
-
}
|
|
367
|
-
function decodePythonLiteral(value) {
|
|
368
|
-
const trimmed = value.trim();
|
|
369
|
-
if (/^[+-]?(?:0|[1-9][0-9]*)$/.test(trimmed)) {
|
|
370
|
-
try {
|
|
371
|
-
return `number:${BigInt(trimmed).toString()}`;
|
|
372
|
-
}
|
|
373
|
-
catch {
|
|
374
|
-
return null;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
if (/^[+-]?(?:(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+))$/.test(trimmed)
|
|
378
|
-
|| /^[+-]?[0-9]+\.[0-9]*$/.test(trimmed)) {
|
|
379
|
-
const number = Number(trimmed);
|
|
380
|
-
return Number.isFinite(number) ? `number:${Object.is(number, -0) ? '0' : String(number)}` : null;
|
|
381
|
-
}
|
|
382
|
-
if (trimmed === 'True')
|
|
383
|
-
return 'boolean:true';
|
|
384
|
-
if (trimmed === 'False')
|
|
385
|
-
return 'boolean:false';
|
|
386
|
-
if (trimmed === 'None')
|
|
387
|
-
return 'null:null';
|
|
388
|
-
const string = trimmed.match(/^(?:[rRuU]{0,2})?(['"])([\s\S]*)\1$/);
|
|
389
|
-
if (!string)
|
|
390
|
-
return null;
|
|
391
|
-
const body = string[2];
|
|
392
|
-
const decoded = body.replace(/\\(['"\\nrt])/g, (_match, escaped) => {
|
|
393
|
-
if (escaped === 'n')
|
|
394
|
-
return '\n';
|
|
395
|
-
if (escaped === 'r')
|
|
396
|
-
return '\r';
|
|
397
|
-
if (escaped === 't')
|
|
398
|
-
return '\t';
|
|
399
|
-
return escaped;
|
|
400
|
-
});
|
|
401
|
-
return `string:${JSON.stringify(decoded)}`;
|
|
402
|
-
}
|
|
403
|
-
function canonicalSelectorExpression(value) {
|
|
404
|
-
const literal = decodePythonLiteral(value);
|
|
405
|
-
if (literal !== null)
|
|
406
|
-
return literal;
|
|
407
|
-
const canonical = canonicalExpression(value);
|
|
408
|
-
const components = canonical.split('.');
|
|
409
|
-
const final = components.at(-1) ?? '';
|
|
410
|
-
return /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(canonical)
|
|
411
|
-
&& /^[A-Z_][A-Z0-9_]*$/.test(final)
|
|
412
|
-
? `expression:${canonical}`
|
|
413
|
-
: null;
|
|
414
|
-
}
|
|
415
|
-
function exactSubject(value) {
|
|
416
|
-
const canonical = canonicalExpression(value);
|
|
417
|
-
return /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*|\[(?:['"][^'"]+['"]|-?[0-9]+)\])*$/.test(canonical)
|
|
418
|
-
? canonical
|
|
419
|
-
: null;
|
|
420
|
-
}
|
|
421
|
-
function equalityCondition(value) {
|
|
422
|
-
const operator = topLevelIndex(value, '==');
|
|
423
|
-
if (operator < 0 || topLevelIndex(value.slice(operator + 2), '==') >= 0)
|
|
424
|
-
return null;
|
|
425
|
-
const left = value.slice(0, operator).trim();
|
|
426
|
-
const right = value.slice(operator + 2).trim();
|
|
427
|
-
const leftLiteral = decodePythonLiteral(left);
|
|
428
|
-
const rightLiteral = decodePythonLiteral(right);
|
|
429
|
-
const leftSubject = exactSubject(left);
|
|
430
|
-
const rightSubject = exactSubject(right);
|
|
431
|
-
if (leftSubject && rightLiteral !== null)
|
|
432
|
-
return { subject: leftSubject, selector: rightLiteral };
|
|
433
|
-
if (rightSubject && leftLiteral !== null)
|
|
434
|
-
return { subject: rightSubject, selector: leftLiteral };
|
|
435
|
-
return null;
|
|
436
|
-
}
|
|
437
|
-
function branchHeader(statement) {
|
|
438
|
-
const match = statement.text.match(/^(if|elif|else)\b\s*(.*)$/);
|
|
439
|
-
if (!match)
|
|
440
|
-
return null;
|
|
441
|
-
const colon = topLevelIndex(match[2], ':');
|
|
442
|
-
if (colon < 0)
|
|
443
|
-
return null;
|
|
444
|
-
return {
|
|
445
|
-
keyword: match[1],
|
|
446
|
-
condition: match[1] === 'else' ? '' : match[2].slice(0, colon).trim(),
|
|
447
|
-
inlineSuite: match[2].slice(colon + 1).trim(),
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
function terminalIdentity(value) {
|
|
451
|
-
const parts = splitTopLevel(value, ';').filter(Boolean);
|
|
452
|
-
const terminal = parts.at(-1)?.trim() ?? '';
|
|
453
|
-
const control = terminal.match(/^(return|raise|yield)\s+(.+)$/);
|
|
454
|
-
if (control)
|
|
455
|
-
return `${control[1]}:${canonicalExpression(control[2])}`;
|
|
456
|
-
const assignment = terminal.match(/^([A-Za-z_][A-Za-z0-9_.]*(?:\[[^\]]+\])?)\s*=\s*(.+)$/);
|
|
457
|
-
if (assignment)
|
|
458
|
-
return `assign:${canonicalExpression(assignment[1])}:${canonicalExpression(assignment[2])}`;
|
|
459
|
-
if (/^(?:await\s+)?[A-Za-z_][A-Za-z0-9_.]*\s*\(/.test(terminal)) {
|
|
460
|
-
return `call:${canonicalExpression(terminal)}`;
|
|
461
|
-
}
|
|
462
|
-
return null;
|
|
463
|
-
}
|
|
464
|
-
function emitEqualityChains(statements, records) {
|
|
465
|
-
for (let index = 0; index < statements.length; index += 1) {
|
|
466
|
-
const first = branchHeader(statements[index]);
|
|
467
|
-
if (!first || first.keyword !== 'if')
|
|
468
|
-
continue;
|
|
469
|
-
const equality = equalityCondition(first.condition);
|
|
470
|
-
if (!equality)
|
|
471
|
-
continue;
|
|
472
|
-
const branches = [];
|
|
473
|
-
let cursor = index;
|
|
474
|
-
let expectedSubject = equality.subject;
|
|
475
|
-
while (cursor < statements.length) {
|
|
476
|
-
const statement = statements[cursor];
|
|
477
|
-
const header = branchHeader(statement);
|
|
478
|
-
if (!header || (header.keyword !== 'if' && header.keyword !== 'elif'))
|
|
479
|
-
break;
|
|
480
|
-
const parsed = equalityCondition(header.condition);
|
|
481
|
-
if (!parsed || parsed.subject !== expectedSubject)
|
|
482
|
-
break;
|
|
483
|
-
let nextPeer = cursor + 1;
|
|
484
|
-
while (nextPeer < statements.length && statements[nextPeer].indent > statement.indent)
|
|
485
|
-
nextPeer += 1;
|
|
486
|
-
const suiteStatements = statements.slice(cursor + 1, nextPeer);
|
|
487
|
-
const implementation = header.inlineSuite
|
|
488
|
-
? terminalIdentity(header.inlineSuite)
|
|
489
|
-
: [...suiteStatements].reverse().map((candidate) => terminalIdentity(candidate.text)).find(Boolean) ?? null;
|
|
490
|
-
if (!implementation)
|
|
491
|
-
break;
|
|
492
|
-
branches.push({ statement, selector: parsed.selector, implementation });
|
|
493
|
-
if (nextPeer >= statements.length) {
|
|
494
|
-
cursor = nextPeer;
|
|
495
|
-
break;
|
|
496
|
-
}
|
|
497
|
-
const nextHeader = branchHeader(statements[nextPeer]);
|
|
498
|
-
if (statements[nextPeer].indent !== statement.indent
|
|
499
|
-
|| statements[nextPeer].owner !== statement.owner
|
|
500
|
-
|| nextHeader?.keyword !== 'elif') {
|
|
501
|
-
cursor = nextPeer;
|
|
502
|
-
break;
|
|
503
|
-
}
|
|
504
|
-
cursor = nextPeer;
|
|
505
|
-
expectedSubject = parsed.subject;
|
|
506
|
-
}
|
|
507
|
-
if (branches.length < 3
|
|
508
|
-
|| new Set(branches.map((branch) => branch.selector)).size !== branches.length
|
|
509
|
-
|| new Set(branches.map((branch) => branch.implementation)).size !== branches.length)
|
|
510
|
-
continue;
|
|
511
|
-
const statement = statements[index];
|
|
512
|
-
const ownerIdentity = `${statement.path}#${statement.owner}`;
|
|
513
|
-
const signature = sha256(branches.map((branch) => `${branch.selector}\0${branch.implementation}`).join('\0')).slice(0, 24);
|
|
514
|
-
const identity = `python-equality:${equality.subject}:${signature}`;
|
|
515
|
-
const familyKind = 'python-exact-equality-selector-family';
|
|
516
|
-
const entryKind = 'python-exact-equality-selector-entry';
|
|
517
|
-
const familyId = recordStableId(familyKind, statement.path, statement.owner, identity);
|
|
518
|
-
records.push({
|
|
519
|
-
kind: familyKind,
|
|
520
|
-
projections: ['registry_families'],
|
|
521
|
-
presumptiveInventory: true,
|
|
522
|
-
proofStatus: 'proof-complete',
|
|
523
|
-
path: statement.path,
|
|
524
|
-
line: statement.startLine,
|
|
525
|
-
anchor: statement.anchor,
|
|
526
|
-
owner: statement.owner,
|
|
527
|
-
identity,
|
|
528
|
-
tokens: [equality.subject, 'proof-complete:python-exact-equality-chain'],
|
|
529
|
-
semanticIdentityKey: `python-registry-family\0${ownerIdentity}\0equality\0${equality.subject}\0${signature}`,
|
|
530
|
-
identityComponents: {
|
|
531
|
-
identity_kind: 'python-registry-family',
|
|
532
|
-
owner_identity: ownerIdentity,
|
|
533
|
-
selector_domain: equality.subject,
|
|
534
|
-
},
|
|
535
|
-
});
|
|
536
|
-
branches.forEach((branch) => {
|
|
537
|
-
const entryIdentity = `${identity}:selector:${branch.selector}:implementation:${branch.implementation}`;
|
|
538
|
-
records.push({
|
|
539
|
-
kind: entryKind,
|
|
540
|
-
projections: ['registry_entries'],
|
|
541
|
-
presumptiveInventory: true,
|
|
542
|
-
proofStatus: 'proof-complete',
|
|
543
|
-
path: branch.statement.path,
|
|
544
|
-
line: branch.statement.startLine,
|
|
545
|
-
anchor: branch.statement.anchor,
|
|
546
|
-
owner: branch.statement.owner,
|
|
547
|
-
identity: entryIdentity,
|
|
548
|
-
tokens: [branch.selector, branch.implementation, 'proof-complete:python-exact-equality-chain'],
|
|
549
|
-
selectorKey: branch.selector,
|
|
550
|
-
implementationKey: `${ownerIdentity}:${branch.implementation}`,
|
|
551
|
-
selectorFamilyRecordId: familyId,
|
|
552
|
-
semanticIdentityKey: `python-registry-entry\0${ownerIdentity}\0equality\0${equality.subject}\0${branch.selector}\0${branch.implementation}`,
|
|
553
|
-
identityComponents: {
|
|
554
|
-
identity_kind: 'python-registry-entry',
|
|
555
|
-
owner_identity: ownerIdentity,
|
|
556
|
-
selector_domain: equality.subject,
|
|
557
|
-
selector_key: branch.selector,
|
|
558
|
-
implementation_identity: `${ownerIdentity}:${branch.implementation}`,
|
|
559
|
-
},
|
|
560
|
-
});
|
|
561
|
-
});
|
|
562
|
-
index = Math.max(index, cursor - 1);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
function unwrapCollection(value, open, close) {
|
|
566
|
-
const trimmed = value.trim();
|
|
567
|
-
if (!trimmed.startsWith(open) || !trimmed.endsWith(close))
|
|
568
|
-
return null;
|
|
569
|
-
return trimmed.slice(open.length, -close.length).trim();
|
|
570
|
-
}
|
|
571
|
-
function parseStaticCollections(statements) {
|
|
572
|
-
const collections = new Map();
|
|
573
|
-
for (const statement of statements) {
|
|
574
|
-
const assignment = statement.text.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*(?::[^=]+)?=\s*([\s\S]+)$/);
|
|
575
|
-
if (!assignment)
|
|
576
|
-
continue;
|
|
577
|
-
const symbol = assignment[1];
|
|
578
|
-
if (symbol === '__all__')
|
|
579
|
-
continue;
|
|
580
|
-
const expression = assignment[2].trim();
|
|
581
|
-
const dictBody = unwrapCollection(expression, '{', '}');
|
|
582
|
-
if (dictBody !== null && !/^\s*$/.test(dictBody)) {
|
|
583
|
-
const entries = [];
|
|
584
|
-
let valid = true;
|
|
585
|
-
for (const rawEntry of splitTopLevel(dictBody, ',')) {
|
|
586
|
-
if (!rawEntry)
|
|
587
|
-
continue;
|
|
588
|
-
const colon = topLevelIndex(rawEntry, ':');
|
|
589
|
-
if (colon < 0 || rawEntry.trimStart().startsWith('**')) {
|
|
590
|
-
valid = false;
|
|
591
|
-
break;
|
|
592
|
-
}
|
|
593
|
-
const selector = canonicalSelectorExpression(rawEntry.slice(0, colon));
|
|
594
|
-
const implementation = rawEntry.slice(colon + 1).trim();
|
|
595
|
-
if (selector === null || !implementation || /\bfor\b/.test(implementation)) {
|
|
596
|
-
valid = false;
|
|
597
|
-
break;
|
|
598
|
-
}
|
|
599
|
-
entries.push({
|
|
600
|
-
selector,
|
|
601
|
-
implementation: canonicalExpression(implementation),
|
|
602
|
-
line: statement.startLine,
|
|
603
|
-
anchor: statement.anchor,
|
|
604
|
-
});
|
|
605
|
-
}
|
|
606
|
-
if (valid && entries.length > 0 && new Set(entries.map((entry) => entry.selector)).size === entries.length) {
|
|
607
|
-
const collection = {
|
|
608
|
-
collectionKind: 'mapping', path: statement.path, symbol, owner: statement.owner,
|
|
609
|
-
line: statement.startLine, anchor: statement.anchor,
|
|
610
|
-
ephemeral: statement.scope === 'function',
|
|
611
|
-
entries,
|
|
612
|
-
};
|
|
613
|
-
collections.set(statement.scope === 'module' ? symbol : `${statement.owner}.${symbol}`, collection);
|
|
614
|
-
}
|
|
615
|
-
continue;
|
|
616
|
-
}
|
|
617
|
-
const listBody = unwrapCollection(expression, '[', ']') ?? unwrapCollection(expression, '(', ')');
|
|
618
|
-
if (listBody === null || !listBody)
|
|
619
|
-
continue;
|
|
620
|
-
const entries = [];
|
|
621
|
-
let valid = true;
|
|
622
|
-
for (const rawItem of splitTopLevel(listBody, ',')) {
|
|
623
|
-
if (!rawItem)
|
|
624
|
-
continue;
|
|
625
|
-
const tupleBody = unwrapCollection(rawItem, '(', ')');
|
|
626
|
-
if (tupleBody !== null) {
|
|
627
|
-
const tuple = splitTopLevel(tupleBody, ',').filter(Boolean);
|
|
628
|
-
const selector = tuple.length === 2 ? canonicalSelectorExpression(tuple[0]) : null;
|
|
629
|
-
if (selector === null || !tuple[1]) {
|
|
630
|
-
valid = false;
|
|
631
|
-
break;
|
|
632
|
-
}
|
|
633
|
-
entries.push({
|
|
634
|
-
selector,
|
|
635
|
-
implementation: canonicalExpression(tuple[1]),
|
|
636
|
-
line: statement.startLine,
|
|
637
|
-
anchor: statement.anchor,
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
else {
|
|
641
|
-
const selector = canonicalSelectorExpression(rawItem);
|
|
642
|
-
if (selector === null) {
|
|
643
|
-
valid = false;
|
|
644
|
-
break;
|
|
645
|
-
}
|
|
646
|
-
entries.push({
|
|
647
|
-
selector,
|
|
648
|
-
implementation: '$selector',
|
|
649
|
-
line: statement.startLine,
|
|
650
|
-
anchor: statement.anchor,
|
|
651
|
-
});
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
if (valid && entries.length > 0 && new Set(entries.map((entry) => entry.selector)).size === entries.length) {
|
|
655
|
-
const collection = {
|
|
656
|
-
collectionKind: 'registration-list', path: statement.path, symbol, owner: statement.owner,
|
|
657
|
-
line: statement.startLine, anchor: statement.anchor,
|
|
658
|
-
ephemeral: statement.scope === 'function',
|
|
659
|
-
entries,
|
|
660
|
-
};
|
|
661
|
-
collections.set(statement.scope === 'module' ? symbol : `${statement.owner}.${symbol}`, collection);
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
return collections;
|
|
665
|
-
}
|
|
666
|
-
function moduleNames(path) {
|
|
667
|
-
const withoutExtension = normalizePath(path).replace(/\.pyi?$/, '').replace(/\/__init__$/, '');
|
|
668
|
-
const names = new Set([withoutExtension.replaceAll('/', '.')]);
|
|
669
|
-
const components = withoutExtension.split('/');
|
|
670
|
-
for (let index = 1; index < components.length; index += 1) {
|
|
671
|
-
if (['dist-packages', 'lib', 'site-packages', 'src'].includes(components[index - 1])) {
|
|
672
|
-
names.add(components.slice(index).join('.'));
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
return [...names];
|
|
676
|
-
}
|
|
677
|
-
function resolveImportModule(importerPath, specifier, modulePaths, knownPaths) {
|
|
678
|
-
if (!specifier.startsWith('.'))
|
|
679
|
-
return modulePaths.get(specifier) ?? null;
|
|
680
|
-
const dots = specifier.match(/^\.+/)?.[0].length ?? 0;
|
|
681
|
-
const suffix = specifier.slice(dots).split('.').filter(Boolean);
|
|
682
|
-
const base = dirname(importerPath).split('/').filter((component) => component !== '.');
|
|
683
|
-
for (let count = 1; count < dots; count += 1)
|
|
684
|
-
base.pop();
|
|
685
|
-
const joined = normalizePath([...base, ...suffix].join('/'));
|
|
686
|
-
return [`${joined}.py`, `${joined}/__init__.py`, `${joined}.pyi`]
|
|
687
|
-
.find((candidate) => knownPaths.has(candidate)) ?? null;
|
|
688
|
-
}
|
|
689
|
-
function importBindings(path, statements, modulePaths, knownPaths) {
|
|
690
|
-
const result = new Map();
|
|
691
|
-
for (const statement of statements) {
|
|
692
|
-
const fromImport = statement.text.match(/^from\s+([.A-Za-z_][.A-Za-z0-9_]*)\s+import\s+(.+)$/);
|
|
693
|
-
if (fromImport) {
|
|
694
|
-
const importedPath = resolveImportModule(path, fromImport[1], modulePaths, knownPaths);
|
|
695
|
-
if (!importedPath)
|
|
696
|
-
continue;
|
|
697
|
-
const importedSymbols = fromImport[2].trim().replace(/^\(|\)$/g, '');
|
|
698
|
-
for (const raw of splitTopLevel(importedSymbols, ',')) {
|
|
699
|
-
if (raw === '*') {
|
|
700
|
-
result.set('*', { path: importedPath, symbol: '*' });
|
|
701
|
-
continue;
|
|
702
|
-
}
|
|
703
|
-
const symbol = raw.match(/^([A-Za-z_][A-Za-z0-9_]*)(?:\s+as\s+([A-Za-z_][A-Za-z0-9_]*))?$/);
|
|
704
|
-
if (symbol)
|
|
705
|
-
result.set(symbol[2] ?? symbol[1], { path: importedPath, symbol: symbol[1] });
|
|
706
|
-
}
|
|
707
|
-
continue;
|
|
708
|
-
}
|
|
709
|
-
const moduleImport = statement.text.match(/^import\s+([A-Za-z_][A-Za-z0-9_.]*)(?:\s+as\s+([A-Za-z_][A-Za-z0-9_]*))?$/);
|
|
710
|
-
if (!moduleImport)
|
|
711
|
-
continue;
|
|
712
|
-
const importedPath = resolveImportModule(path, moduleImport[1], modulePaths, knownPaths);
|
|
713
|
-
if (importedPath) {
|
|
714
|
-
result.set(moduleImport[2] ?? moduleImport[1].split('.').at(-1), {
|
|
715
|
-
path: importedPath,
|
|
716
|
-
symbol: '*',
|
|
717
|
-
});
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
return result;
|
|
721
|
-
}
|
|
722
|
-
function identifierParts(value) {
|
|
723
|
-
const parts = new Set();
|
|
724
|
-
for (const identifier of value.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? []) {
|
|
725
|
-
const withBoundaries = identifier.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
|
726
|
-
withBoundaries.split('_').filter(Boolean).forEach((part) => parts.add(part));
|
|
727
|
-
}
|
|
728
|
-
return parts;
|
|
729
|
-
}
|
|
730
|
-
function escaped(value) {
|
|
731
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
732
|
-
}
|
|
733
|
-
function dynamicSelectorExpression(value) {
|
|
734
|
-
const canonical = canonicalExpression(value);
|
|
735
|
-
if (decodePythonLiteral(canonical) !== null)
|
|
736
|
-
return false;
|
|
737
|
-
if (/^[A-Z_][A-Z0-9_]*$/.test(canonical))
|
|
738
|
-
return false;
|
|
739
|
-
if (/^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)+$/.test(canonical))
|
|
740
|
-
return false;
|
|
741
|
-
return /^[A-Za-z_][A-Za-z0-9_]*(?:\[[^\]]+\])?$/.test(canonical);
|
|
742
|
-
}
|
|
743
|
-
function mappingLookup(expression, binding) {
|
|
744
|
-
const symbol = escaped(binding);
|
|
745
|
-
const bracket = new RegExp(`${symbol}\\[([^\\u005d]+)\\]`).exec(expression);
|
|
746
|
-
const get = new RegExp(`${symbol}\\.get\\(([^,)]+)(?:,[^)]*)?\\)`).exec(expression);
|
|
747
|
-
const candidates = [bracket, get].filter((match) => match !== null)
|
|
748
|
-
.sort((left, right) => left.index - right.index);
|
|
749
|
-
const selected = candidates[0];
|
|
750
|
-
return selected ? { index: selected.index, text: selected[0], selector: selected[1] } : null;
|
|
751
|
-
}
|
|
752
|
-
function mappingGate(statement, binding) {
|
|
753
|
-
const text = statement.text;
|
|
754
|
-
const parts = identifierParts(text);
|
|
755
|
-
const ownerParts = identifierParts(statement.owner);
|
|
756
|
-
if ([...parts, ...ownerParts].some((part) => NEGATIVE_PIPELINE_PARTS.has(part))
|
|
757
|
-
|| statement.owner.split('.').some((owner) => /^(?:test|tests)_/.test(owner.toLowerCase())))
|
|
758
|
-
return null;
|
|
759
|
-
const symbol = escaped(binding);
|
|
760
|
-
const canonical = canonicalExpression(text);
|
|
761
|
-
const lookup = mappingLookup(canonical, binding);
|
|
762
|
-
if (lookup && dynamicSelectorExpression(lookup.selector)) {
|
|
763
|
-
if (new RegExp(`^for.+in${escaped(lookup.text)}`).test(canonical))
|
|
764
|
-
return null;
|
|
765
|
-
const afterLookup = canonical.slice(lookup.index + lookup.text.length);
|
|
766
|
-
if (afterLookup.startsWith('('))
|
|
767
|
-
return 'behavior';
|
|
768
|
-
if (/^(?:return|yield)/.test(canonical))
|
|
769
|
-
return 'choice';
|
|
770
|
-
if (new RegExp(`^[A-Za-z_][A-Za-z0-9_.]*\\[[^\\u005d]+\\]=`).test(canonical))
|
|
771
|
-
return 'registration';
|
|
772
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*=/.test(canonical) && /\([^)]/.test(canonical))
|
|
773
|
-
return 'choice';
|
|
774
|
-
}
|
|
775
|
-
if (new RegExp(`(?:choices|options|values)=${symbol}\\.keys\\(\\)(?:[,)]|$)`).test(canonical)) {
|
|
776
|
-
return 'choice';
|
|
777
|
-
}
|
|
778
|
-
return null;
|
|
779
|
-
}
|
|
780
|
-
function mappingResultGate(statements, binding) {
|
|
781
|
-
for (let index = 0; index < statements.length; index += 1) {
|
|
782
|
-
const assignment = statements[index].text.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)$/);
|
|
783
|
-
if (!assignment)
|
|
784
|
-
continue;
|
|
785
|
-
const lookup = mappingLookup(canonicalExpression(assignment[2]), binding);
|
|
786
|
-
if (!lookup || !dynamicSelectorExpression(lookup.selector))
|
|
787
|
-
continue;
|
|
788
|
-
const assignmentParts = identifierParts(statements[index].text);
|
|
789
|
-
const ownerParts = identifierParts(statements[index].owner);
|
|
790
|
-
if ([...assignmentParts, ...ownerParts].some((part) => NEGATIVE_PIPELINE_PARTS.has(part))
|
|
791
|
-
|| statements[index].owner.split('.').some((owner) => /^(?:test|tests)_/.test(owner.toLowerCase())))
|
|
792
|
-
continue;
|
|
793
|
-
const values = new Set([assignment[1]]);
|
|
794
|
-
const exhaustivelyIterated = statements.slice(index + 1).some((candidate) => candidate.owner === statements[index].owner
|
|
795
|
-
&& new RegExp(`^\\s*for\\s+.+?\\s+in\\s+${escaped(assignment[1])}\\s*:`).test(candidate.text));
|
|
796
|
-
if (exhaustivelyIterated)
|
|
797
|
-
continue;
|
|
798
|
-
for (let cursor = index + 1; cursor < statements.length; cursor += 1) {
|
|
799
|
-
const candidate = statements[cursor];
|
|
800
|
-
if (candidate.owner !== statements[index].owner)
|
|
801
|
-
continue;
|
|
802
|
-
const alias = candidate.text.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
803
|
-
if (alias && values.has(alias[2]))
|
|
804
|
-
values.add(alias[1]);
|
|
805
|
-
const canonical = canonicalExpression(candidate.text);
|
|
806
|
-
const selected = [...values].find((value) => new RegExp(`\\b${escaped(value)}\\b`).test(canonical));
|
|
807
|
-
if (!selected)
|
|
808
|
-
continue;
|
|
809
|
-
const parts = identifierParts(candidate.text);
|
|
810
|
-
if ([...parts].some((part) => NEGATIVE_PIPELINE_PARTS.has(part)))
|
|
811
|
-
continue;
|
|
812
|
-
const returned = new RegExp(`^(?:return|yield)${escaped(selected)}$`).test(canonical);
|
|
813
|
-
const configAssignment = new RegExp(`^[A-Za-z_][A-Za-z0-9_.]*\\[[^\\u005d]+\\]=${escaped(selected)}$`).test(canonical);
|
|
814
|
-
const call = new RegExp(`(?:^|[=(,])(?:[A-Za-z_][A-Za-z0-9_.]*\\()?[^(]*\\([^)]*\\b${escaped(selected)}\\b`).test(canonical);
|
|
815
|
-
if (returned || configAssignment || call) {
|
|
816
|
-
return {
|
|
817
|
-
path: candidate.path,
|
|
818
|
-
line: candidate.startLine,
|
|
819
|
-
anchor: candidate.anchor,
|
|
820
|
-
owner: candidate.owner,
|
|
821
|
-
statement: `${statements[index].text} -> ${candidate.text}`,
|
|
822
|
-
};
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
return null;
|
|
827
|
-
}
|
|
828
|
-
function mappingHasMutation(statements, binding, collection) {
|
|
829
|
-
const symbol = escaped(binding);
|
|
830
|
-
return statements.some((statement) => {
|
|
831
|
-
if (statement.path === collection.path && statement.startLine === collection.line)
|
|
832
|
-
return false;
|
|
833
|
-
const canonical = canonicalExpression(statement.text);
|
|
834
|
-
return new RegExp('^(?:del)?' + symbol + '\\[[^\\u005d]+\\](?:=|$)').test(canonical)
|
|
835
|
-
|| new RegExp('^' + symbol + '\\.(?:clear|pop|popitem|setdefault|update)\\(').test(canonical);
|
|
836
|
-
});
|
|
837
|
-
}
|
|
838
|
-
function registrationListGate(statements, loopIndex, binding) {
|
|
839
|
-
const loop = statements[loopIndex];
|
|
840
|
-
const symbol = escaped(binding);
|
|
841
|
-
const match = loop.text.match(new RegExp(`^for\\s+(.+?)\\s+in\\s+${symbol}\\s*:\\s*(.*)$`));
|
|
842
|
-
if (!match)
|
|
843
|
-
return null;
|
|
844
|
-
const variables = match[1].replace(/[()]/g, '').split(',').map((value) => value.trim());
|
|
845
|
-
if (variables.length < 2
|
|
846
|
-
|| variables.some((variable) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(variable)))
|
|
847
|
-
return null;
|
|
848
|
-
const suite = [];
|
|
849
|
-
if (match[2])
|
|
850
|
-
suite.push({ ...loop, text: match[2] });
|
|
851
|
-
let cursor = loopIndex + 1;
|
|
852
|
-
while (cursor < statements.length && statements[cursor].indent > loop.indent) {
|
|
853
|
-
suite.push(statements[cursor]);
|
|
854
|
-
cursor += 1;
|
|
855
|
-
}
|
|
856
|
-
const keyVariable = escaped(variables[0]);
|
|
857
|
-
const implementationVariable = escaped(variables[1]);
|
|
858
|
-
for (const candidate of suite) {
|
|
859
|
-
const parts = identifierParts(candidate.text);
|
|
860
|
-
if ([...parts].some((part) => NEGATIVE_PIPELINE_PARTS.has(part)))
|
|
861
|
-
continue;
|
|
862
|
-
const canonical = canonicalExpression(candidate.text);
|
|
863
|
-
const addressAssignment = new RegExp(`^[A-Za-z_][A-Za-z0-9_.]*\\[${keyVariable}\\]=`).test(canonical);
|
|
864
|
-
const registrationCall = new RegExp(`(?:^|\\.)register\\([^)]*\\b${keyVariable}\\b`).test(canonical);
|
|
865
|
-
const preservesImplementation = new RegExp(`\\b${implementationVariable}\\b`).test(canonical);
|
|
866
|
-
if ((addressAssignment || registrationCall) && preservesImplementation) {
|
|
867
|
-
return {
|
|
868
|
-
gate: {
|
|
869
|
-
path: candidate.path,
|
|
870
|
-
line: candidate.startLine,
|
|
871
|
-
anchor: candidate.anchor,
|
|
872
|
-
owner: candidate.owner,
|
|
873
|
-
statement: candidate.text,
|
|
874
|
-
},
|
|
875
|
-
variables,
|
|
876
|
-
};
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
return null;
|
|
880
|
-
}
|
|
881
|
-
function mappingIterationGate(statements, loopIndex, binding) {
|
|
882
|
-
const loop = statements[loopIndex];
|
|
883
|
-
const symbol = escaped(binding);
|
|
884
|
-
const match = loop.text.match(new RegExp(`^for\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*,\\s*([A-Za-z_][A-Za-z0-9_]*)`
|
|
885
|
-
+ `\\s+in\\s+${symbol}\\.items\\(\\)\\s*:\\s*(.*)$`));
|
|
886
|
-
if (!match)
|
|
887
|
-
return null;
|
|
888
|
-
const suite = [];
|
|
889
|
-
if (match[3])
|
|
890
|
-
suite.push({ ...loop, text: match[3] });
|
|
891
|
-
let cursor = loopIndex + 1;
|
|
892
|
-
while (cursor < statements.length && statements[cursor].indent > loop.indent) {
|
|
893
|
-
suite.push(statements[cursor]);
|
|
894
|
-
cursor += 1;
|
|
895
|
-
}
|
|
896
|
-
const key = escaped(match[1]);
|
|
897
|
-
const implementation = escaped(match[2]);
|
|
898
|
-
const gate = suite.find((candidate) => {
|
|
899
|
-
const canonical = canonicalExpression(candidate.text);
|
|
900
|
-
return new RegExp(`^[A-Za-z_][A-Za-z0-9_.]*\\[${key}\\]=${implementation}$`).test(canonical)
|
|
901
|
-
|| new RegExp(`(?:^|\\.)register\\(${key},${implementation}(?:,|\\))`).test(canonical);
|
|
902
|
-
});
|
|
903
|
-
return gate ? {
|
|
904
|
-
path: gate.path,
|
|
905
|
-
line: gate.startLine,
|
|
906
|
-
anchor: gate.anchor,
|
|
907
|
-
owner: gate.owner,
|
|
908
|
-
statement: gate.text,
|
|
909
|
-
} : null;
|
|
910
|
-
}
|
|
911
|
-
function interproceduralIterationGate(statements, binding, collectionKind) {
|
|
912
|
-
const definitions = statements.flatMap((statement) => {
|
|
913
|
-
const definition = statement.text.match(/^(?:async\s+def|def)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(([^)]*)\)/);
|
|
914
|
-
return definition ? [{ statement, name: definition[1], parameters: splitTopLevel(definition[2], ',')
|
|
915
|
-
.map((parameter) => parameter.replace(/:.*/, '').replace(/=.*/, '').trim()) }] : [];
|
|
916
|
-
});
|
|
917
|
-
for (const definition of definitions) {
|
|
918
|
-
const callPattern = new RegExp(`(?:^|[^A-Za-z0-9_.])${escaped(definition.name)}\\(([^)]*)\\)`);
|
|
919
|
-
const call = statements.map((statement) => ({ statement, match: callPattern.exec(statement.text) }))
|
|
920
|
-
.find((candidate) => candidate.match && !/^(?:async\s+def|def)\s/.test(candidate.statement.text));
|
|
921
|
-
if (!call?.match)
|
|
922
|
-
continue;
|
|
923
|
-
const argumentsList = splitTopLevel(call.match[1], ',');
|
|
924
|
-
const argumentIndex = argumentsList.findIndex((argument) => canonicalExpression(argument) === canonicalExpression(binding));
|
|
925
|
-
const parameter = definition.parameters[argumentIndex];
|
|
926
|
-
if (!parameter || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(parameter))
|
|
927
|
-
continue;
|
|
928
|
-
const body = statements.filter((statement) => statement.owner === definition.name || statement.owner.startsWith(`${definition.name}.`));
|
|
929
|
-
if (collectionKind === 'mapping') {
|
|
930
|
-
for (let index = 0; index < body.length; index += 1) {
|
|
931
|
-
const gate = mappingIterationGate(body, index, parameter);
|
|
932
|
-
if (gate)
|
|
933
|
-
return { gate, variables: null };
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
else {
|
|
937
|
-
for (let index = 0; index < body.length; index += 1) {
|
|
938
|
-
const gate = registrationListGate(body, index, parameter);
|
|
939
|
-
if (gate)
|
|
940
|
-
return gate;
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
return null;
|
|
945
|
-
}
|
|
946
|
-
function generatedChoiceGate(statements, binding) {
|
|
947
|
-
const symbol = escaped(binding);
|
|
948
|
-
const joined = statements.find((statement) => new RegExp(`(['"])[^'"]*\\1\\.join\\(${symbol}\\)`).test(canonicalExpression(statement.text)));
|
|
949
|
-
if (!joined)
|
|
950
|
-
return null;
|
|
951
|
-
for (let index = 0; index < statements.length; index += 1) {
|
|
952
|
-
const loop = statements[index];
|
|
953
|
-
const enumeration = loop.text.match(/^for\s+([A-Za-z_][A-Za-z0-9_]*)\s*,\s*[A-Za-z_][A-Za-z0-9_]*\s+in\s+enumerate\(\s*([A-Za-z_][A-Za-z0-9_.]*)\s*\)\s*:/);
|
|
954
|
-
if (!enumeration || canonicalExpression(enumeration[2]) === canonicalExpression(binding))
|
|
955
|
-
continue;
|
|
956
|
-
let cursor = index + 1;
|
|
957
|
-
while (cursor < statements.length && statements[cursor].indent > loop.indent) {
|
|
958
|
-
const canonical = canonicalExpression(statements[cursor].text);
|
|
959
|
-
if (new RegExp(`${symbol}\\[${escaped(enumeration[1])}\\]`).test(canonical)) {
|
|
960
|
-
return {
|
|
961
|
-
path: statements[cursor].path,
|
|
962
|
-
line: statements[cursor].startLine,
|
|
963
|
-
anchor: statements[cursor].anchor,
|
|
964
|
-
owner: statements[cursor].owner,
|
|
965
|
-
statement: `${joined.text} -> ${statements[cursor].text}`,
|
|
966
|
-
};
|
|
967
|
-
}
|
|
968
|
-
cursor += 1;
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
return null;
|
|
972
|
-
}
|
|
973
|
-
function choiceListGate(statement, binding) {
|
|
974
|
-
const parts = identifierParts(statement.text);
|
|
975
|
-
if ([...parts].some((part) => NEGATIVE_PIPELINE_PARTS.has(part)))
|
|
976
|
-
return false;
|
|
977
|
-
const symbol = escaped(binding);
|
|
978
|
-
const canonical = canonicalExpression(statement.text);
|
|
979
|
-
const indexed = new RegExp(`(?:return|yield)${symbol}\\[([^\\u005d]+)\\]`).exec(canonical);
|
|
980
|
-
return new RegExp(`(?:choices|options|values)=${symbol}(?:[,)]|$)`).test(canonical)
|
|
981
|
-
|| (indexed !== null && dynamicSelectorExpression(indexed[1]))
|
|
982
|
-
|| new RegExp(`(?:^|[:(])[^:()]+\\bin${symbol}(?:[:)])`).test(canonical);
|
|
983
|
-
}
|
|
984
|
-
function pythonExportedSymbols(statements) {
|
|
985
|
-
const declaration = statements.find((statement) => /^__all__\s*=/.test(statement.text));
|
|
986
|
-
if (!declaration)
|
|
987
|
-
return null;
|
|
988
|
-
const body = declaration.text.replace(/^__all__\s*=\s*/, '');
|
|
989
|
-
const inner = unwrapCollection(body, '[', ']') ?? unwrapCollection(body, '(', ')');
|
|
990
|
-
if (inner === null)
|
|
991
|
-
return new Set();
|
|
992
|
-
const result = new Set();
|
|
993
|
-
for (const item of splitTopLevel(inner, ',')) {
|
|
994
|
-
const selector = decodePythonLiteral(item);
|
|
995
|
-
if (!selector?.startsWith('string:'))
|
|
996
|
-
continue;
|
|
997
|
-
try {
|
|
998
|
-
result.add(JSON.parse(selector.slice('string:'.length)));
|
|
999
|
-
}
|
|
1000
|
-
catch {
|
|
1001
|
-
continue;
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
return result;
|
|
1005
|
-
}
|
|
1006
|
-
function classOwner(collection) {
|
|
1007
|
-
return collection.ephemeral || collection.owner === basename(collection.path)
|
|
1008
|
-
? null
|
|
1009
|
-
: collection.owner;
|
|
1010
|
-
}
|
|
1011
|
-
function collectionBindingsForFile(path, statements, own, all, imports, exportsByPath) {
|
|
1012
|
-
const bindings = new Map();
|
|
1013
|
-
const classSymbolCounts = new Map();
|
|
1014
|
-
for (const collection of own.values()) {
|
|
1015
|
-
if (classOwner(collection)) {
|
|
1016
|
-
classSymbolCounts.set(collection.symbol, (classSymbolCounts.get(collection.symbol) ?? 0) + 1);
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
for (const collection of own.values()) {
|
|
1020
|
-
const owner = classOwner(collection);
|
|
1021
|
-
bindings.set(collection.symbol, collection);
|
|
1022
|
-
if (owner) {
|
|
1023
|
-
bindings.set(`${owner}.${collection.symbol}`, collection);
|
|
1024
|
-
if (classSymbolCounts.get(collection.symbol) === 1) {
|
|
1025
|
-
bindings.set(`self.${collection.symbol}`, collection);
|
|
1026
|
-
bindings.set(`cls.${collection.symbol}`, collection);
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
for (const [binding, target] of imports) {
|
|
1031
|
-
const imported = all.get(target.path);
|
|
1032
|
-
if (!imported)
|
|
1033
|
-
continue;
|
|
1034
|
-
if (target.symbol === '*') {
|
|
1035
|
-
const exported = exportsByPath.get(target.path) ?? null;
|
|
1036
|
-
for (const collection of imported.values()) {
|
|
1037
|
-
const owner = classOwner(collection);
|
|
1038
|
-
const exportedRoot = owner?.split('.')[0] ?? collection.symbol;
|
|
1039
|
-
if (exported ? !exported.has(exportedRoot) : exportedRoot.startsWith('_'))
|
|
1040
|
-
continue;
|
|
1041
|
-
const exposed = owner ? `${owner}.${collection.symbol}` : collection.symbol;
|
|
1042
|
-
bindings.set(binding === '*' ? exposed : `${binding}.${exposed}`, collection);
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
else {
|
|
1046
|
-
const moduleCollection = [...imported.values()].find((collection) => !classOwner(collection) && collection.symbol === target.symbol);
|
|
1047
|
-
if (moduleCollection)
|
|
1048
|
-
bindings.set(binding, moduleCollection);
|
|
1049
|
-
for (const collection of imported.values()) {
|
|
1050
|
-
const owner = classOwner(collection);
|
|
1051
|
-
if (owner?.split('.')[0] === target.symbol) {
|
|
1052
|
-
const suffix = owner.split('.').slice(1);
|
|
1053
|
-
bindings.set([binding, ...suffix, collection.symbol].join('.'), collection);
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
let changed = true;
|
|
1059
|
-
for (let pass = 0; changed && pass < statements.length + 1; pass += 1) {
|
|
1060
|
-
changed = false;
|
|
1061
|
-
for (const statement of statements) {
|
|
1062
|
-
const alias = statement.text.match(/^([A-Za-z_][A-Za-z0-9_.]*)\s*=\s*([A-Za-z_][A-Za-z0-9_.]*)$/);
|
|
1063
|
-
if (!alias || bindings.has(alias[1]))
|
|
1064
|
-
continue;
|
|
1065
|
-
const collection = bindings.get(alias[2]);
|
|
1066
|
-
if (collection) {
|
|
1067
|
-
bindings.set(alias[1], collection);
|
|
1068
|
-
changed = true;
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
return [...bindings].map(([binding, collection]) => ({ binding, collection }));
|
|
1073
|
-
}
|
|
1074
|
-
function bindingAppliesToStatement(binding, collection, statement) {
|
|
1075
|
-
if (collection.ephemeral)
|
|
1076
|
-
return statement.owner === collection.owner;
|
|
1077
|
-
const owner = classOwner(collection);
|
|
1078
|
-
if (!owner)
|
|
1079
|
-
return true;
|
|
1080
|
-
if (binding === collection.symbol || binding.startsWith('self.') || binding.startsWith('cls.')) {
|
|
1081
|
-
return statement.owner === owner || statement.owner.startsWith(`${owner}.`);
|
|
1082
|
-
}
|
|
1083
|
-
return true;
|
|
1084
|
-
}
|
|
1085
|
-
function resolveImplementationIdentity(expression, sourcePath, imports) {
|
|
1086
|
-
if (expression === '$selector')
|
|
1087
|
-
return expression;
|
|
1088
|
-
const root = expression.match(/^([A-Za-z_][A-Za-z0-9_]*)/)?.[1];
|
|
1089
|
-
const target = root ? imports.get(root) : undefined;
|
|
1090
|
-
if (!target)
|
|
1091
|
-
return `${sourcePath}#${expression}`;
|
|
1092
|
-
const suffix = expression.slice(root.length).replace(/^\./, '.');
|
|
1093
|
-
return target.symbol === '*'
|
|
1094
|
-
? `${target.path}#${suffix.replace(/^\./, '')}`
|
|
1095
|
-
: `${target.path}#${target.symbol}${suffix}`;
|
|
1096
|
-
}
|
|
1097
|
-
function emitStaticCollection(collection, emissionKind, gate, loopVariables, sourceImports, records) {
|
|
1098
|
-
const familyKind = emissionKind === 'mapping'
|
|
1099
|
-
? 'python-static-mapping-selector-family'
|
|
1100
|
-
: emissionKind === 'choice'
|
|
1101
|
-
? 'python-static-choice-selector-family'
|
|
1102
|
-
: 'python-static-registration-selector-family';
|
|
1103
|
-
const entryKind = emissionKind === 'mapping'
|
|
1104
|
-
? 'python-static-mapping-selector-entry'
|
|
1105
|
-
: emissionKind === 'choice'
|
|
1106
|
-
? 'python-static-choice-selector-entry'
|
|
1107
|
-
: 'python-static-registration-selector-entry';
|
|
1108
|
-
const ownerIdentity = `${collection.path}#${collection.owner}.${collection.symbol}`;
|
|
1109
|
-
const signature = sha256(collection.entries.map((entry) => `${entry.selector}\0${entry.implementation}`).join('\0')).slice(0, 24);
|
|
1110
|
-
const identity = `${emissionKind}:${collection.path}#${collection.symbol}:${signature}`;
|
|
1111
|
-
const familyId = recordStableId(familyKind, gate.path, ownerIdentity, identity);
|
|
1112
|
-
records.push({
|
|
1113
|
-
kind: familyKind,
|
|
1114
|
-
projections: ['registry_families'],
|
|
1115
|
-
presumptiveInventory: true,
|
|
1116
|
-
proofStatus: 'proof-complete',
|
|
1117
|
-
path: gate.path,
|
|
1118
|
-
line: gate.line,
|
|
1119
|
-
anchor: gate.anchor,
|
|
1120
|
-
owner: ownerIdentity,
|
|
1121
|
-
identity,
|
|
1122
|
-
tokens: [collection.symbol, canonicalExpression(gate.statement), 'proof-complete:python-static-registry'],
|
|
1123
|
-
semanticIdentityKey: `python-registry-family\0${ownerIdentity}\0${emissionKind}\0${signature}`,
|
|
1124
|
-
identityComponents: {
|
|
1125
|
-
identity_kind: 'python-registry-family',
|
|
1126
|
-
owner_identity: ownerIdentity,
|
|
1127
|
-
selector_domain: `${collection.path}#${collection.symbol}`,
|
|
1128
|
-
},
|
|
1129
|
-
});
|
|
1130
|
-
collection.entries.forEach((entry) => {
|
|
1131
|
-
let implementation = resolveImplementationIdentity(entry.implementation, collection.path, sourceImports);
|
|
1132
|
-
if (entry.implementation === '$selector' && loopVariables) {
|
|
1133
|
-
let instantiated = canonicalExpression(gate.statement);
|
|
1134
|
-
instantiated = instantiated.replace(new RegExp(`\\b${escaped(loopVariables[0])}\\b`, 'g'), JSON.stringify(entry.selector));
|
|
1135
|
-
implementation = `${gate.path}#registration:${instantiated}`;
|
|
1136
|
-
}
|
|
1137
|
-
const entryIdentity = `${identity}:selector:${entry.selector}:implementation:${implementation}`;
|
|
1138
|
-
records.push({
|
|
1139
|
-
kind: entryKind,
|
|
1140
|
-
projections: ['registry_entries'],
|
|
1141
|
-
presumptiveInventory: true,
|
|
1142
|
-
proofStatus: 'proof-complete',
|
|
1143
|
-
path: collection.path,
|
|
1144
|
-
line: entry.line,
|
|
1145
|
-
anchor: entry.anchor,
|
|
1146
|
-
owner: ownerIdentity,
|
|
1147
|
-
identity: entryIdentity,
|
|
1148
|
-
tokens: [entry.selector, implementation, 'proof-complete:python-static-registry'],
|
|
1149
|
-
selectorKey: entry.selector,
|
|
1150
|
-
implementationKey: implementation,
|
|
1151
|
-
selectorFamilyRecordId: familyId,
|
|
1152
|
-
semanticIdentityKey: `python-registry-entry\0${ownerIdentity}\0${emissionKind}\0${entry.selector}\0${implementation}`,
|
|
1153
|
-
identityComponents: {
|
|
1154
|
-
identity_kind: 'python-registry-entry',
|
|
1155
|
-
owner_identity: ownerIdentity,
|
|
1156
|
-
selector_domain: `${collection.path}#${collection.symbol}`,
|
|
1157
|
-
selector_key: entry.selector,
|
|
1158
|
-
implementation_identity: implementation,
|
|
1159
|
-
},
|
|
1160
|
-
});
|
|
1161
|
-
});
|
|
1162
|
-
}
|
|
1163
|
-
function parseCopyAliases(repositoryRoot, trackedPaths, sources) {
|
|
1164
|
-
const knownPaths = new Set(trackedPaths);
|
|
1165
|
-
const aliases = new Map();
|
|
1166
|
-
const resolvePair = (scriptPath, rawSource, rawTarget) => {
|
|
1167
|
-
if (/^(?:[A-Za-z]+:|\/)/.test(rawSource) || /^(?:[A-Za-z]+:|\/)/.test(rawTarget))
|
|
1168
|
-
return;
|
|
1169
|
-
const sourceCandidates = [
|
|
1170
|
-
normalizePath(rawSource),
|
|
1171
|
-
normalizePath(posix.join(dirname(scriptPath), rawSource)),
|
|
1172
|
-
];
|
|
1173
|
-
const targetCandidates = [
|
|
1174
|
-
normalizePath(rawTarget),
|
|
1175
|
-
normalizePath(posix.join(dirname(scriptPath), rawTarget)),
|
|
1176
|
-
];
|
|
1177
|
-
const source = sourceCandidates.find((candidate) => knownPaths.has(candidate) || trackedPaths.some((path) => path.startsWith(`${candidate}/`)));
|
|
1178
|
-
if (!source)
|
|
1179
|
-
return;
|
|
1180
|
-
let target = targetCandidates.find((candidate) => knownPaths.has(candidate));
|
|
1181
|
-
if (!target) {
|
|
1182
|
-
target = targetCandidates.map((candidate) => normalizePath(posix.join(candidate, basename(source))))
|
|
1183
|
-
.find((candidate) => knownPaths.has(candidate));
|
|
1184
|
-
}
|
|
1185
|
-
if (!target && !knownPaths.has(source)) {
|
|
1186
|
-
target = targetCandidates.find((candidate) => trackedPaths.some((path) => path.startsWith(`${candidate}/`)));
|
|
1187
|
-
}
|
|
1188
|
-
if (target && target !== source)
|
|
1189
|
-
aliases.set(target, source);
|
|
1190
|
-
};
|
|
1191
|
-
for (const [path, source] of sources) {
|
|
1192
|
-
if (isTestPath(path) || isDependencyPath(path))
|
|
1193
|
-
continue;
|
|
1194
|
-
for (const match of source.matchAll(/\b(?:shutil\.)?(?:copy|copy2|copyfile|copytree)\(\s*(['"])([^'"]+)\1\s*,\s*(['"])([^'"]+)\3/g))
|
|
1195
|
-
resolvePair(path, match[2], match[4]);
|
|
1196
|
-
for (const line of source.split(/\r?\n/)) {
|
|
1197
|
-
const match = stripPythonComment(line).match(/^\s*cp\s+(?:-[A-Za-z]+\s+)?(?:['"]([^'"]+)['"]|(\S+))\s+(?:['"]([^'"]+)['"]|(\S+))\s*$/);
|
|
1198
|
-
if (match)
|
|
1199
|
-
resolvePair(path, match[1] ?? match[2], match[3] ?? match[4]);
|
|
1200
|
-
}
|
|
1201
|
-
if (!['.toml', '.yaml', '.yml'].includes(extname(path).toLowerCase()))
|
|
1202
|
-
continue;
|
|
1203
|
-
let section = '';
|
|
1204
|
-
let structuredSource = null;
|
|
1205
|
-
for (const line of source.split(/\r?\n/)) {
|
|
1206
|
-
const header = line.match(/^\s*\[([^\]]+)]\s*$/);
|
|
1207
|
-
if (header) {
|
|
1208
|
-
section = header[1];
|
|
1209
|
-
structuredSource = null;
|
|
1210
|
-
continue;
|
|
1211
|
-
}
|
|
1212
|
-
const parts = identifierParts(section);
|
|
1213
|
-
if (![...parts].some((part) => ['copy', 'package', 'resource'].includes(part)))
|
|
1214
|
-
continue;
|
|
1215
|
-
const property = line.match(/^\s*(source|destination|target)\s*[:=]\s*['"]([^'"]+)['"]\s*$/);
|
|
1216
|
-
if (!property)
|
|
1217
|
-
continue;
|
|
1218
|
-
if (property[1] === 'source')
|
|
1219
|
-
structuredSource = property[2];
|
|
1220
|
-
else if (structuredSource)
|
|
1221
|
-
resolvePair(path, structuredSource, property[2]);
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
const hashes = new Map();
|
|
1225
|
-
const blobHash = (path) => {
|
|
1226
|
-
if (hashes.has(path))
|
|
1227
|
-
return hashes.get(path) ?? null;
|
|
1228
|
-
try {
|
|
1229
|
-
const stat = statSync(join(repositoryRoot, path));
|
|
1230
|
-
const hash = stat.isFile() && stat.size <= MAX_SOURCE_BYTES
|
|
1231
|
-
? sha256(readFileSync(join(repositoryRoot, path)))
|
|
1232
|
-
: null;
|
|
1233
|
-
hashes.set(path, hash);
|
|
1234
|
-
return hash;
|
|
1235
|
-
}
|
|
1236
|
-
catch {
|
|
1237
|
-
hashes.set(path, null);
|
|
1238
|
-
return null;
|
|
1239
|
-
}
|
|
1240
|
-
};
|
|
1241
|
-
for (const target of trackedPaths) {
|
|
1242
|
-
if (aliases.has(target) || isTestPath(target))
|
|
1243
|
-
continue;
|
|
1244
|
-
const tail = structuredPackageResourceTail(target);
|
|
1245
|
-
if (!tail)
|
|
1246
|
-
continue;
|
|
1247
|
-
const targetHash = blobHash(target);
|
|
1248
|
-
if (!targetHash)
|
|
1249
|
-
continue;
|
|
1250
|
-
const candidates = trackedPaths.filter((candidate) => candidate !== target
|
|
1251
|
-
&& structuredPackageResourceTail(candidate) === null
|
|
1252
|
-
&& (candidate === tail || candidate.endsWith(`/${tail}`))
|
|
1253
|
-
&& blobHash(candidate) === targetHash);
|
|
1254
|
-
if (candidates.length === 1)
|
|
1255
|
-
aliases.set(target, candidates[0]);
|
|
1256
|
-
}
|
|
1257
|
-
const packageDirectories = [...trackedDirectories(trackedPaths)]
|
|
1258
|
-
.filter((directory) => structuredPackageResourceTail(`${directory}/placeholder`) !== null)
|
|
1259
|
-
.sort((left, right) => right.length - left.length);
|
|
1260
|
-
for (const targetDirectory of packageDirectories) {
|
|
1261
|
-
const descendants = trackedPaths.filter((path) => path.startsWith(`${targetDirectory}/`));
|
|
1262
|
-
if (descendants.length === 0)
|
|
1263
|
-
continue;
|
|
1264
|
-
const sourceDirectories = new Set();
|
|
1265
|
-
let complete = true;
|
|
1266
|
-
for (const target of descendants) {
|
|
1267
|
-
const source = aliases.get(target);
|
|
1268
|
-
const relative = target.slice(targetDirectory.length + 1);
|
|
1269
|
-
if (!source || !(source === relative || source.endsWith(`/${relative}`))) {
|
|
1270
|
-
complete = false;
|
|
1271
|
-
break;
|
|
1272
|
-
}
|
|
1273
|
-
sourceDirectories.add(source.slice(0, source.length - relative.length).replace(/\/$/, ''));
|
|
1274
|
-
}
|
|
1275
|
-
if (complete && sourceDirectories.size === 1) {
|
|
1276
|
-
const sourceDirectory = [...sourceDirectories][0];
|
|
1277
|
-
if (sourceDirectory)
|
|
1278
|
-
aliases.set(targetDirectory, sourceDirectory);
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
return aliases;
|
|
1282
|
-
}
|
|
1283
|
-
function structuredPackageResourceTail(path) {
|
|
1284
|
-
const components = normalizePath(path).split('/');
|
|
1285
|
-
const packageSuffixes = ['.app', '.appex', '.bundle', '.plugin', '.xpc'];
|
|
1286
|
-
for (let index = 0; index < components.length; index += 1) {
|
|
1287
|
-
const lower = components[index].toLowerCase();
|
|
1288
|
-
if (packageSuffixes.some((suffix) => lower.endsWith(suffix))) {
|
|
1289
|
-
if (components[index + 1]?.toLowerCase() === 'contents'
|
|
1290
|
-
&& ['plugins', 'resources', 'sharedsupport'].includes(components[index + 2]?.toLowerCase()))
|
|
1291
|
-
return components.slice(index + 3).join('/') || null;
|
|
1292
|
-
if (components[index + 1]?.toLowerCase() === 'resources') {
|
|
1293
|
-
return components.slice(index + 2).join('/') || null;
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
if (lower.endsWith('.framework') && components[index + 1]?.toLowerCase() === 'resources') {
|
|
1297
|
-
return components.slice(index + 2).join('/') || null;
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
return null;
|
|
1301
|
-
}
|
|
1302
|
-
function canonicalCopiedPath(path, aliases) {
|
|
1303
|
-
let current = normalizePath(path);
|
|
1304
|
-
const seen = new Set();
|
|
1305
|
-
while (!seen.has(current)) {
|
|
1306
|
-
seen.add(current);
|
|
1307
|
-
const exact = aliases.get(current);
|
|
1308
|
-
if (exact) {
|
|
1309
|
-
current = exact;
|
|
1310
|
-
continue;
|
|
1311
|
-
}
|
|
1312
|
-
const prefix = [...aliases.keys()]
|
|
1313
|
-
.filter((candidate) => current.startsWith(`${candidate}/`))
|
|
1314
|
-
.sort((left, right) => right.length - left.length)[0];
|
|
1315
|
-
if (!prefix)
|
|
1316
|
-
break;
|
|
1317
|
-
current = normalizePath(posix.join(aliases.get(prefix), current.slice(prefix.length + 1)));
|
|
1318
|
-
}
|
|
1319
|
-
return current;
|
|
1320
|
-
}
|
|
1321
|
-
function parseIni(source) {
|
|
1322
|
-
const sections = new Map();
|
|
1323
|
-
let section = '';
|
|
1324
|
-
source.split(/\r?\n/).forEach((line, index) => {
|
|
1325
|
-
const header = line.match(/^\s*\[([^\]]+)]\s*(?:[;#].*)?$/);
|
|
1326
|
-
if (header) {
|
|
1327
|
-
section = header[1].trim();
|
|
1328
|
-
sections.set(section, sections.get(section) ?? []);
|
|
1329
|
-
return;
|
|
1330
|
-
}
|
|
1331
|
-
if (!section || /^\s*[;#]/.test(line))
|
|
1332
|
-
return;
|
|
1333
|
-
const property = line.match(/^\s*([^=]+?)\s*=\s*(.*?)\s*$/);
|
|
1334
|
-
if (!property)
|
|
1335
|
-
return;
|
|
1336
|
-
const values = sections.get(section) ?? [];
|
|
1337
|
-
values.push({ key: property[1].trim(), value: property[2], line: index + 1, anchor: line });
|
|
1338
|
-
sections.set(section, values);
|
|
1339
|
-
});
|
|
1340
|
-
return sections;
|
|
1341
|
-
}
|
|
1342
|
-
function firstIniValue(entries, keys) {
|
|
1343
|
-
for (const key of keys) {
|
|
1344
|
-
const entry = entries.find((candidate) => candidate.key.toLowerCase() === key);
|
|
1345
|
-
if (entry && entry.value !== '')
|
|
1346
|
-
return entry.value;
|
|
1347
|
-
}
|
|
1348
|
-
return null;
|
|
1349
|
-
}
|
|
1350
|
-
function scanInstallManifests(root, paths, sources, aliases, records) {
|
|
1351
|
-
const seen = new Set();
|
|
1352
|
-
for (const originalPath of paths.filter((path) => basename(path).toLowerCase() === 'install.inf')) {
|
|
1353
|
-
if (isTestPath(originalPath) || isDependencyPath(originalPath))
|
|
1354
|
-
continue;
|
|
1355
|
-
const path = canonicalCopiedPath(originalPath, aliases);
|
|
1356
|
-
if (seen.has(path))
|
|
1357
|
-
continue;
|
|
1358
|
-
const source = sources.get(path) ?? sources.get(originalPath) ?? readText(root, path);
|
|
1359
|
-
if (!source)
|
|
1360
|
-
continue;
|
|
1361
|
-
const sections = parseIni(source);
|
|
1362
|
-
const infoName = [...sections.keys()].find((section) => section.toLowerCase() === 'info');
|
|
1363
|
-
if (!infoName)
|
|
1364
|
-
continue;
|
|
1365
|
-
const info = sections.get(infoName);
|
|
1366
|
-
const namespace = firstIniValue(info, MANIFEST_NAMESPACE_KEYS);
|
|
1367
|
-
const manifestType = firstIniValue(info, ['type']);
|
|
1368
|
-
if (!namespace)
|
|
1369
|
-
continue;
|
|
1370
|
-
const entries = [];
|
|
1371
|
-
for (const [section, properties] of sections) {
|
|
1372
|
-
if (section.toLowerCase() === 'info' || properties.length === 0)
|
|
1373
|
-
continue;
|
|
1374
|
-
const method = firstIniValue(properties, ['method']);
|
|
1375
|
-
const implementation = method
|
|
1376
|
-
? `${namespace}.${method}`
|
|
1377
|
-
: `${path}#[${section}]:declarative:${sha256(properties.map((property) => `${property.key}\0${property.value}`).join('\0')).slice(0, 24)}`;
|
|
1378
|
-
const evidence = properties[0];
|
|
1379
|
-
entries.push({
|
|
1380
|
-
selector: section,
|
|
1381
|
-
implementation,
|
|
1382
|
-
line: evidence.line,
|
|
1383
|
-
anchor: evidence.anchor,
|
|
1384
|
-
});
|
|
1385
|
-
}
|
|
1386
|
-
if (entries.length === 0)
|
|
1387
|
-
continue;
|
|
1388
|
-
const owner = `install.inf:${path}#manifest`;
|
|
1389
|
-
const identity = `${path}#manifest`;
|
|
1390
|
-
const familyKind = 'install-inf-selector-family';
|
|
1391
|
-
const familyId = recordStableId(familyKind, path, owner, identity);
|
|
1392
|
-
const first = entries[0];
|
|
1393
|
-
records.push({
|
|
1394
|
-
kind: familyKind,
|
|
1395
|
-
projections: ['registry_families'],
|
|
1396
|
-
presumptiveInventory: true,
|
|
1397
|
-
proofStatus: 'proof-complete',
|
|
1398
|
-
path,
|
|
1399
|
-
line: first.line,
|
|
1400
|
-
anchor: first.anchor,
|
|
1401
|
-
owner,
|
|
1402
|
-
identity,
|
|
1403
|
-
tokens: [namespace, ...(manifestType ? [manifestType] : []), ...entries.map((entry) => entry.selector),
|
|
1404
|
-
'proof-complete:install-inf-registry'],
|
|
1405
|
-
semanticIdentityKey: `manifest-registry-family\0${path}\0manifest`,
|
|
1406
|
-
identityComponents: {
|
|
1407
|
-
identity_kind: 'manifest-registry-family',
|
|
1408
|
-
owner_identity: `${path}#manifest`,
|
|
1409
|
-
selector_domain: `${path}#manifest-sections`,
|
|
1410
|
-
},
|
|
1411
|
-
});
|
|
1412
|
-
entries.forEach((entry) => {
|
|
1413
|
-
const entryIdentity = `${identity}:section:${entry.selector}:implementation:${entry.implementation}`;
|
|
1414
|
-
records.push({
|
|
1415
|
-
kind: 'install-inf-selector-entry',
|
|
1416
|
-
projections: ['registry_entries'],
|
|
1417
|
-
presumptiveInventory: true,
|
|
1418
|
-
proofStatus: 'proof-complete',
|
|
1419
|
-
path,
|
|
1420
|
-
line: entry.line,
|
|
1421
|
-
anchor: entry.anchor,
|
|
1422
|
-
owner,
|
|
1423
|
-
identity: entryIdentity,
|
|
1424
|
-
tokens: [entry.selector, entry.implementation, 'proof-complete:install-inf-registry'],
|
|
1425
|
-
selectorKey: entry.selector,
|
|
1426
|
-
implementationKey: entry.implementation,
|
|
1427
|
-
selectorFamilyRecordId: familyId,
|
|
1428
|
-
semanticIdentityKey: `manifest-registry-entry\0${path}\0${entry.selector}\0${entry.implementation}`,
|
|
1429
|
-
identityComponents: {
|
|
1430
|
-
identity_kind: 'manifest-registry-entry',
|
|
1431
|
-
owner_identity: `${path}#manifest`,
|
|
1432
|
-
selector_domain: `${path}#manifest-sections`,
|
|
1433
|
-
selector_key: entry.selector,
|
|
1434
|
-
implementation_identity: entry.implementation,
|
|
1435
|
-
},
|
|
1436
|
-
});
|
|
1437
|
-
});
|
|
1438
|
-
seen.add(path);
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
function trackedDirectories(paths) {
|
|
1442
|
-
const result = new Set();
|
|
1443
|
-
for (const path of paths) {
|
|
1444
|
-
const components = normalizePath(path).split('/');
|
|
1445
|
-
for (let length = 1; length < components.length; length += 1) {
|
|
1446
|
-
result.add(components.slice(0, length).join('/'));
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1449
|
-
return result;
|
|
1450
|
-
}
|
|
1451
|
-
function exactStringLiterals(expression) {
|
|
1452
|
-
return [...expression.matchAll(/(['"])([^'"]+)\1/g)].map((match) => match[2]);
|
|
1453
|
-
}
|
|
1454
|
-
function uniqueDirectory(candidates, directories) {
|
|
1455
|
-
const existing = [...new Set([...candidates].map(normalizePath))]
|
|
1456
|
-
.filter((candidate) => directories.has(candidate));
|
|
1457
|
-
return existing.length === 1 ? existing[0] : null;
|
|
1458
|
-
}
|
|
1459
|
-
function pythonMaterializationRoots(path, statements, trackedPaths, modulePaths, aliases) {
|
|
1460
|
-
const directories = new Set([...trackedDirectories(trackedPaths)]
|
|
1461
|
-
.filter((directory) => !isTestPath(`${directory}/placeholder`) && !isDependencyPath(`${directory}/placeholder`))
|
|
1462
|
-
.map((directory) => canonicalCopiedPath(directory, aliases)));
|
|
1463
|
-
const roots = new Map();
|
|
1464
|
-
const assignments = statements.flatMap((statement) => {
|
|
1465
|
-
const match = statement.text.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*(?::[^=]+)?=\s*(.+)$/);
|
|
1466
|
-
return match ? [{ statement, symbol: match[1], expression: match[2] }] : [];
|
|
1467
|
-
});
|
|
1468
|
-
const literalConstants = new Map();
|
|
1469
|
-
assignments.forEach((assignment) => {
|
|
1470
|
-
const literal = assignment.expression.match(/^(['"])([^'"]+)\1$/);
|
|
1471
|
-
if (literal)
|
|
1472
|
-
literalConstants.set(assignment.symbol, literal[2]);
|
|
1473
|
-
});
|
|
1474
|
-
const resolved = new Map();
|
|
1475
|
-
let changed = true;
|
|
1476
|
-
for (let pass = 0; changed && pass < assignments.length + 2; pass += 1) {
|
|
1477
|
-
changed = false;
|
|
1478
|
-
for (const assignment of assignments) {
|
|
1479
|
-
const candidates = new Set(resolved.get(assignment.symbol) ?? []);
|
|
1480
|
-
const literals = exactStringLiterals(assignment.expression);
|
|
1481
|
-
if (/\bfiles\s*\(/.test(assignment.expression) && literals.length > 0) {
|
|
1482
|
-
const modulePath = modulePaths.get(literals[0]);
|
|
1483
|
-
if (modulePath)
|
|
1484
|
-
candidates.add(normalizePath(posix.join(dirname(modulePath), ...literals.slice(1))));
|
|
1485
|
-
}
|
|
1486
|
-
if (/\b__file__\b/.test(assignment.expression)) {
|
|
1487
|
-
const parentCount = (assignment.expression.match(/\.parent\b/g) ?? []).length
|
|
1488
|
-
+ (assignment.expression.match(/(?:os\.path\.)?dirname\s*\(\s*__file__\s*\)/g) ?? []).length;
|
|
1489
|
-
let basePath = path;
|
|
1490
|
-
for (let count = 0; count < Math.max(1, parentCount); count += 1)
|
|
1491
|
-
basePath = dirname(basePath);
|
|
1492
|
-
const pathLiterals = /\bfiles\s*\(/.test(assignment.expression) ? literals.slice(1) : literals;
|
|
1493
|
-
candidates.add(normalizePath(posix.join(basePath, ...pathLiterals)));
|
|
1494
|
-
}
|
|
1495
|
-
else if (/\bPath\s*\(/.test(assignment.expression) && literals.length > 0) {
|
|
1496
|
-
candidates.add(normalizePath(posix.join(...literals)));
|
|
1497
|
-
candidates.add(normalizePath(posix.join(dirname(path), ...literals)));
|
|
1498
|
-
}
|
|
1499
|
-
if (/\bos\.path\.join\s*\(/.test(assignment.expression) && literals.length > 0) {
|
|
1500
|
-
const suffix = normalizePath(posix.join(...literals));
|
|
1501
|
-
directories.forEach((directory) => {
|
|
1502
|
-
if (directory === suffix || directory.endsWith(`/${suffix}`))
|
|
1503
|
-
candidates.add(directory);
|
|
1504
|
-
});
|
|
1505
|
-
}
|
|
1506
|
-
for (const [binding, baseCandidates] of resolved) {
|
|
1507
|
-
if (!new RegExp(`\\b${escaped(binding)}\\b`).test(assignment.expression))
|
|
1508
|
-
continue;
|
|
1509
|
-
const suffixes = exactStringLiterals(assignment.expression);
|
|
1510
|
-
for (const reference of assignment.expression.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? []) {
|
|
1511
|
-
const constant = literalConstants.get(reference);
|
|
1512
|
-
if (constant)
|
|
1513
|
-
suffixes.push(constant);
|
|
1514
|
-
}
|
|
1515
|
-
const exactAlias = canonicalExpression(assignment.expression) === binding;
|
|
1516
|
-
if (!exactAlias && suffixes.length === 0)
|
|
1517
|
-
continue;
|
|
1518
|
-
for (const base of baseCandidates)
|
|
1519
|
-
candidates.add(normalizePath(posix.join(base, ...suffixes)));
|
|
1520
|
-
}
|
|
1521
|
-
const existing = new Set([...candidates]
|
|
1522
|
-
.map((candidate) => canonicalCopiedPath(candidate, aliases))
|
|
1523
|
-
.filter((candidate) => directories.has(candidate)));
|
|
1524
|
-
const before = resolved.get(assignment.symbol);
|
|
1525
|
-
if (existing.size > 0 && (!before || [...existing].some((candidate) => !before.has(candidate)))) {
|
|
1526
|
-
resolved.set(assignment.symbol, existing);
|
|
1527
|
-
changed = true;
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
for (const assignment of assignments) {
|
|
1532
|
-
const rootPath = uniqueDirectory(resolved.get(assignment.symbol) ?? [], directories);
|
|
1533
|
-
if (!rootPath || roots.has(assignment.symbol))
|
|
1534
|
-
continue;
|
|
1535
|
-
roots.set(assignment.symbol, {
|
|
1536
|
-
path,
|
|
1537
|
-
symbol: assignment.symbol,
|
|
1538
|
-
owner: assignment.statement.owner,
|
|
1539
|
-
line: assignment.statement.startLine,
|
|
1540
|
-
anchor: assignment.statement.anchor,
|
|
1541
|
-
rootPath,
|
|
1542
|
-
});
|
|
1543
|
-
}
|
|
1544
|
-
return roots;
|
|
1545
|
-
}
|
|
1546
|
-
function pascalStatements(path, source) {
|
|
1547
|
-
const result = [];
|
|
1548
|
-
let blockComment = false;
|
|
1549
|
-
source.split(/\r?\n/).forEach((original, index) => {
|
|
1550
|
-
let line = original;
|
|
1551
|
-
if (blockComment) {
|
|
1552
|
-
const end = line.indexOf('}');
|
|
1553
|
-
if (end < 0)
|
|
1554
|
-
return;
|
|
1555
|
-
line = line.slice(end + 1);
|
|
1556
|
-
blockComment = false;
|
|
1557
|
-
}
|
|
1558
|
-
const start = line.indexOf('{');
|
|
1559
|
-
if (start >= 0) {
|
|
1560
|
-
const end = line.indexOf('}', start + 1);
|
|
1561
|
-
if (end >= 0)
|
|
1562
|
-
line = `${line.slice(0, start)} ${line.slice(end + 1)}`;
|
|
1563
|
-
else {
|
|
1564
|
-
line = line.slice(0, start);
|
|
1565
|
-
blockComment = true;
|
|
1566
|
-
}
|
|
1567
|
-
}
|
|
1568
|
-
line = line.replace(/\/\/.*$/, '').trim();
|
|
1569
|
-
if (!line)
|
|
1570
|
-
return;
|
|
1571
|
-
result.push({
|
|
1572
|
-
path,
|
|
1573
|
-
startLine: index + 1,
|
|
1574
|
-
endLine: index + 1,
|
|
1575
|
-
indent: indentation(original),
|
|
1576
|
-
text: line.replace(/;\s*$/, ''),
|
|
1577
|
-
anchor: original,
|
|
1578
|
-
owner: basename(path),
|
|
1579
|
-
scope: 'module',
|
|
1580
|
-
});
|
|
1581
|
-
});
|
|
1582
|
-
return result;
|
|
1583
|
-
}
|
|
1584
|
-
function pascalMaterializationRoots(path, statements, trackedPaths, aliases) {
|
|
1585
|
-
const directories = new Set([...trackedDirectories(trackedPaths)]
|
|
1586
|
-
.filter((directory) => !isTestPath(`${directory}/placeholder`) && !isDependencyPath(`${directory}/placeholder`))
|
|
1587
|
-
.map((directory) => canonicalCopiedPath(directory, aliases)));
|
|
1588
|
-
const roots = new Map();
|
|
1589
|
-
const literalConstants = new Map();
|
|
1590
|
-
const assignments = statements.flatMap((statement) => {
|
|
1591
|
-
const match = statement.text.match(/^(?:const\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*(?::?=)\s*(.+)$/i);
|
|
1592
|
-
if (!match)
|
|
1593
|
-
return [];
|
|
1594
|
-
const literal = match[2].match(/^(['"])([^'"]+)\1$/);
|
|
1595
|
-
if (literal)
|
|
1596
|
-
literalConstants.set(match[1].toLowerCase(), literal[2]);
|
|
1597
|
-
return [{ statement, symbol: match[1], expression: match[2] }];
|
|
1598
|
-
});
|
|
1599
|
-
const resolved = new Map();
|
|
1600
|
-
let changed = true;
|
|
1601
|
-
for (let pass = 0; changed && pass < assignments.length + 2; pass += 1) {
|
|
1602
|
-
changed = false;
|
|
1603
|
-
for (const assignment of assignments) {
|
|
1604
|
-
const candidates = new Set(resolved.get(assignment.symbol.toLowerCase()) ?? []);
|
|
1605
|
-
const literals = exactStringLiterals(assignment.expression);
|
|
1606
|
-
for (const reference of assignment.expression.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? []) {
|
|
1607
|
-
const constant = literalConstants.get(reference.toLowerCase());
|
|
1608
|
-
if (constant)
|
|
1609
|
-
literals.push(constant);
|
|
1610
|
-
for (const base of resolved.get(reference.toLowerCase()) ?? []) {
|
|
1611
|
-
candidates.add(normalizePath(posix.join(base, ...literals)));
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
if (literals.length > 0 && /(?:ExtractFile|IncludeTrailingPathDelimiter|TPath\.Combine|PathDelim|DirectorySeparator)/i.test(assignment.expression)) {
|
|
1615
|
-
const suffix = normalizePath(posix.join(...literals));
|
|
1616
|
-
directories.forEach((directory) => {
|
|
1617
|
-
if (directory === suffix || directory.endsWith(`/${suffix}`))
|
|
1618
|
-
candidates.add(directory);
|
|
1619
|
-
});
|
|
1620
|
-
candidates.add(normalizePath(posix.join(dirname(path), suffix)));
|
|
1621
|
-
candidates.add(suffix);
|
|
1622
|
-
}
|
|
1623
|
-
const existing = new Set([...candidates]
|
|
1624
|
-
.map((candidate) => canonicalCopiedPath(candidate, aliases))
|
|
1625
|
-
.filter((candidate) => directories.has(candidate)));
|
|
1626
|
-
const key = assignment.symbol.toLowerCase();
|
|
1627
|
-
const before = resolved.get(key);
|
|
1628
|
-
if (existing.size > 0 && (!before || [...existing].some((candidate) => !before.has(candidate)))) {
|
|
1629
|
-
resolved.set(key, existing);
|
|
1630
|
-
changed = true;
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
for (const assignment of assignments) {
|
|
1635
|
-
const rootPath = uniqueDirectory(resolved.get(assignment.symbol.toLowerCase()) ?? [], directories);
|
|
1636
|
-
if (!rootPath || roots.has(assignment.symbol))
|
|
1637
|
-
continue;
|
|
1638
|
-
roots.set(assignment.symbol, {
|
|
1639
|
-
path,
|
|
1640
|
-
symbol: assignment.symbol,
|
|
1641
|
-
owner: assignment.statement.owner,
|
|
1642
|
-
line: assignment.statement.startLine,
|
|
1643
|
-
anchor: assignment.statement.anchor,
|
|
1644
|
-
rootPath,
|
|
1645
|
-
});
|
|
1646
|
-
}
|
|
1647
|
-
return roots;
|
|
1648
|
-
}
|
|
1649
|
-
function resourceUse(statement, binding, joinedPath = false) {
|
|
1650
|
-
const symbol = escaped(binding);
|
|
1651
|
-
const canonical = canonicalExpression(statement.text);
|
|
1652
|
-
if (joinedPath) {
|
|
1653
|
-
const directOpen = canonical.match(new RegExp(`open\\(${symbol}(?:,([^)]*))?\\)`));
|
|
1654
|
-
if (directOpen) {
|
|
1655
|
-
return /(['"])[wax+][^'"]*\1/.test(directOpen[1] ?? '') ? { kind: 'write' } : { kind: 'load' };
|
|
1656
|
-
}
|
|
1657
|
-
if (new RegExp(`${symbol}\\.(?:open|read_text|read_bytes)\\(`).test(canonical)
|
|
1658
|
-
|| new RegExp(`(?:InitLibrary|LoadFromDir|LoadFromFile|ReadAllText|ReadAllBytes|TFileStream\\.Create)\\(${symbol}(?:,|\\))`, 'i').test(canonical)) {
|
|
1659
|
-
return { kind: 'load' };
|
|
1660
|
-
}
|
|
1661
|
-
if (new RegExp(`${symbol}\\.(?:mkdir|touch|write_bytes|write_text)\\(`).test(canonical)
|
|
1662
|
-
|| new RegExp(`(?:SaveToFile|WriteAllText|WriteAllBytes|DeleteFile)\\(${symbol}(?:,|\\))`, 'i').test(canonical)) {
|
|
1663
|
-
return { kind: 'write' };
|
|
1664
|
-
}
|
|
1665
|
-
}
|
|
1666
|
-
const pythonGlob = canonical.match(new RegExp(`${symbol}\\.(glob|rglob)\\((['"])([^'"]+)\\2\\)`));
|
|
1667
|
-
if (pythonGlob)
|
|
1668
|
-
return {
|
|
1669
|
-
kind: 'enumerate', filter: pythonGlob[3], recursive: pythonGlob[1] === 'rglob', entryKind: 'file',
|
|
1670
|
-
};
|
|
1671
|
-
if (new RegExp(`${symbol}\\.iterdir\\(`).test(canonical)
|
|
1672
|
-
|| new RegExp(`(?:os\\.)?(?:listdir|scandir)\\(${symbol}\\)`).test(canonical)) {
|
|
1673
|
-
const suffix = canonical.match(/\.suffix==(['"])([^'"]+)\1/)?.[2];
|
|
1674
|
-
return { kind: 'enumerate', filter: suffix ? `*${suffix}` : '*', recursive: false, entryKind: 'either' };
|
|
1675
|
-
}
|
|
1676
|
-
const findAllFiles = canonical.match(new RegExp(`FindAllFiles\\([^,]+,${symbol},(['"])([^'"]+)\\1,(true|false)\\)`, 'i'));
|
|
1677
|
-
if (findAllFiles)
|
|
1678
|
-
return {
|
|
1679
|
-
kind: 'enumerate', filter: findAllFiles[2], recursive: findAllFiles[3].toLowerCase() === 'true',
|
|
1680
|
-
entryKind: 'file',
|
|
1681
|
-
};
|
|
1682
|
-
const pascalEnumeration = canonical.match(new RegExp(`(?:FindFirst|TDirectory\\.GetFiles)\\([^)]*${symbol}[^'"]*(['"])([^'"]*[?*][^'"]*)\\1`, 'i'));
|
|
1683
|
-
if (pascalEnumeration)
|
|
1684
|
-
return {
|
|
1685
|
-
kind: 'enumerate', filter: pascalEnumeration[2], recursive: false, entryKind: 'file',
|
|
1686
|
-
};
|
|
1687
|
-
if (new RegExp(`(?:^|[^A-Za-z0-9_])LoadFromDir\\(${symbol}(?:,|\\))`, 'i').test(canonical)) {
|
|
1688
|
-
return { kind: 'materialize', filter: '*', recursive: false, entryKind: 'file' };
|
|
1689
|
-
}
|
|
1690
|
-
if (new RegExp(`(?:^|[^A-Za-z0-9_])InitLibrary\\(${symbol}(?:,|\\))`, 'i').test(canonical)) {
|
|
1691
|
-
return { kind: 'materialize', filter: '*', recursive: false, entryKind: 'directory' };
|
|
1692
|
-
}
|
|
1693
|
-
if (new RegExp(`${symbol}/[A-Za-z_][A-Za-z0-9_]*[^;]*\\.(?:open|read_text|read_bytes)\\(`).test(canonical)
|
|
1694
|
-
|| new RegExp(`${symbol}\\.joinpath\\([A-Za-z_][A-Za-z0-9_]*\\)\\.(?:open|read_text|read_bytes)\\(`).test(canonical)
|
|
1695
|
-
|| new RegExp(`open\\([^)]*${symbol}/[A-Za-z_][A-Za-z0-9_]*`).test(canonical)
|
|
1696
|
-
|| new RegExp(`open\\(os\\.path\\.join\\(${symbol},[A-Za-z_][A-Za-z0-9_]*\\)`).test(canonical)
|
|
1697
|
-
|| new RegExp(`(?:LoadFromFile|ReadAllText|ReadAllBytes|TFileStream\\.Create)\\([^)]*${symbol}[^)]*[A-Za-z_][A-Za-z0-9_]*`, 'i').test(canonical))
|
|
1698
|
-
return { kind: 'load' };
|
|
1699
|
-
if (new RegExp(`${symbol}(?:/[^;]+|\\.joinpath\\([^)]*\\))\\.(?:mkdir|touch|write_bytes|write_text)\\(`).test(canonical)
|
|
1700
|
-
|| new RegExp(`(?:copy|copy2|copyfile|copytree)\\([^)]*,[^)]*${symbol}`).test(canonical)
|
|
1701
|
-
|| new RegExp(`(?:SaveToFile|WriteAllText|WriteAllBytes|DeleteFile|fmCreate)[^;]*${symbol}`, 'i').test(canonical))
|
|
1702
|
-
return { kind: 'write' };
|
|
1703
|
-
return null;
|
|
1704
|
-
}
|
|
1705
|
-
function joinedPathBindings(statements, rootBinding) {
|
|
1706
|
-
const result = new Set();
|
|
1707
|
-
let changed = true;
|
|
1708
|
-
for (let pass = 0; changed && pass < statements.length + 1; pass += 1) {
|
|
1709
|
-
changed = false;
|
|
1710
|
-
const sources = [rootBinding, ...result];
|
|
1711
|
-
for (const statement of statements) {
|
|
1712
|
-
const assignment = statement.text.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*(?::?=)\s*(.+)$/);
|
|
1713
|
-
if (!assignment || result.has(assignment[1]) || assignment[1] === rootBinding)
|
|
1714
|
-
continue;
|
|
1715
|
-
const source = sources.find((binding) => new RegExp(`\\b${escaped(binding)}\\b`, 'i').test(assignment[2]));
|
|
1716
|
-
if (!source)
|
|
1717
|
-
continue;
|
|
1718
|
-
const pathJoin = /(?:os\.path\.join|joinpath|TPath\.Combine|DirectorySeparator|PathDelim)/i.test(assignment[2])
|
|
1719
|
-
|| new RegExp(`\\b${escaped(source)}\\b\\s*[/+]`, 'i').test(assignment[2]);
|
|
1720
|
-
if (!pathJoin)
|
|
1721
|
-
continue;
|
|
1722
|
-
const remainingIdentifiers = (assignment[2].match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? [])
|
|
1723
|
-
.filter((identifier) => identifier.toLowerCase() !== source.toLowerCase())
|
|
1724
|
-
.filter((identifier) => ![
|
|
1725
|
-
'directoryseparator', 'join', 'joinpath', 'os', 'path', 'pathdelim',
|
|
1726
|
-
'tpath',
|
|
1727
|
-
].includes(identifier.toLowerCase()));
|
|
1728
|
-
if (remainingIdentifiers.length === 0)
|
|
1729
|
-
continue;
|
|
1730
|
-
result.add(assignment[1]);
|
|
1731
|
-
changed = true;
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
return [...result];
|
|
1735
|
-
}
|
|
1736
|
-
function globMatches(value, filter) {
|
|
1737
|
-
const pattern = filter.replace(/[.+^${}()|[\]\\]/g, '\\$&').replaceAll('?', '.').replaceAll('*', '.*');
|
|
1738
|
-
return new RegExp(`^${pattern}$`).test(value);
|
|
1739
|
-
}
|
|
1740
|
-
function emitResourceProviders(paths, statementsByPath, rootsByPath, importsByPath, aliases, records) {
|
|
1741
|
-
const emitted = new Set();
|
|
1742
|
-
for (const consumerPath of [...statementsByPath.keys()].filter((path) => !isTestPath(path) && !isDependencyPath(path))) {
|
|
1743
|
-
const statements = statementsByPath.get(consumerPath) ?? [];
|
|
1744
|
-
const bindings = new Map(rootsByPath.get(consumerPath));
|
|
1745
|
-
for (const [binding, target] of importsByPath.get(consumerPath) ?? []) {
|
|
1746
|
-
const importedRoots = rootsByPath.get(target.path);
|
|
1747
|
-
if (!importedRoots)
|
|
1748
|
-
continue;
|
|
1749
|
-
if (target.symbol === '*') {
|
|
1750
|
-
for (const [symbol, root] of importedRoots) {
|
|
1751
|
-
bindings.set(binding === '*' ? symbol : `${binding}.${symbol}`, root);
|
|
1752
|
-
}
|
|
1753
|
-
}
|
|
1754
|
-
else {
|
|
1755
|
-
const root = importedRoots.get(target.symbol);
|
|
1756
|
-
if (root)
|
|
1757
|
-
bindings.set(binding, root);
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
for (const [binding, root] of bindings) {
|
|
1761
|
-
const joinedBindings = joinedPathBindings(statements, binding);
|
|
1762
|
-
const uses = statements.flatMap((statement) => [
|
|
1763
|
-
{ statement, use: resourceUse(statement, binding) },
|
|
1764
|
-
...joinedBindings.map((joined) => ({
|
|
1765
|
-
statement,
|
|
1766
|
-
use: resourceUse(statement, joined, true),
|
|
1767
|
-
})),
|
|
1768
|
-
]);
|
|
1769
|
-
const enumeration = uses.find((candidate) => candidate.use?.kind === 'enumerate' || candidate.use?.kind === 'materialize');
|
|
1770
|
-
const load = uses.find((candidate) => candidate.use?.kind === 'load' || candidate.use?.kind === 'materialize');
|
|
1771
|
-
if (!enumeration || !load || uses.some((candidate) => candidate.use?.kind === 'write'))
|
|
1772
|
-
continue;
|
|
1773
|
-
const filter = enumeration.use?.filter ?? '*';
|
|
1774
|
-
let entryKind = enumeration.use?.entryKind ?? 'either';
|
|
1775
|
-
if (entryKind === 'either' && joinedBindings.length > 0) {
|
|
1776
|
-
const resourceBindings = joinedBindings.map(escaped).join('|');
|
|
1777
|
-
const hasDirectoryCheck = statements.some((statement) => {
|
|
1778
|
-
const canonical = canonicalExpression(statement.text);
|
|
1779
|
-
return new RegExp(`(?:isdir|DirectoryExists)\\([^)]*(?:${resourceBindings})`, 'i').test(canonical);
|
|
1780
|
-
});
|
|
1781
|
-
const hasFileCheck = statements.some((statement) => {
|
|
1782
|
-
const canonical = canonicalExpression(statement.text);
|
|
1783
|
-
return new RegExp(`(?:isfile|FileExists)\\([^)]*(?:${resourceBindings})`, 'i').test(canonical);
|
|
1784
|
-
});
|
|
1785
|
-
if (hasDirectoryCheck !== hasFileCheck)
|
|
1786
|
-
entryKind = hasDirectoryCheck ? 'directory' : 'file';
|
|
1787
|
-
}
|
|
1788
|
-
const canonicalRoot = canonicalCopiedPath(root.rootPath, aliases);
|
|
1789
|
-
const rootIdentity = `${root.path}#${root.symbol}:${canonicalRoot}:filter:${filter}:entries:${entryKind}`;
|
|
1790
|
-
if (emitted.has(rootIdentity))
|
|
1791
|
-
continue;
|
|
1792
|
-
const children = new Set();
|
|
1793
|
-
for (const tracked of paths) {
|
|
1794
|
-
if (!tracked.startsWith(`${root.rootPath}/`))
|
|
1795
|
-
continue;
|
|
1796
|
-
const relative = tracked.slice(root.rootPath.length + 1);
|
|
1797
|
-
if (!relative)
|
|
1798
|
-
continue;
|
|
1799
|
-
const recursive = enumeration.use?.recursive ?? false;
|
|
1800
|
-
if (recursive && entryKind !== 'directory') {
|
|
1801
|
-
if (globMatches(basename(relative), filter))
|
|
1802
|
-
children.add(canonicalCopiedPath(tracked, aliases));
|
|
1803
|
-
continue;
|
|
1804
|
-
}
|
|
1805
|
-
const first = relative.split('/')[0];
|
|
1806
|
-
if (relative.includes('/')) {
|
|
1807
|
-
if (entryKind !== 'file' && filter === '*') {
|
|
1808
|
-
children.add(canonicalCopiedPath(`${root.rootPath}/${first}`, aliases));
|
|
1809
|
-
}
|
|
1810
|
-
}
|
|
1811
|
-
else if (entryKind !== 'directory' && globMatches(first, filter)) {
|
|
1812
|
-
children.add(canonicalCopiedPath(tracked, aliases));
|
|
1813
|
-
}
|
|
1814
|
-
}
|
|
1815
|
-
if (children.size === 0)
|
|
1816
|
-
continue;
|
|
1817
|
-
const familyKind = 'resource-provider-selector-family';
|
|
1818
|
-
const owner = `${root.path}#${root.owner}.${root.symbol}`;
|
|
1819
|
-
const identity = `resource-provider:${rootIdentity}`;
|
|
1820
|
-
const familyId = recordStableId(familyKind, enumeration.statement.path, owner, identity);
|
|
1821
|
-
records.push({
|
|
1822
|
-
kind: familyKind,
|
|
1823
|
-
projections: ['registry_families'],
|
|
1824
|
-
presumptiveInventory: true,
|
|
1825
|
-
proofStatus: 'proof-complete',
|
|
1826
|
-
path: enumeration.statement.path,
|
|
1827
|
-
line: enumeration.statement.startLine,
|
|
1828
|
-
anchor: enumeration.statement.anchor,
|
|
1829
|
-
owner,
|
|
1830
|
-
identity,
|
|
1831
|
-
tokens: [canonicalRoot, `filter:${filter}`, canonicalExpression(load.statement.text),
|
|
1832
|
-
'proof-complete:resource-provider-registry'],
|
|
1833
|
-
semanticIdentityKey: `resource-registry-family\0${rootIdentity}`,
|
|
1834
|
-
identityComponents: {
|
|
1835
|
-
identity_kind: 'resource-registry-family',
|
|
1836
|
-
owner_identity: owner,
|
|
1837
|
-
selector_domain: canonicalRoot,
|
|
1838
|
-
selector_filter: filter,
|
|
1839
|
-
selector_entry_kind: entryKind,
|
|
1840
|
-
},
|
|
1841
|
-
});
|
|
1842
|
-
[...children].sort().forEach((child) => {
|
|
1843
|
-
const implementation = `repository-resource:${child}`;
|
|
1844
|
-
const entryIdentity = `${identity}:selector:${child}:implementation:${implementation}`;
|
|
1845
|
-
records.push({
|
|
1846
|
-
kind: 'resource-provider-selector-entry',
|
|
1847
|
-
projections: ['registry_entries'],
|
|
1848
|
-
presumptiveInventory: true,
|
|
1849
|
-
proofStatus: 'proof-complete',
|
|
1850
|
-
path: load.statement.path,
|
|
1851
|
-
line: load.statement.startLine,
|
|
1852
|
-
anchor: load.statement.anchor,
|
|
1853
|
-
owner,
|
|
1854
|
-
identity: entryIdentity,
|
|
1855
|
-
tokens: [child, canonicalRoot, 'proof-complete:resource-provider-registry'],
|
|
1856
|
-
selectorKey: child,
|
|
1857
|
-
implementationKey: implementation,
|
|
1858
|
-
selectorFamilyRecordId: familyId,
|
|
1859
|
-
semanticIdentityKey: `resource-registry-entry\0${rootIdentity}\0${child}\0${implementation}`,
|
|
1860
|
-
identityComponents: {
|
|
1861
|
-
identity_kind: 'resource-registry-entry',
|
|
1862
|
-
owner_identity: owner,
|
|
1863
|
-
selector_domain: canonicalRoot,
|
|
1864
|
-
selector_filter: filter,
|
|
1865
|
-
selector_entry_kind: entryKind,
|
|
1866
|
-
selector_key: child,
|
|
1867
|
-
implementation_identity: implementation,
|
|
1868
|
-
},
|
|
1869
|
-
});
|
|
1870
|
-
});
|
|
1871
|
-
emitted.add(rootIdentity);
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
|
-
}
|
|
1875
|
-
export function scanPythonManifestSelectorIdentities(repositoryRoot, trackedPaths) {
|
|
1876
|
-
const paths = [...new Set(trackedPaths.map(normalizePath))].sort();
|
|
1877
|
-
const knownPaths = new Set(paths);
|
|
1878
|
-
const sources = new Map();
|
|
1879
|
-
for (const path of paths) {
|
|
1880
|
-
const source = readText(repositoryRoot, path);
|
|
1881
|
-
if (source !== null)
|
|
1882
|
-
sources.set(path, source);
|
|
1883
|
-
}
|
|
1884
|
-
const structuredDependencyPrefixes = structuredDependencyPackagePrefixes(paths, sources);
|
|
1885
|
-
const aliases = parseCopyAliases(repositoryRoot, paths, sources);
|
|
1886
|
-
const pythonPaths = paths.filter((path) => PYTHON_EXTENSIONS.has(extname(path).toLowerCase())
|
|
1887
|
-
&& !isTestPath(path)
|
|
1888
|
-
&& canonicalCopiedPath(path, aliases) === path);
|
|
1889
|
-
const statementsByPath = new Map();
|
|
1890
|
-
const collectionsByPath = new Map();
|
|
1891
|
-
const exportsByPath = new Map();
|
|
1892
|
-
const modulePathCandidates = new Map();
|
|
1893
|
-
for (const path of pythonPaths) {
|
|
1894
|
-
moduleNames(path).forEach((moduleName) => {
|
|
1895
|
-
const candidates = modulePathCandidates.get(moduleName) ?? new Set();
|
|
1896
|
-
candidates.add(path);
|
|
1897
|
-
modulePathCandidates.set(moduleName, candidates);
|
|
1898
|
-
});
|
|
1899
|
-
}
|
|
1900
|
-
const modulePaths = new Map();
|
|
1901
|
-
modulePathCandidates.forEach((candidates, moduleName) => {
|
|
1902
|
-
if (candidates.size === 1)
|
|
1903
|
-
modulePaths.set(moduleName, [...candidates][0]);
|
|
1904
|
-
});
|
|
1905
|
-
for (const path of pythonPaths) {
|
|
1906
|
-
const source = sources.get(path);
|
|
1907
|
-
if (!source)
|
|
1908
|
-
continue;
|
|
1909
|
-
const statements = pythonStatements(path, source);
|
|
1910
|
-
statementsByPath.set(path, statements);
|
|
1911
|
-
collectionsByPath.set(path, parseStaticCollections(statements));
|
|
1912
|
-
exportsByPath.set(path, pythonExportedSymbols(statements));
|
|
1913
|
-
}
|
|
1914
|
-
const importsByPath = new Map();
|
|
1915
|
-
for (const path of pythonPaths) {
|
|
1916
|
-
importsByPath.set(path, importBindings(path, statementsByPath.get(path) ?? [], modulePaths, knownPaths));
|
|
1917
|
-
}
|
|
1918
|
-
const records = [];
|
|
1919
|
-
for (const path of pythonPaths.filter((candidate) => !isStructuredDependencyPath(candidate, structuredDependencyPrefixes))) {
|
|
1920
|
-
emitEqualityChains(statementsByPath.get(path) ?? [], records);
|
|
1921
|
-
}
|
|
1922
|
-
const emittedCollections = new Set();
|
|
1923
|
-
for (const path of pythonPaths.filter((candidate) => !isStructuredDependencyPath(candidate, structuredDependencyPrefixes))) {
|
|
1924
|
-
const statements = statementsByPath.get(path) ?? [];
|
|
1925
|
-
const bindings = collectionBindingsForFile(path, statements, collectionsByPath.get(path) ?? new Map(), collectionsByPath, importsByPath.get(path) ?? new Map(), exportsByPath);
|
|
1926
|
-
for (const { binding, collection } of bindings) {
|
|
1927
|
-
if (isStructuredDependencyPath(collection.path, structuredDependencyPrefixes))
|
|
1928
|
-
continue;
|
|
1929
|
-
const applicableStatements = statements.filter((statement) => bindingAppliesToStatement(binding, collection, statement));
|
|
1930
|
-
const identity = `${collection.path}#${collection.symbol}`;
|
|
1931
|
-
if (emittedCollections.has(identity))
|
|
1932
|
-
continue;
|
|
1933
|
-
if (collection.collectionKind === 'mapping') {
|
|
1934
|
-
if (new Set(collection.entries.map((entry) => entry.implementation)).size < 2
|
|
1935
|
-
|| mappingHasMutation(applicableStatements, binding, collection))
|
|
1936
|
-
continue;
|
|
1937
|
-
const gates = applicableStatements.flatMap((candidate) => {
|
|
1938
|
-
if (candidate.path === collection.path && candidate.startLine === collection.line)
|
|
1939
|
-
return [];
|
|
1940
|
-
const gateKind = mappingGate(candidate, binding);
|
|
1941
|
-
return gateKind ? [{ statement: candidate, gateKind }] : [];
|
|
1942
|
-
});
|
|
1943
|
-
let iterationGate = null;
|
|
1944
|
-
for (let index = 0; index < applicableStatements.length && !iterationGate; index += 1) {
|
|
1945
|
-
iterationGate = mappingIterationGate(applicableStatements, index, binding);
|
|
1946
|
-
}
|
|
1947
|
-
iterationGate ??= interproceduralIterationGate(applicableStatements, binding, collection.collectionKind)?.gate ?? null;
|
|
1948
|
-
const resultGate = mappingResultGate(applicableStatements, binding);
|
|
1949
|
-
const selected = gates.sort((left, right) => {
|
|
1950
|
-
const rank = { behavior: 0, registration: 1, choice: 2 };
|
|
1951
|
-
return rank[left.gateKind] - rank[right.gateKind]
|
|
1952
|
-
|| left.statement.startLine - right.statement.startLine;
|
|
1953
|
-
})[0];
|
|
1954
|
-
if (!selected && !iterationGate && !resultGate)
|
|
1955
|
-
continue;
|
|
1956
|
-
const gate = iterationGate ?? resultGate ?? {
|
|
1957
|
-
path: selected.statement.path,
|
|
1958
|
-
line: selected.statement.startLine,
|
|
1959
|
-
anchor: selected.statement.anchor,
|
|
1960
|
-
owner: selected.statement.owner,
|
|
1961
|
-
statement: selected.statement.text,
|
|
1962
|
-
};
|
|
1963
|
-
emitStaticCollection(collection, 'mapping', gate, null, importsByPath.get(collection.path) ?? new Map(), records);
|
|
1964
|
-
}
|
|
1965
|
-
else {
|
|
1966
|
-
let registration = null;
|
|
1967
|
-
for (let index = 0; index < applicableStatements.length && !registration; index += 1) {
|
|
1968
|
-
registration = registrationListGate(applicableStatements, index, binding);
|
|
1969
|
-
}
|
|
1970
|
-
registration ??= interproceduralIterationGate(applicableStatements, binding, collection.collectionKind);
|
|
1971
|
-
if (registration) {
|
|
1972
|
-
emitStaticCollection(collection, 'registration', registration.gate, registration.variables, importsByPath.get(collection.path) ?? new Map(), records);
|
|
1973
|
-
}
|
|
1974
|
-
else {
|
|
1975
|
-
const statement = applicableStatements.find((candidate) => choiceListGate(candidate, binding));
|
|
1976
|
-
const generated = generatedChoiceGate(applicableStatements, binding);
|
|
1977
|
-
if (!statement && !generated)
|
|
1978
|
-
continue;
|
|
1979
|
-
const choiceGate = generated ?? {
|
|
1980
|
-
path: statement.path,
|
|
1981
|
-
line: statement.startLine,
|
|
1982
|
-
anchor: statement.anchor,
|
|
1983
|
-
owner: statement.owner,
|
|
1984
|
-
statement: statement.text,
|
|
1985
|
-
};
|
|
1986
|
-
emitStaticCollection(collection, 'choice', choiceGate, null, importsByPath.get(collection.path) ?? new Map(), records);
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
1989
|
-
emittedCollections.add(identity);
|
|
1990
|
-
}
|
|
1991
|
-
}
|
|
1992
|
-
scanInstallManifests(repositoryRoot, paths, sources, aliases, records);
|
|
1993
|
-
const pascalPaths = paths.filter((path) => ['.inc', '.pas', '.pp'].includes(extname(path).toLowerCase())
|
|
1994
|
-
&& !isTestPath(path)
|
|
1995
|
-
&& canonicalCopiedPath(path, aliases) === path);
|
|
1996
|
-
for (const path of pascalPaths) {
|
|
1997
|
-
const source = sources.get(path);
|
|
1998
|
-
if (source)
|
|
1999
|
-
statementsByPath.set(path, pascalStatements(path, source));
|
|
2000
|
-
}
|
|
2001
|
-
const rootsByPath = new Map();
|
|
2002
|
-
for (const path of pythonPaths) {
|
|
2003
|
-
rootsByPath.set(path, pythonMaterializationRoots(path, statementsByPath.get(path) ?? [], paths, modulePaths, aliases));
|
|
2004
|
-
}
|
|
2005
|
-
for (const path of pascalPaths) {
|
|
2006
|
-
rootsByPath.set(path, pascalMaterializationRoots(path, statementsByPath.get(path) ?? [], paths, aliases));
|
|
2007
|
-
}
|
|
2008
|
-
emitResourceProviders(paths, statementsByPath, rootsByPath, importsByPath, aliases, records);
|
|
2009
|
-
const bySemanticKey = new Map();
|
|
2010
|
-
records.forEach((record) => bySemanticKey.set(record.semanticIdentityKey, record));
|
|
2011
|
-
return [...bySemanticKey.values()].sort((left, right) => left.path.localeCompare(right.path)
|
|
2012
|
-
|| left.line - right.line
|
|
2013
|
-
|| left.kind.localeCompare(right.kind)
|
|
2014
|
-
|| left.semanticIdentityKey.localeCompare(right.semanticIdentityKey));
|
|
2015
|
-
}
|