ucn 5.5.2 → 5.6.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/.claude/skills/ucn/SKILL.md +35 -12
- package/.claude/skills/ucn/references/commands.md +21 -17
- package/.claude/skills/ucn/references/resolution-rules.md +4 -0
- package/.claude/skills/ucn/references/trust-contract.md +4 -4
- package/CHANGELOG.md +474 -0
- package/README.md +71 -29
- package/cli/flags.js +184 -0
- package/cli/index.js +138 -324
- package/core/account.js +44 -87
- package/core/analysis.js +273 -107
- package/core/ast-analysis.js +2 -0
- package/core/bridge.js +29 -65
- package/core/cache.js +101 -16
- package/core/callers.js +352 -70
- package/core/capture.js +331 -0
- package/core/change-kinds.js +888 -0
- package/core/check.js +1117 -139
- package/core/client-receivers.js +189 -0
- package/core/command-contracts.js +7 -6
- package/core/constructed-class.js +1195 -0
- package/core/contract-membership.js +142 -0
- package/core/deadcode.js +388 -85
- package/core/deletion.js +401 -0
- package/core/discovery.js +104 -9
- package/core/entrypoints.js +12 -6
- package/core/evidence-bands.js +68 -0
- package/core/evidence-bands.json +44 -0
- package/core/execute.js +172 -161
- package/core/hot-summary.js +241 -0
- package/core/index-ir.js +3 -1
- package/core/inprocess-clients.js +41 -15
- package/core/ir.js +5 -1
- package/core/member-signatures.js +789 -0
- package/core/output/analysis-ext.js +20 -10
- package/core/output/analysis.js +98 -60
- package/core/output/check.js +98 -37
- package/core/output/doctor.js +34 -1
- package/core/output/find.js +2 -1
- package/core/output/lines.js +63 -3
- package/core/output/public.js +30 -11
- package/core/output/refactoring.js +32 -0
- package/core/output/reporting.js +21 -2
- package/core/output/search.js +11 -2
- package/core/output/tracing.js +35 -7
- package/core/output-budget.js +124 -57
- package/core/parallel-build.js +3 -3
- package/core/project.js +106 -23
- package/core/provenance-type.js +12 -5
- package/core/public-command.js +35 -9
- package/core/python-modules.js +6 -0
- package/core/reflection.js +166 -23
- package/core/registry.js +623 -72
- package/core/reporting.js +69 -34
- package/core/request-helpers.js +63 -7
- package/core/review-items.js +75 -0
- package/core/search.js +89 -128
- package/core/shared.js +0 -45
- package/core/snapshot.js +350 -0
- package/core/source-universe.js +707 -0
- package/core/test-units.js +3624 -0
- package/core/tracing.js +150 -96
- package/core/verify.js +27 -11
- package/languages/adapter.js +8 -3
- package/languages/c-family.js +12 -0
- package/languages/csharp.js +634 -0
- package/languages/go.js +120 -0
- package/languages/index.js +422 -4
- package/languages/java.js +712 -0
- package/languages/javascript.js +1460 -2
- package/languages/lexical-scope.js +68 -0
- package/languages/python.js +1672 -1
- package/languages/rust.js +91 -0
- package/mcp/server.js +44 -110
- package/package.json +11 -6
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* core/hot-summary.js - The HOT ranking of `repo`, kept per index state.
|
|
5
|
+
*
|
|
6
|
+
* Ranking the most-called functions runs an exact, pinned caller query on
|
|
7
|
+
* every definition able to enter the list (core/reporting.js getStats). On a
|
|
8
|
+
* 3,400-file project that is about 1,300 queries and ten seconds, and the
|
|
9
|
+
* answer is the same until a file changes: it is a function of the index and
|
|
10
|
+
* of the options that shape the ranking, nothing else. So it is kept:
|
|
11
|
+
*
|
|
12
|
+
* - in memory, on the index, until the index is rebuilt;
|
|
13
|
+
* - on disk beside the index (hot-summary.json), tagged with the index
|
|
14
|
+
* generation it was computed on. A loaded index reads it back only when
|
|
15
|
+
* its own generation is the tagged one. Every save of the index starts a
|
|
16
|
+
* new generation and rewrites the file under it; a rebuild drops the
|
|
17
|
+
* summaries first, so a changed file always ranks afresh.
|
|
18
|
+
*
|
|
19
|
+
* A summary answers only the request it was computed for: the key is every
|
|
20
|
+
* option that reaches the ranking (list length, production-only counting,
|
|
21
|
+
* both budgets, scope). No summary is derived from another, so a kept answer
|
|
22
|
+
* is the answer a fresh ranking gives, byte for byte. Fresh and kept answers
|
|
23
|
+
* leave this module through the same serialization.
|
|
24
|
+
*
|
|
25
|
+
* Two more conditions keep that true. A ranking is kept only when every
|
|
26
|
+
* indexed file still is what the index recorded once it has been computed:
|
|
27
|
+
* call records are refreshed from a file that changes under a running
|
|
28
|
+
* command, and such a ranking describes neither the index nor the edit. And
|
|
29
|
+
* a stored ranking is read only by the engine build that computed it: unlike
|
|
30
|
+
* a parsed fact it is a verdict of the caller engine, so an edited engine
|
|
31
|
+
* file in a development tree ends it.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const crypto = require('crypto');
|
|
35
|
+
const fs = require('fs');
|
|
36
|
+
const path = require('path');
|
|
37
|
+
const { version: UCN_VERSION } = require('../package.json');
|
|
38
|
+
|
|
39
|
+
const SUMMARY_FILE = 'hot-summary.json';
|
|
40
|
+
const SUMMARY_FORMAT = 1;
|
|
41
|
+
// Requests differ by scope and list length; a session asks a handful.
|
|
42
|
+
const MAX_SUMMARIES = 8;
|
|
43
|
+
// Listed functions kept on disk across all summaries. A list longer than
|
|
44
|
+
// this (`--hot --top=10000`) is ranked again instead of being stored.
|
|
45
|
+
const MAX_STORED_ITEMS = 2000;
|
|
46
|
+
|
|
47
|
+
/** The request a summary answers: every option the ranking reads. */
|
|
48
|
+
function keyOf({ top, productionCallsOnly, maxRefine, workBudget, file, in: inScope, exclude }) {
|
|
49
|
+
return JSON.stringify([
|
|
50
|
+
top,
|
|
51
|
+
!!productionCallsOnly,
|
|
52
|
+
Number.isFinite(maxRefine) ? maxRefine : null,
|
|
53
|
+
Number.isFinite(workBudget) ? workBudget : null,
|
|
54
|
+
file || null,
|
|
55
|
+
inScope || null,
|
|
56
|
+
Array.isArray(exclude) && exclude.length > 0 ? exclude : null,
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let engineStampMemo = null;
|
|
61
|
+
/**
|
|
62
|
+
* The engine build: the package version and the size and modification time
|
|
63
|
+
* of every engine source file. Constant for an installed package.
|
|
64
|
+
*/
|
|
65
|
+
function engineStamp() {
|
|
66
|
+
if (engineStampMemo) return engineStampMemo;
|
|
67
|
+
const hash = crypto.createHash('md5').update(UCN_VERSION);
|
|
68
|
+
const root = path.join(__dirname, '..');
|
|
69
|
+
for (const folder of ['core', 'core/output', 'languages']) {
|
|
70
|
+
let names = [];
|
|
71
|
+
try {
|
|
72
|
+
names = fs.readdirSync(path.join(root, folder)).filter(name => name.endsWith('.js')).sort();
|
|
73
|
+
} catch (_) { /* a folder this build does not have */ }
|
|
74
|
+
for (const name of names) {
|
|
75
|
+
let state = '?';
|
|
76
|
+
try {
|
|
77
|
+
const stat = fs.statSync(path.join(root, folder, name));
|
|
78
|
+
state = `${stat.size}:${stat.mtimeMs}`;
|
|
79
|
+
} catch (_) { /* unreadable: its own state */ }
|
|
80
|
+
hash.update(`\0${folder}/${name}:${state}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return (engineStampMemo = hash.digest('hex'));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Whether every indexed file still is what the index recorded: same size
|
|
88
|
+
* and modification time, or the same content when only the time moved (the
|
|
89
|
+
* rule the staleness check applies).
|
|
90
|
+
*/
|
|
91
|
+
function sourceMatchesIndex(index) {
|
|
92
|
+
for (const [filePath, entry] of index.files) {
|
|
93
|
+
try {
|
|
94
|
+
const stat = fs.statSync(filePath);
|
|
95
|
+
if (entry.size !== undefined && stat.size !== entry.size) return false;
|
|
96
|
+
if (entry.mtime && stat.mtimeMs === entry.mtime) continue;
|
|
97
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
98
|
+
if (crypto.createHash('md5').update(content).digest('hex') !== entry.hash) return false;
|
|
99
|
+
} catch (_) { return false; }
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function cacheDirOf(index, cachePath) {
|
|
105
|
+
if (cachePath) return path.dirname(cachePath);
|
|
106
|
+
if (index._cacheDir) return index._cacheDir;
|
|
107
|
+
return require('./cache').getProjectCacheDir(index.root);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** A summary as every reader receives it: plain data, its own copy. */
|
|
111
|
+
function copyOf(summary) {
|
|
112
|
+
return JSON.parse(JSON.stringify(summary));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function validSummary(summary) {
|
|
116
|
+
return !!summary && typeof summary === 'object' && Number.isInteger(summary.top) &&
|
|
117
|
+
Array.isArray(summary.items) && summary.items.every(item =>
|
|
118
|
+
item && typeof item === 'object' && typeof item.name === 'string' &&
|
|
119
|
+
Number.isInteger(item.callCount));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Read the stored summaries of the generation in memory, once per
|
|
124
|
+
* generation. An index that was rebuilt, or never saved, has none.
|
|
125
|
+
*/
|
|
126
|
+
function ensure(index) {
|
|
127
|
+
const generation = index._callsGenerationKey || null;
|
|
128
|
+
if (index._hotSummariesGeneration === generation && index._hotSummaries) return;
|
|
129
|
+
// Summaries computed since the last save stay: the state they describe
|
|
130
|
+
// is the one in memory.
|
|
131
|
+
const kept = index._hotSummaries || new Map();
|
|
132
|
+
index._hotSummaries = kept;
|
|
133
|
+
index._hotSummariesGeneration = generation;
|
|
134
|
+
if (!generation || index.isProvenanceFactIndex) return;
|
|
135
|
+
let stored;
|
|
136
|
+
try {
|
|
137
|
+
stored = JSON.parse(fs.readFileSync(path.join(cacheDirOf(index), SUMMARY_FILE), 'utf8'));
|
|
138
|
+
} catch (_) { return; }
|
|
139
|
+
if (!stored || stored.format !== SUMMARY_FORMAT || stored.engine !== engineStamp() ||
|
|
140
|
+
stored.generation !== generation || !Array.isArray(stored.summaries)) return;
|
|
141
|
+
for (const row of stored.summaries) {
|
|
142
|
+
if (!Array.isArray(row) || row.length !== 2 || typeof row[0] !== 'string' ||
|
|
143
|
+
!validSummary(row[1])) return;
|
|
144
|
+
}
|
|
145
|
+
for (const [key, summary] of stored.summaries) {
|
|
146
|
+
if (!kept.has(key)) kept.set(key, summary);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The kept answer to this request, or undefined. */
|
|
151
|
+
function get(index, key) {
|
|
152
|
+
ensure(index);
|
|
153
|
+
const summary = index._hotSummaries.get(key);
|
|
154
|
+
if (summary === undefined) return undefined;
|
|
155
|
+
// A caller may edit source between the surface's freshness check and
|
|
156
|
+
// this query, or use a live index directly. Native caller queries refresh
|
|
157
|
+
// those records; a kept verdict must not bypass that existing path.
|
|
158
|
+
if (!sourceMatchesIndex(index)) {
|
|
159
|
+
reset(index);
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
// Most recently asked last: the bound drops the oldest.
|
|
163
|
+
index._hotSummaries.delete(key);
|
|
164
|
+
index._hotSummaries.set(key, summary);
|
|
165
|
+
return copyOf(summary);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Keep a freshly ranked answer; returns the copy the caller hands out. */
|
|
169
|
+
function put(index, key, summary) {
|
|
170
|
+
const stored = copyOf(summary);
|
|
171
|
+
// Ranked while a file was being edited: answered, not kept.
|
|
172
|
+
if (!sourceMatchesIndex(index)) return stored;
|
|
173
|
+
ensure(index);
|
|
174
|
+
index._hotSummaries.delete(key);
|
|
175
|
+
index._hotSummaries.set(key, stored);
|
|
176
|
+
while (index._hotSummaries.size > MAX_SUMMARIES) {
|
|
177
|
+
index._hotSummaries.delete(index._hotSummaries.keys().next().value);
|
|
178
|
+
}
|
|
179
|
+
index.hotSummaryDirty = true;
|
|
180
|
+
return copyOf(stored);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Drop every summary: the index is being changed. */
|
|
184
|
+
function reset(index) {
|
|
185
|
+
index._hotSummaries = new Map();
|
|
186
|
+
// The generation in memory no longer describes the index, so its stored
|
|
187
|
+
// summaries are not read back either.
|
|
188
|
+
index._hotSummariesGeneration = index._callsGenerationKey || null;
|
|
189
|
+
// A direct indexFile/removeFileSymbols mutation leaves the calls-shard
|
|
190
|
+
// generation in place. It no longer identifies the state ranked here.
|
|
191
|
+
index._hotSummarySavedGeneration = null;
|
|
192
|
+
index.hotSummaryDirty = false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Another saved state was loaded: its stored summaries are read on demand. */
|
|
196
|
+
function forget(index) {
|
|
197
|
+
index._hotSummaries = null;
|
|
198
|
+
index._hotSummariesGeneration = undefined;
|
|
199
|
+
index._hotSummarySavedGeneration = index._callsGenerationKey || null;
|
|
200
|
+
index.hotSummaryDirty = false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** A completed index save establishes which state a stored answer may name. */
|
|
204
|
+
function adoptSavedGeneration(index) {
|
|
205
|
+
const generation = index._callsGenerationKey || null;
|
|
206
|
+
index._hotSummarySavedGeneration = generation;
|
|
207
|
+
index._hotSummariesGeneration = generation;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Write the summaries under the generation in memory. An index with no
|
|
212
|
+
* generation has no saved state they could be read back against.
|
|
213
|
+
*/
|
|
214
|
+
function save(index, cachePath) {
|
|
215
|
+
const generation = index._callsGenerationKey || null;
|
|
216
|
+
if (!generation || index._hotSummarySavedGeneration !== generation || index.isProvenanceFactIndex) return null;
|
|
217
|
+
// The file is replaced, so what it holds for this generation comes first.
|
|
218
|
+
ensure(index);
|
|
219
|
+
const target = path.join(cacheDirOf(index, cachePath), SUMMARY_FILE);
|
|
220
|
+
const summaries = [];
|
|
221
|
+
let items = 0;
|
|
222
|
+
// Newest first, within the stored-item bound.
|
|
223
|
+
for (const [key, summary] of [...index._hotSummaries].reverse()) {
|
|
224
|
+
if (items + summary.items.length > MAX_STORED_ITEMS) continue;
|
|
225
|
+
items += summary.items.length;
|
|
226
|
+
summaries.unshift([key, summary]);
|
|
227
|
+
}
|
|
228
|
+
index.hotSummaryDirty = false;
|
|
229
|
+
if (summaries.length === 0) {
|
|
230
|
+
try { fs.rmSync(target, { force: true }); } catch (_) { /* nothing stored */ }
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
234
|
+
fs.writeFileSync(target + '.tmp', JSON.stringify({
|
|
235
|
+
format: SUMMARY_FORMAT, engine: engineStamp(), generation, summaries,
|
|
236
|
+
}));
|
|
237
|
+
fs.renameSync(target + '.tmp', target);
|
|
238
|
+
return target;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
module.exports = { SUMMARY_FILE, keyOf, engineStamp, ensure, get, put, reset, forget, adoptSavedGeneration, save };
|
package/core/index-ir.js
CHANGED
|
@@ -146,6 +146,8 @@ function createFileEntryFromIR({
|
|
|
146
146
|
...(ir.reflectionSites?.length > 0 && {
|
|
147
147
|
reflectionSites: ir.reflectionSites,
|
|
148
148
|
}),
|
|
149
|
+
...(ir.reflectionUses > 0 && { reflectionUses: ir.reflectionUses }),
|
|
150
|
+
...(ir.evalUses > 0 && { evalUses: ir.evalUses }),
|
|
149
151
|
...(Array.isArray(ir.computedDispatchSites) && {
|
|
150
152
|
computedDispatchSites: ir.computedDispatchSites,
|
|
151
153
|
}),
|
|
@@ -201,7 +203,7 @@ const OPTIONAL_SYMBOL_FIELDS = Object.freeze([
|
|
|
201
203
|
'annotationType', 'valueShape', 'friendOf', 'templateParams', 'delegateParams',
|
|
202
204
|
'typedefName', 'forwardDeclaration', 'anonymousNamespace', 'specialization',
|
|
203
205
|
'memberConst', 'refQualifier', 'fieldPointers', 'memberPaths', 'instanceAttribute',
|
|
204
|
-
'ambientAugmentation',
|
|
206
|
+
'ambientAugmentation', 'requiredMember', 'protocolClass',
|
|
205
207
|
]);
|
|
206
208
|
|
|
207
209
|
function materializeSymbol(fileEntry, item) {
|
|
@@ -195,9 +195,23 @@ class PythonClientResolver {
|
|
|
195
195
|
|
|
196
196
|
/** The fixture named `name` a test in `file` requests: same module first,
|
|
197
197
|
* then conftest.py files from the test's directory up to the root. */
|
|
198
|
-
fixtureDef(file, name) {
|
|
199
|
-
|
|
198
|
+
fixtureDef(file, name, requester = null) {
|
|
199
|
+
let owner = requester?.parent;
|
|
200
|
+
while (owner && owner.type !== 'class_definition') owner = owner.parent;
|
|
201
|
+
const key = `${file}\0${name}\0${owner?.startIndex ?? ''}`;
|
|
200
202
|
if (this.fixtureMemo.has(key)) return this.fixtureMemo.get(key);
|
|
203
|
+
this.fixtureMemo.set(key, null);
|
|
204
|
+
if (owner) {
|
|
205
|
+
const matches = namedChildren(field(owner, 'body')).map(stmt =>
|
|
206
|
+
stmt.type === 'decorated_definition' ? field(stmt, 'definition') : stmt)
|
|
207
|
+
.filter(fn => fn?.type === 'function_definition' && field(fn, 'name')?.text === name);
|
|
208
|
+
if (matches.length) {
|
|
209
|
+
const found = matches.length === 1 && this.isFixture(file, matches[0])
|
|
210
|
+
? { file, fnNode: matches[0] } : null;
|
|
211
|
+
this.fixtureMemo.set(key, found);
|
|
212
|
+
return found;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
201
215
|
let found = this.moduleFixture(file, name);
|
|
202
216
|
if (!found) {
|
|
203
217
|
for (let dir = path.dirname(file); dir.startsWith(this.index.root); dir = path.dirname(dir)) {
|
|
@@ -213,7 +227,8 @@ class PythonClientResolver {
|
|
|
213
227
|
return found;
|
|
214
228
|
}
|
|
215
229
|
|
|
216
|
-
moduleFixture(file, name) {
|
|
230
|
+
moduleFixture(file, name, depth = 0) {
|
|
231
|
+
if (depth > MAX_HOPS) return null;
|
|
217
232
|
const root = this.sess.root(file);
|
|
218
233
|
if (!root) return null;
|
|
219
234
|
const hits = [];
|
|
@@ -222,7 +237,12 @@ class PythonClientResolver {
|
|
|
222
237
|
if (fn?.type === 'function_definition' && field(fn, 'name')?.text === name &&
|
|
223
238
|
this.isFixture(file, fn)) hits.push(fn);
|
|
224
239
|
}
|
|
225
|
-
return hits.length === 1 ? { file, fnNode: hits[0] } : null;
|
|
240
|
+
if (hits.length) return hits.length === 1 ? { file, fnNode: hits[0] } : null;
|
|
241
|
+
const entry = this.sess.entry(file);
|
|
242
|
+
const bindings = (entry?.importBindings || []).filter(b => (b.alias || b.name) === name && !b.deferred);
|
|
243
|
+
if (bindings.length !== 1 || bindings[0].kind !== 'from') return null;
|
|
244
|
+
const relative = entry.moduleResolved?.[bindings[0].module];
|
|
245
|
+
return relative ? this.moduleFixture(path.join(this.index.root, relative), bindings[0].name, depth + 1) : null;
|
|
226
246
|
}
|
|
227
247
|
|
|
228
248
|
/** The expression a fixture provides: its single yielded/returned value,
|
|
@@ -253,11 +273,14 @@ class PythonClientResolver {
|
|
|
253
273
|
if (!expr || expr.type !== 'identifier' || hops > MAX_HOPS) return { file, expr };
|
|
254
274
|
if (fnNode) {
|
|
255
275
|
const values = boundValues(field(fnNode, 'body'), expr.text, this.bindingMemo);
|
|
256
|
-
if (values === null)
|
|
276
|
+
if (values === null) {
|
|
277
|
+
const decl = routeGraph.findDecl(this.sess, file, expr.text, expr);
|
|
278
|
+
return decl?.kind === 'function' ? { file, expr: decl.valueNode } : null;
|
|
279
|
+
}
|
|
257
280
|
if (values.length === 1) return this.followBinding(file, fnNode, unwrap(values[0].node), hops + 1);
|
|
258
281
|
if (values.length > 1) return null;
|
|
259
282
|
if (isParameterOf(fnNode, expr.text)) {
|
|
260
|
-
const fixture = this.fixtureDef(file, expr.text);
|
|
283
|
+
const fixture = this.fixtureDef(file, expr.text, fnNode);
|
|
261
284
|
return fixture ? this.fixtureValue(fixture.file, fixture.fnNode, hops + 1) : null;
|
|
262
285
|
}
|
|
263
286
|
}
|
|
@@ -319,7 +342,7 @@ class PythonClientResolver {
|
|
|
319
342
|
if (expr.type !== 'identifier') return null;
|
|
320
343
|
const fnNode = enclosingFunctionNode(expr);
|
|
321
344
|
if (fnNode && isParameterOf(fnNode, expr.text)) {
|
|
322
|
-
const fixture = this.fixtureDef(file, expr.text);
|
|
345
|
+
const fixture = this.fixtureDef(file, expr.text, fnNode);
|
|
323
346
|
if (!fixture) return null;
|
|
324
347
|
const value = this.fixtureValue(fixture.file, fixture.fnNode, hops + 1);
|
|
325
348
|
if (!value?.expr) return null;
|
|
@@ -345,13 +368,8 @@ class PythonClientResolver {
|
|
|
345
368
|
return null;
|
|
346
369
|
}
|
|
347
370
|
|
|
348
|
-
/**
|
|
349
|
-
|
|
350
|
-
* through an in-process client, { inProcess: true } when the client is
|
|
351
|
-
* built from an app that did not resolve, else null (not a client built
|
|
352
|
-
* from an app).
|
|
353
|
-
*/
|
|
354
|
-
resolve(file, line, receiver, name) {
|
|
371
|
+
/** Values bound to a request receiver, without assuming they are clients. */
|
|
372
|
+
clientValues(file, line, receiver, name) {
|
|
355
373
|
const root = this.sess.root(file);
|
|
356
374
|
if (!root) return null;
|
|
357
375
|
const sites = requestCallNodes(root, line, receiver, name);
|
|
@@ -365,7 +383,7 @@ class PythonClientResolver {
|
|
|
365
383
|
if (values.length > 0) {
|
|
366
384
|
clients = this.bindingInEffect(values, receiverNode).map(node => ({ file, expr: unwrap(node) }));
|
|
367
385
|
} else if (isParameterOf(fnNode, receiver)) {
|
|
368
|
-
const fixture = this.fixtureDef(file, receiver);
|
|
386
|
+
const fixture = this.fixtureDef(file, receiver, fnNode);
|
|
369
387
|
const value = fixture ? this.fixtureValue(fixture.file, fixture.fnNode, 1) : null;
|
|
370
388
|
clients = value ? [value] : [];
|
|
371
389
|
}
|
|
@@ -376,6 +394,14 @@ class PythonClientResolver {
|
|
|
376
394
|
clients = this.bindingInEffect(values, receiverNode).map(node => ({ file, expr: unwrap(node) }));
|
|
377
395
|
}
|
|
378
396
|
}
|
|
397
|
+
const values = clients?.map(c => this.followBinding(c.file,
|
|
398
|
+
enclosingFunctionNode(c.expr), c.expr, 0));
|
|
399
|
+
return values?.every(Boolean) ? values : null;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** The served app key, an unresolved in-process app, or null. */
|
|
403
|
+
resolve(file, line, receiver, name) {
|
|
404
|
+
const clients = this.clientValues(file, line, receiver, name);
|
|
379
405
|
if (!clients || clients.length === 0 || clients.some(c => c.expr?.type !== 'call')) return null;
|
|
380
406
|
const keys = clients.map(c => this.clientAppOnce(c.file, c.expr));
|
|
381
407
|
if (keys.every(key => key && key === keys[0])) {
|
package/core/ir.js
CHANGED
|
@@ -84,7 +84,7 @@ function normalizeSymbol(symbol, family, language, kind, owner = null) {
|
|
|
84
84
|
'annotationType', 'valueShape', 'friendOf', 'templateParams', 'delegateParams',
|
|
85
85
|
'typedefName', 'forwardDeclaration', 'anonymousNamespace', 'specialization',
|
|
86
86
|
'memberConst', 'refQualifier', 'fieldPointers', 'memberPaths', 'instanceAttribute',
|
|
87
|
-
'ambientAugmentation',
|
|
87
|
+
'ambientAugmentation', 'requiredMember', 'protocolClass',
|
|
88
88
|
];
|
|
89
89
|
for (const field of passthrough) {
|
|
90
90
|
if (symbol[field] !== undefined && symbol[field] !== null) {
|
|
@@ -261,6 +261,10 @@ function createFileIR({
|
|
|
261
261
|
...(parsed.reflectionSites?.length > 0 && {
|
|
262
262
|
reflectionSites: parsed.reflectionSites.map(item => ({ ...item })),
|
|
263
263
|
}),
|
|
264
|
+
// Uses of the reflection and code-execution vocabulary, counted in
|
|
265
|
+
// the tree (core/reflection.js).
|
|
266
|
+
...(parsed.reflectionUses > 0 && { reflectionUses: parsed.reflectionUses }),
|
|
267
|
+
...(parsed.evalUses > 0 && { evalUses: parsed.evalUses }),
|
|
264
268
|
...(Array.isArray(parsed.computedDispatchSites) && {
|
|
265
269
|
computedDispatchSites: parsed.computedDispatchSites.map(item => ({ ...item })),
|
|
266
270
|
}),
|