ucn 5.4.1 → 5.5.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 +58 -236
- package/.claude/skills/ucn/references/commands.md +144 -68
- package/.claude/skills/ucn/references/resolution-rules.md +171 -0
- package/.claude/skills/ucn/references/trust-contract.md +27 -33
- package/README.md +18 -12
- package/cli/index.js +42 -16
- package/core/accessors.js +118 -3
- package/core/account.js +30 -0
- package/core/analysis.js +508 -264
- package/core/ast-analysis.js +76 -117
- package/core/async-producers.js +1729 -0
- package/core/bridge.js +1877 -268
- package/core/brief.js +6 -0
- package/core/build-worker.js +78 -14
- package/core/cache.js +522 -31
- package/core/callers.js +12053 -1047
- package/core/check.js +30 -38
- package/core/class-identity.js +1028 -0
- package/core/command-contracts.js +6 -6
- package/core/configuration-items.js +86 -0
- package/core/contract-membership.js +2262 -0
- package/core/cpp-scope.js +532 -0
- package/core/deadcode.js +171 -49
- package/core/decorator-binding.js +260 -0
- package/core/discovery.js +281 -164
- package/core/errors.js +52 -0
- package/core/execute.js +21 -9
- package/core/external-macros.js +326 -0
- package/core/graph-build.js +277 -8
- package/core/graph.js +35 -2
- package/core/ignore-rules.js +472 -0
- package/core/imports.js +504 -42
- package/core/index-ir.js +94 -11
- package/core/inprocess-clients.js +636 -0
- package/core/ir.js +87 -8
- package/core/macro-expansion.js +1416 -0
- package/core/output/analysis-ext.js +6 -6
- package/core/output/analysis.js +18 -2
- package/core/output/brief.js +3 -0
- package/core/output/doctor.js +4 -0
- package/core/output/endpoints.js +15 -6
- package/core/output/find.js +11 -2
- package/core/output/lines.js +8 -2
- package/core/output/refactoring.js +121 -14
- package/core/output/reporting.js +31 -3
- package/core/output/shared.js +14 -0
- package/core/output-budget.js +316 -122
- package/core/parallel-build.js +124 -47
- package/core/parser.js +3 -2
- package/core/project.js +457 -60
- package/core/provenance-facts.js +156 -12
- package/core/provenance-overload.js +30 -3
- package/core/provenance.js +104 -30
- package/core/pytest-fixtures.js +434 -0
- package/core/python-fixture-flow.js +17 -3
- package/core/python-modules.js +173 -0
- package/core/reflection.js +877 -0
- package/core/registry.js +1 -1
- package/core/reporting.js +234 -52
- package/core/request-helpers.js +561 -0
- package/core/route-graph.js +1922 -0
- package/core/route-lists.js +2414 -0
- package/core/rust-macro-expansion.js +2113 -0
- package/core/rust-modules.js +185 -0
- package/core/search.js +121 -29
- package/core/shared.js +19 -1
- package/core/test-name-bindings.js +256 -0
- package/core/type-denotation.js +1161 -0
- package/core/type-references.js +3229 -0
- package/core/verify.js +3360 -672
- package/languages/adapter.js +31 -5
- package/languages/c-family.js +4061 -212
- package/languages/c-macro-invocations.js +380 -0
- package/languages/c-preprocessor.js +254 -0
- package/languages/csharp.js +1406 -49
- package/languages/go.js +1243 -70
- package/languages/index.js +728 -6
- package/languages/java.js +473 -61
- package/languages/javascript.js +1270 -197
- package/languages/lexical-scope.js +1280 -0
- package/languages/python.js +1557 -72
- package/languages/rust-macro-rules.js +1020 -0
- package/languages/rust-value-flow.js +416 -0
- package/languages/rust.js +2237 -115
- package/languages/utils.js +298 -4
- package/mcp/server.js +25 -11
- package/package.json +1 -1
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a Rust call's value goes, from the syntax tree alone (fix #372).
|
|
3
|
+
*
|
|
4
|
+
* Shared by the Rust parser, which records per call record whether the
|
|
5
|
+
* value can possibly be lost or misused (`valueConsumed` / `consumingMethod`),
|
|
6
|
+
* and by audit-async, which classifies a resolved future's flow. The one
|
|
7
|
+
* decision that needs project facts (a method called on the value) is left
|
|
8
|
+
* to the caller: with no producer it is reported as kind 'member', otherwise
|
|
9
|
+
* `memberVerdict(future, member, isMethod, exprNode)` decides.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const { sameNode } = require('./utils');
|
|
15
|
+
|
|
16
|
+
function fieldIs(parent, field, node) {
|
|
17
|
+
const child = parent.childForFieldName(field);
|
|
18
|
+
return !!child && sameNode(child, node);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function hasChildToken(node, tokenType) {
|
|
22
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
23
|
+
if (node.child(i).type === tokenType) return true;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const RUST_FUNCTION_BOUNDARY = new Set(['function_item', 'closure_expression']);
|
|
29
|
+
|
|
30
|
+
function rustPatternBinds(pattern, name) {
|
|
31
|
+
if (!pattern) return false;
|
|
32
|
+
if (pattern.type === 'identifier') return pattern.text === name;
|
|
33
|
+
if (pattern.type === 'field_identifier' || pattern.type === 'type_identifier' ||
|
|
34
|
+
pattern.type === 'scoped_identifier') return false;
|
|
35
|
+
for (let i = 0; i < pattern.namedChildCount; i++) {
|
|
36
|
+
const child = pattern.namedChild(i);
|
|
37
|
+
// `Some(x)`: the variant path is not a binding.
|
|
38
|
+
if (sameNode(pattern.childForFieldName('type'), child)) continue;
|
|
39
|
+
if (rustPatternBinds(child, name)) return true;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function rustLetPattern(letNode) {
|
|
45
|
+
return letNode.childForFieldName('pattern');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function rustMatchDestructures(matchNode) {
|
|
49
|
+
const body = matchNode.childForFieldName('body');
|
|
50
|
+
for (const arm of body?.namedChildren || []) {
|
|
51
|
+
if (arm.type !== 'match_arm') continue;
|
|
52
|
+
const pattern = arm.childForFieldName('pattern');
|
|
53
|
+
const inner = pattern?.type === 'match_pattern' ? pattern.namedChild(0) : pattern;
|
|
54
|
+
if (inner && inner.type !== 'identifier' && !(pattern.namedChildCount > 1 && inner.type === 'identifier')) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Where an unpolled Rust future value goes. Returns
|
|
63
|
+
* { kind: 'awaited' | 'flow' } polled or handed on
|
|
64
|
+
* { kind: 'discarded' } statement position
|
|
65
|
+
* { kind: 'dropped' } `let _ = fut;`
|
|
66
|
+
* { kind: 'stored', holder, binding, declared } plain local
|
|
67
|
+
* { kind: 'used-as-value', member? } treated as its Output
|
|
68
|
+
*/
|
|
69
|
+
function rustValueFlow(exprNode, future, memberVerdict) {
|
|
70
|
+
let expr = exprNode;
|
|
71
|
+
for (let depth = 0; depth < 32; depth++) {
|
|
72
|
+
const parent = expr.parent;
|
|
73
|
+
if (!parent) return { kind: 'flow' };
|
|
74
|
+
switch (parent.type) {
|
|
75
|
+
case 'parenthesized_expression':
|
|
76
|
+
case 'unsafe_block':
|
|
77
|
+
case 'else_clause':
|
|
78
|
+
case 'match_arm':
|
|
79
|
+
case 'match_block':
|
|
80
|
+
expr = parent;
|
|
81
|
+
continue;
|
|
82
|
+
case 'block': {
|
|
83
|
+
// Only the tail expression is the block's value.
|
|
84
|
+
let tail = parent.namedChildCount - 1;
|
|
85
|
+
while (tail >= 0 && ['line_comment', 'block_comment'].includes(parent.namedChild(tail).type)) tail--;
|
|
86
|
+
if (tail < 0 || !sameNode(parent.namedChild(tail), expr)) return { kind: 'flow' };
|
|
87
|
+
expr = parent;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
case 'if_expression':
|
|
91
|
+
if (fieldIs(parent, 'condition', expr)) return { kind: 'used-as-value' };
|
|
92
|
+
expr = parent;
|
|
93
|
+
continue;
|
|
94
|
+
case 'match_expression':
|
|
95
|
+
if (fieldIs(parent, 'value', expr)) {
|
|
96
|
+
return rustMatchDestructures(parent) ? { kind: 'used-as-value' } : { kind: 'flow' };
|
|
97
|
+
}
|
|
98
|
+
expr = parent;
|
|
99
|
+
continue;
|
|
100
|
+
case 'let_condition': {
|
|
101
|
+
if (!fieldIs(parent, 'value', expr)) return { kind: 'flow' };
|
|
102
|
+
const pattern = parent.childForFieldName('pattern');
|
|
103
|
+
return pattern && pattern.type !== 'identifier' ? { kind: 'used-as-value' } : { kind: 'flow' };
|
|
104
|
+
}
|
|
105
|
+
case 'await_expression':
|
|
106
|
+
return { kind: 'awaited' };
|
|
107
|
+
case 'expression_statement': {
|
|
108
|
+
// A block-like expression (`if`/`match`) without `;` at the
|
|
109
|
+
// end of a block is that block's value, not a statement.
|
|
110
|
+
const block = parent.parent;
|
|
111
|
+
if (block?.type === 'block' && !hasChildToken(parent, ';')) {
|
|
112
|
+
let tail = block.namedChildCount - 1;
|
|
113
|
+
while (tail >= 0 && ['line_comment', 'block_comment'].includes(block.namedChild(tail).type)) tail--;
|
|
114
|
+
if (tail >= 0 && sameNode(block.namedChild(tail), parent)) {
|
|
115
|
+
expr = block;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { kind: 'discarded' };
|
|
120
|
+
}
|
|
121
|
+
case 'let_declaration': {
|
|
122
|
+
if (!fieldIs(parent, 'value', expr)) return { kind: 'flow' };
|
|
123
|
+
const pattern = rustLetPattern(parent);
|
|
124
|
+
if (!pattern || pattern.text === '_') return { kind: 'dropped' };
|
|
125
|
+
if (pattern.type === 'identifier') {
|
|
126
|
+
return { kind: 'stored', holder: parent, binding: pattern.text, declared: true };
|
|
127
|
+
}
|
|
128
|
+
return { kind: 'used-as-value' };
|
|
129
|
+
}
|
|
130
|
+
case 'assignment_expression': {
|
|
131
|
+
if (!fieldIs(parent, 'right', expr)) return { kind: 'flow' };
|
|
132
|
+
const left = parent.childForFieldName('left');
|
|
133
|
+
if (left?.type === 'identifier') {
|
|
134
|
+
return { kind: 'stored', holder: parent, binding: left.text, declared: false };
|
|
135
|
+
}
|
|
136
|
+
return left?.text === '_' ? { kind: 'dropped' } : { kind: 'flow' };
|
|
137
|
+
}
|
|
138
|
+
case 'try_expression':
|
|
139
|
+
case 'binary_expression':
|
|
140
|
+
case 'unary_expression':
|
|
141
|
+
case 'compound_assignment_expr':
|
|
142
|
+
case 'range_expression':
|
|
143
|
+
return { kind: 'used-as-value' };
|
|
144
|
+
case 'index_expression':
|
|
145
|
+
return sameNode(parent.namedChild(0), expr) ? { kind: 'used-as-value' } : { kind: 'flow' };
|
|
146
|
+
case 'for_expression':
|
|
147
|
+
return fieldIs(parent, 'value', expr) ? { kind: 'used-as-value' } : { kind: 'flow' };
|
|
148
|
+
case 'while_expression':
|
|
149
|
+
return fieldIs(parent, 'condition', expr) ? { kind: 'used-as-value' } : { kind: 'flow' };
|
|
150
|
+
case 'call_expression':
|
|
151
|
+
return fieldIs(parent, 'function', expr) ? { kind: 'used-as-value' } : { kind: 'flow' };
|
|
152
|
+
case 'field_expression': {
|
|
153
|
+
if (!fieldIs(parent, 'value', expr)) return { kind: 'flow' };
|
|
154
|
+
const member = parent.childForFieldName('field')?.text;
|
|
155
|
+
const call = parent.parent;
|
|
156
|
+
const isMethod = call?.type === 'call_expression' && fieldIs(call, 'function', parent);
|
|
157
|
+
// Unresolved producer (a first, cheap pass): undecided.
|
|
158
|
+
if (!future) return { kind: 'member', member, node: expr };
|
|
159
|
+
return memberVerdict(future, member, isMethod, expr);
|
|
160
|
+
}
|
|
161
|
+
default:
|
|
162
|
+
// Arguments, collections, struct fields, returns, closures,
|
|
163
|
+
// references, casts, `Box::pin(..)`, macro token trees.
|
|
164
|
+
return { kind: 'flow' };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { kind: 'flow' };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Follow a local that received a future: the first use that takes it for its
|
|
172
|
+
* Output, or whether it is used at all before it is shadowed, reassigned or
|
|
173
|
+
* goes out of scope. Returns { read: bool, misuse: node|null }.
|
|
174
|
+
*/
|
|
175
|
+
function rustStoredFutureUse(holder, name, future, memberVerdict) {
|
|
176
|
+
let statement = holder;
|
|
177
|
+
while (statement.parent && statement.parent.type !== 'block') statement = statement.parent;
|
|
178
|
+
const declBlock = statement.parent;
|
|
179
|
+
if (!declBlock) return { read: true, misuse: null };
|
|
180
|
+
let read = false;
|
|
181
|
+
let misuse = null;
|
|
182
|
+
let stop = false;
|
|
183
|
+
// A loop around an assignment can read the value on the next iteration.
|
|
184
|
+
if (holder.type === 'assignment_expression') {
|
|
185
|
+
for (let up = holder.parent; up && !RUST_FUNCTION_BOUNDARY.has(up.type); up = up.parent) {
|
|
186
|
+
if (['loop_expression', 'while_expression', 'for_expression'].includes(up.type)) return { read: true, misuse: null };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Only nodes whose text holds `name` can read, misuse, rebind or
|
|
190
|
+
// overwrite the local (fix #388): the walk skips every other subtree.
|
|
191
|
+
// Offsets of every substring occurrence in the region the walk can
|
|
192
|
+
// reach (the declaring block, or the enclosing function for an
|
|
193
|
+
// assignment that may be read after its block).
|
|
194
|
+
let region = declBlock;
|
|
195
|
+
if (holder.type === 'assignment_expression') {
|
|
196
|
+
region = null;
|
|
197
|
+
for (let up = declBlock; up; up = up.parent) {
|
|
198
|
+
region = up;
|
|
199
|
+
if (RUST_FUNCTION_BOUNDARY.has(up.type)) break;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const occurrences = [];
|
|
203
|
+
{
|
|
204
|
+
const text = region.text;
|
|
205
|
+
const base = region.startIndex;
|
|
206
|
+
for (let at = name ? text.indexOf(name) : -1; at >= 0; at = text.indexOf(name, at + 1)) {
|
|
207
|
+
occurrences.push(base + at);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const mayHoldName = node => {
|
|
211
|
+
const start = node.startIndex;
|
|
212
|
+
let lo = 0;
|
|
213
|
+
let hi = occurrences.length;
|
|
214
|
+
while (lo < hi) {
|
|
215
|
+
const mid = (lo + hi) >> 1;
|
|
216
|
+
if (occurrences[mid] < start) lo = mid + 1;
|
|
217
|
+
else hi = mid;
|
|
218
|
+
}
|
|
219
|
+
return lo < occurrences.length && occurrences[lo] + name.length <= node.endIndex;
|
|
220
|
+
};
|
|
221
|
+
const visit = node => {
|
|
222
|
+
if (stop || misuse) return;
|
|
223
|
+
if (!mayHoldName(node)) return;
|
|
224
|
+
switch (node.type) {
|
|
225
|
+
case 'function_item':
|
|
226
|
+
return;
|
|
227
|
+
case 'closure_expression':
|
|
228
|
+
if (rustPatternBinds(node.childForFieldName('parameters'), name)) return;
|
|
229
|
+
break;
|
|
230
|
+
case 'match_arm':
|
|
231
|
+
if (rustPatternBinds(node.childForFieldName('pattern'), name)) return;
|
|
232
|
+
break;
|
|
233
|
+
case 'token_tree':
|
|
234
|
+
// Macro arguments (`join!`, `select!`, `println!`) consume it.
|
|
235
|
+
if (tokenTreeNames(node, name)) read = true;
|
|
236
|
+
return;
|
|
237
|
+
case 'identifier':
|
|
238
|
+
if (node.text === name) {
|
|
239
|
+
read = true;
|
|
240
|
+
const flow = rustValueFlow(node, future, memberVerdict);
|
|
241
|
+
if (flow.kind === 'used-as-value' || flow.kind === 'member') misuse = node;
|
|
242
|
+
}
|
|
243
|
+
return;
|
|
244
|
+
case 'block': {
|
|
245
|
+
for (let i = 0; i < node.namedChildCount && !stop && !misuse; i++) {
|
|
246
|
+
const child = node.namedChild(i);
|
|
247
|
+
if (child.type === 'let_declaration' && rustPatternBinds(rustLetPattern(child), name)) {
|
|
248
|
+
const value = child.childForFieldName('value');
|
|
249
|
+
if (value) visit(value);
|
|
250
|
+
return; // shadowed for the rest of this block
|
|
251
|
+
}
|
|
252
|
+
visit(child);
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
case 'assignment_expression': {
|
|
257
|
+
const left = node.childForFieldName('left');
|
|
258
|
+
if (left?.type === 'identifier' && left.text === name) {
|
|
259
|
+
const right = node.childForFieldName('right');
|
|
260
|
+
if (right) visit(right);
|
|
261
|
+
stop = true; // overwritten: the old future is dropped
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
default:
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
for (let i = 0; i < node.namedChildCount && !stop && !misuse; i++) visit(node.namedChild(i));
|
|
270
|
+
};
|
|
271
|
+
let started = false;
|
|
272
|
+
for (let i = 0; i < declBlock.namedChildCount && !stop && !misuse; i++) {
|
|
273
|
+
const child = declBlock.namedChild(i);
|
|
274
|
+
if (!started) {
|
|
275
|
+
if (sameNode(child, statement)) {
|
|
276
|
+
started = true;
|
|
277
|
+
// `x = fut` inside an expression statement: nothing else in it.
|
|
278
|
+
}
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (child.type === 'let_declaration' && rustPatternBinds(rustLetPattern(child), name)) {
|
|
282
|
+
const value = child.childForFieldName('value');
|
|
283
|
+
if (value) visit(value);
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
visit(child);
|
|
287
|
+
}
|
|
288
|
+
// An assignment to an outer local stays readable after its block ends.
|
|
289
|
+
if (!read && !stop && !misuse && holder.type === 'assignment_expression') {
|
|
290
|
+
let inner = declBlock;
|
|
291
|
+
for (let outer = inner.parent; outer && !stop && !misuse; inner = outer, outer = outer.parent) {
|
|
292
|
+
if (RUST_FUNCTION_BOUNDARY.has(outer.type)) break;
|
|
293
|
+
if (outer.type !== 'block') continue;
|
|
294
|
+
let after = false;
|
|
295
|
+
for (let i = 0; i < outer.namedChildCount && !stop && !misuse; i++) {
|
|
296
|
+
const child = outer.namedChild(i);
|
|
297
|
+
if (!after) {
|
|
298
|
+
if (child.startIndex <= inner.startIndex && child.endIndex >= inner.endIndex) after = true;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
visit(child);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return { read: read || !!misuse, misuse };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function tokenTreeNames(node, name) {
|
|
309
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
310
|
+
const child = node.namedChild(i);
|
|
311
|
+
if (child.type === 'identifier' && child.text === name) return true;
|
|
312
|
+
if (child.type === 'token_tree' && tokenTreeNames(child, name)) return true;
|
|
313
|
+
}
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Parser fact (fix #372): can this call's value be lost or used as a
|
|
319
|
+
* resolved value, judged before knowing what the call returns? Mirrors
|
|
320
|
+
* audit-async's pre-resolution test exactly: a value that is awaited or
|
|
321
|
+
* flows on is consumed; a method called on it (directly or on the local it
|
|
322
|
+
* is stored in) is recorded as `consumingMethod` for the name-level Output
|
|
323
|
+
* test; anything else (discarded, dropped, stored and never read, used as
|
|
324
|
+
* a value, a field read) stays open.
|
|
325
|
+
* @param {object} callNode - call_expression
|
|
326
|
+
* @returns {{ valueConsumed: string|null, consumingMethod: string|null }}
|
|
327
|
+
*/
|
|
328
|
+
function rustValueFacts(callNode) {
|
|
329
|
+
const open = { valueConsumed: null, consumingMethod: null };
|
|
330
|
+
let expr = callNode;
|
|
331
|
+
while (expr.parent?.type === 'parenthesized_expression') expr = expr.parent;
|
|
332
|
+
const parent = expr.parent;
|
|
333
|
+
if (parent?.type === 'await_expression') return { valueConsumed: 'awaited', consumingMethod: null };
|
|
334
|
+
if (parent?.type === 'arguments') return { valueConsumed: 'argument', consumingMethod: null };
|
|
335
|
+
const methodOf = flow => {
|
|
336
|
+
const member = flow.node?.parent;
|
|
337
|
+
const call = member?.parent;
|
|
338
|
+
return call?.type === 'call_expression' && fieldIs(call, 'function', member) ? flow.member || null : null;
|
|
339
|
+
};
|
|
340
|
+
const flow = rustValueFlow(callNode, null, null);
|
|
341
|
+
if (flow.kind === 'awaited') return { valueConsumed: 'awaited', consumingMethod: null };
|
|
342
|
+
if (flow.kind === 'flow') return { valueConsumed: 'flow', consumingMethod: null };
|
|
343
|
+
if (flow.kind === 'member') {
|
|
344
|
+
const method = methodOf(flow);
|
|
345
|
+
return method ? { valueConsumed: null, consumingMethod: method } : open;
|
|
346
|
+
}
|
|
347
|
+
if (flow.kind !== 'stored') return open;
|
|
348
|
+
const use = rustStoredFutureUse(flow.holder, flow.binding, null, null);
|
|
349
|
+
if (!use.read) return open;
|
|
350
|
+
if (!use.misuse) return { valueConsumed: 'stored', consumingMethod: null };
|
|
351
|
+
const misuseFlow = rustValueFlow(use.misuse, null, null);
|
|
352
|
+
if (misuseFlow.kind !== 'member') return open;
|
|
353
|
+
const method = methodOf(misuseFlow);
|
|
354
|
+
return method ? { valueConsumed: null, consumingMethod: method } : open;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Standard-library path roots: a `std::..::f()` call never reaches a
|
|
358
|
+
// project definition.
|
|
359
|
+
const STD_ROOTS = new Set(['std', 'core', 'alloc']);
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Compact file-level shape of a call record whose value is not consumed
|
|
363
|
+
* where produced (fix #372), persisted per file so audit-async decides
|
|
364
|
+
* whether a file can hold a lost future without loading its call records:
|
|
365
|
+
* name bare call
|
|
366
|
+
* name.Type method call (Type = receiverType, may be empty)
|
|
367
|
+
* name:Last path call (Last = last receiver segment, 'std' for std roots)
|
|
368
|
+
* ...~member a method is called on the value
|
|
369
|
+
* @param {object} record - call record
|
|
370
|
+
*/
|
|
371
|
+
function openCallShape(record) {
|
|
372
|
+
let shape = record.name;
|
|
373
|
+
if (record.isPathCall) {
|
|
374
|
+
const parts = String(record.receiver || '').split('::').filter(Boolean);
|
|
375
|
+
shape += ':' + (STD_ROOTS.has(parts[0]) ? 'std' : (parts[parts.length - 1] || ''));
|
|
376
|
+
} else if (record.isMethod) {
|
|
377
|
+
shape += '.' + (record.receiverType || '');
|
|
378
|
+
}
|
|
379
|
+
return record.consumingMethod ? `${shape}~${record.consumingMethod}` : shape;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* The call-record fields an openCallShape keeps, as a record-shaped object
|
|
384
|
+
* (a superset test: fields the shape drops only ever narrow a match).
|
|
385
|
+
*/
|
|
386
|
+
function parseOpenCallShape(shape) {
|
|
387
|
+
const text = String(shape);
|
|
388
|
+
const tilde = text.indexOf('~');
|
|
389
|
+
const head = tilde < 0 ? text : text.slice(0, tilde);
|
|
390
|
+
const record = tilde < 0 ? {} : { consumingMethod: text.slice(tilde + 1) };
|
|
391
|
+
const colon = head.indexOf(':');
|
|
392
|
+
const dot = head.indexOf('.');
|
|
393
|
+
if (colon >= 0) {
|
|
394
|
+
const last = head.slice(colon + 1);
|
|
395
|
+
return { ...record, name: head.slice(0, colon), isMethod: true, isPathCall: true,
|
|
396
|
+
receiver: last === 'std' ? 'std::_' : last };
|
|
397
|
+
}
|
|
398
|
+
if (dot >= 0) {
|
|
399
|
+
const type = head.slice(dot + 1);
|
|
400
|
+
return { ...record, name: head.slice(0, dot), isMethod: true, ...(type && { receiverType: type }) };
|
|
401
|
+
}
|
|
402
|
+
return { ...record, name: head };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
module.exports = {
|
|
406
|
+
openCallShape,
|
|
407
|
+
parseOpenCallShape,
|
|
408
|
+
RUST_FUNCTION_BOUNDARY,
|
|
409
|
+
rustPatternBinds,
|
|
410
|
+
rustLetPattern,
|
|
411
|
+
rustMatchDestructures,
|
|
412
|
+
rustValueFlow,
|
|
413
|
+
rustStoredFutureUse,
|
|
414
|
+
tokenTreeNames,
|
|
415
|
+
rustValueFacts,
|
|
416
|
+
};
|