ucn 5.5.3 → 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.
Files changed (71) hide show
  1. package/.claude/skills/ucn/SKILL.md +35 -12
  2. package/.claude/skills/ucn/references/commands.md +18 -14
  3. package/.claude/skills/ucn/references/resolution-rules.md +4 -0
  4. package/.claude/skills/ucn/references/trust-contract.md +4 -4
  5. package/CHANGELOG.md +474 -0
  6. package/README.md +69 -29
  7. package/cli/flags.js +184 -0
  8. package/cli/index.js +137 -324
  9. package/core/account.js +44 -87
  10. package/core/analysis.js +273 -107
  11. package/core/ast-analysis.js +2 -0
  12. package/core/bridge.js +14 -0
  13. package/core/cache.js +101 -16
  14. package/core/callers.js +352 -70
  15. package/core/capture.js +331 -0
  16. package/core/change-kinds.js +888 -0
  17. package/core/check.js +1117 -139
  18. package/core/command-contracts.js +7 -6
  19. package/core/constructed-class.js +1195 -0
  20. package/core/contract-membership.js +142 -0
  21. package/core/deadcode.js +362 -84
  22. package/core/deletion.js +401 -0
  23. package/core/discovery.js +104 -9
  24. package/core/entrypoints.js +12 -6
  25. package/core/evidence-bands.js +68 -0
  26. package/core/evidence-bands.json +44 -0
  27. package/core/execute.js +172 -161
  28. package/core/hot-summary.js +241 -0
  29. package/core/index-ir.js +3 -1
  30. package/core/ir.js +5 -1
  31. package/core/member-signatures.js +789 -0
  32. package/core/output/analysis-ext.js +20 -10
  33. package/core/output/analysis.js +98 -60
  34. package/core/output/check.js +98 -37
  35. package/core/output/doctor.js +34 -1
  36. package/core/output/find.js +2 -1
  37. package/core/output/lines.js +63 -3
  38. package/core/output/public.js +29 -11
  39. package/core/output/refactoring.js +32 -0
  40. package/core/output/reporting.js +15 -2
  41. package/core/output/search.js +11 -2
  42. package/core/output/tracing.js +35 -7
  43. package/core/output-budget.js +124 -57
  44. package/core/parallel-build.js +3 -3
  45. package/core/project.js +106 -23
  46. package/core/provenance-type.js +12 -5
  47. package/core/public-command.js +35 -9
  48. package/core/python-modules.js +6 -0
  49. package/core/reflection.js +151 -23
  50. package/core/registry.js +623 -72
  51. package/core/reporting.js +69 -34
  52. package/core/review-items.js +75 -0
  53. package/core/search.js +89 -128
  54. package/core/shared.js +0 -45
  55. package/core/snapshot.js +350 -0
  56. package/core/source-universe.js +707 -0
  57. package/core/test-units.js +3624 -0
  58. package/core/tracing.js +150 -96
  59. package/core/verify.js +27 -11
  60. package/languages/adapter.js +8 -3
  61. package/languages/c-family.js +12 -0
  62. package/languages/csharp.js +634 -0
  63. package/languages/go.js +120 -0
  64. package/languages/index.js +422 -4
  65. package/languages/java.js +712 -0
  66. package/languages/javascript.js +1460 -2
  67. package/languages/lexical-scope.js +68 -0
  68. package/languages/python.js +1672 -1
  69. package/languages/rust.js +91 -0
  70. package/mcp/server.js +44 -110
  71. package/package.json +10 -5
@@ -0,0 +1,888 @@
1
+ /**
2
+ * core/change-kinds.js - What did a change do to a callable's contract?
3
+ *
4
+ * A caller relies on three things, and a diff can touch each of them
5
+ * without touching the others:
6
+ *
7
+ * header every token of the declaration outside its body: name,
8
+ * parameters, return annotation, modifiers, decorators and
9
+ * attributes, template head. Compared token by token, so a
10
+ * reformatted header is the same header.
11
+ * kind what the BODY makes of the callable where the language lets
12
+ * it (trait `callableKindFromBody`): a Python `def` becomes a
13
+ * generator when its own body yields.
14
+ * return where the language takes the return contract from the body
15
+ * (trait `inferredReturnForms`: no annotation, a placeholder
16
+ * type, an opaque type, a body instantiated per caller), the
17
+ * expressions that produce the result and every statement
18
+ * connected to them through a variable name.
19
+ * members where a member's type is whatever the class's methods
20
+ * assign to it (trait `memberTypesFromAssignments`), the
21
+ * assignments the body makes to members of its receiver.
22
+ *
23
+ * A change that leaves all three alone is INERT: no caller can observe it
24
+ * through the contract, however many callers there are. Anything else is a
25
+ * CONTRACT change, named by what moved.
26
+ *
27
+ * Everything here is read from the syntax tree. A declaration the tree
28
+ * cannot place answers `unknown`, which callers treat as a contract change:
29
+ * a failed projection is never a clean one.
30
+ */
31
+
32
+ 'use strict';
33
+
34
+ const { getParser, safeParse, langTraits } = require('../languages');
35
+ const { CALLABLE_NODES, COMMENT_NODES } = require('./ast-analysis');
36
+
37
+ // Statements that end the callable with a value.
38
+ const RESULT_NODES = new Set(['return_statement', 'return_expression']);
39
+ const YIELD_NODES = new Set(['yield', 'yield_expression', 'yield_statement']);
40
+
41
+ // Statements that end a path of the callable without a value.
42
+ const EXIT_NODES = new Map([
43
+ ['throw_statement', 'throw'], ['raise_statement', 'throw'],
44
+ ['break_statement', 'break'], ['continue_statement', 'continue'],
45
+ ]);
46
+ const ASSERT_NODES = new Set(['assert_statement']);
47
+ // Statements a condition decides: with one that names nothing (`while
48
+ // True`, `for (;;)`, `if False`) the checker knows which way it goes.
49
+ const CONDITION_NODES = new Set(['while_statement', 'do_statement', 'for_statement', 'if_statement', 'elif_clause']);
50
+ const LOOP_NODES = new Set(['while_statement', 'do_statement', 'for_statement']);
51
+ const SWITCH_NODES = new Set(['switch_statement']);
52
+ const CASE_NODES = new Set(['switch_case', 'switch_default']);
53
+ // A context manager can swallow what its block raises: whether the code
54
+ // after the block is reached is a property of the manager.
55
+ const MANAGER_NODES = new Set(['with_statement', 'with_clause']);
56
+ const CALL_NODES = new Set(['call_expression', 'call']);
57
+ const AWAIT_NODES = new Set(['await_expression', 'await']);
58
+
59
+ // Nodes that hold declarations rather than being one: a callable's
60
+ // declaration never grows past them.
61
+ const CONTAINER_NODES = new Set([
62
+ 'program', 'module', 'source_file', 'translation_unit', 'compilation_unit',
63
+ 'class_body', 'interface_body', 'enum_body', 'declaration_list', 'field_declaration_list',
64
+ 'block', 'statement_block', 'compound_statement', 'object', 'object_type',
65
+ 'class_declaration', 'class_definition', 'class_specifier', 'struct_specifier',
66
+ 'impl_item', 'trait_item', 'namespace_definition', 'namespace_declaration',
67
+ 'arguments', 'argument_list', 'array', 'list', 'tuple', 'dictionary',
68
+ ]);
69
+
70
+ // Written before the declaration, not inside it.
71
+ const LEADING_NODES = new Set([
72
+ 'decorator', 'attribute_item', 'annotation', 'marker_annotation', 'attribute_list',
73
+ ]);
74
+
75
+ // Nodes that bind a name, and the field that holds what they bind. Only the
76
+ // languages whose return contract is read expression by expression need a
77
+ // row; the others compare the whole body.
78
+ const BINDING_FIELDS = Object.freeze({
79
+ // Python
80
+ assignment: ['left'],
81
+ augmented_assignment: ['left'],
82
+ for_statement: ['left'],
83
+ for_in_clause: ['left'],
84
+ as_pattern: ['alias'],
85
+ named_expression: ['name'],
86
+ function_definition: ['name'],
87
+ class_definition: ['name'],
88
+ // JavaScript / TypeScript
89
+ variable_declarator: ['name'],
90
+ assignment_expression: ['left'],
91
+ augmented_assignment_expression: ['left'],
92
+ update_expression: ['argument'],
93
+ for_in_statement: ['left'],
94
+ function_declaration: ['name'],
95
+ generator_function_declaration: ['name'],
96
+ class_declaration: ['name'],
97
+ catch_clause: ['parameter'],
98
+ });
99
+ // Statements that bind every name they list.
100
+ const BINDING_LISTS = new Set([
101
+ 'global_statement', 'nonlocal_statement', 'delete_statement',
102
+ 'import_statement', 'import_from_statement', 'except_clause',
103
+ ]);
104
+ // A binding form with no row above still binds: its type says so.
105
+ const BINDING_SHAPE = /assignment|declarator|pattern|binding/;
106
+ const BINDING_SHAPE_FIELDS = ['left', 'name', 'pattern', 'declarator', 'alias', 'argument'];
107
+
108
+ function sameNode(a, b) {
109
+ return !!a && !!b && a.id === b.id;
110
+ }
111
+
112
+ /** Leaf tokens of a subtree, comments left out, `skip` not entered. */
113
+ function tokensOf(node, skip, out = []) {
114
+ if (!node || COMMENT_NODES.has(node.type) || (skip && sameNode(node, skip))) return out;
115
+ if (node.childCount === 0) {
116
+ out.push([node.type, node.text]);
117
+ return out;
118
+ }
119
+ for (const child of node.children) tokensOf(child, skip, out);
120
+ return out;
121
+ }
122
+
123
+ function identifiersOf(node, out = new Set()) {
124
+ if (!node) return out;
125
+ if (node.childCount === 0) {
126
+ if (node.type.includes('identifier')) out.add(node.text);
127
+ return out;
128
+ }
129
+ for (const child of node.children) identifiersOf(child, out);
130
+ return out;
131
+ }
132
+
133
+ function within(node, range) {
134
+ const startRow = node.startPosition.row + 1;
135
+ const endRow = node.endPosition.row + 1;
136
+ if (startRow < range.startLine || endRow > range.endLine) return false;
137
+ if (range.startColumn != null && startRow === range.startLine &&
138
+ node.startPosition.column < range.startColumn) return false;
139
+ if (range.endColumn != null && endRow === range.endLine &&
140
+ node.endPosition.column > range.endColumn) return false;
141
+ return true;
142
+ }
143
+
144
+ /** The rows, and where the parser recorded them the columns, of a symbol. */
145
+ function rangeOf(symbol) {
146
+ const span = symbol.sourceSpan;
147
+ // A span is trusted only when it has all four coordinates: a decorated
148
+ // definition's span and its symbol's lines can disagree on where the
149
+ // declaration starts, and the wider of the two is the declaration.
150
+ const exact = span && [span.startLine, span.endLine, span.startColumn, span.endColumn]
151
+ .every(Number.isInteger);
152
+ return {
153
+ startLine: exact ? Math.min(symbol.startLine, span.startLine) : symbol.startLine,
154
+ endLine: exact ? Math.max(symbol.endLine || symbol.startLine, span.endLine)
155
+ : symbol.endLine || symbol.startLine,
156
+ ...(exact && span.startLine <= symbol.startLine && { startColumn: span.startColumn }),
157
+ ...(exact && span.endLine >= (symbol.endLine || symbol.startLine) && { endColumn: span.endColumn }),
158
+ };
159
+ }
160
+
161
+ /** The callable node a symbol's range holds: the outermost one in it. */
162
+ function callableNodeOf(root, range) {
163
+ let found = null;
164
+ const visit = node => {
165
+ const startRow = node.startPosition.row + 1;
166
+ const endRow = node.endPosition.row + 1;
167
+ if (endRow < range.startLine || startRow > range.endLine) return;
168
+ if (CALLABLE_NODES.has(node.type) && within(node, range)) {
169
+ if (!found || (node.endIndex - node.startIndex) > (found.endIndex - found.startIndex)) found = node;
170
+ return; // nested callables are inside this one
171
+ }
172
+ for (const child of node.namedChildren) visit(child);
173
+ };
174
+ visit(root);
175
+ return found;
176
+ }
177
+
178
+ /**
179
+ * The whole declaration a callable node belongs to: the node, grown through
180
+ * every wrapper that is part of the same declaration (a decorated or
181
+ * exported definition, a template head, the field or variable a function
182
+ * value is assigned to) and never into what merely contains it.
183
+ */
184
+ function declarationOf(node, range) {
185
+ let declaration = node;
186
+ while (declaration.parent && !CONTAINER_NODES.has(declaration.parent.type) &&
187
+ within(declaration.parent, range)) declaration = declaration.parent;
188
+ return declaration;
189
+ }
190
+
191
+ /** Own nodes of a body: nested callables are seen and not entered. */
192
+ function walkOwn(node, visit, path = []) {
193
+ for (const child of node.namedChildren) {
194
+ if (visit(child, path) === false) continue;
195
+ if (CALLABLE_NODES.has(child.type)) continue;
196
+ path.push(child.type);
197
+ walkOwn(child, visit, path);
198
+ path.pop();
199
+ }
200
+ }
201
+
202
+ function bindingTargets(node) {
203
+ const fields = BINDING_FIELDS[node.type] ||
204
+ (BINDING_SHAPE.test(node.type) ? BINDING_SHAPE_FIELDS : null);
205
+ if (!fields) return BINDING_LISTS.has(node.type) ? [node] : [];
206
+ return fields.map(field => node.childForFieldName(field)).filter(Boolean);
207
+ }
208
+
209
+ // Statement-level nodes: the units a body is made of. A compound statement
210
+ // is the unit of its own header (a condition, a loop target, an `except`
211
+ // pattern); the statements it holds are units of their own.
212
+ const STATEMENT_SHAPE = /(?:_statement|_declaration|_definition|_clause|^switch_case|^switch_default)$/;
213
+ const VARIABLE_LEAVES = new Set([
214
+ 'identifier', 'shorthand_property_identifier', 'shorthand_property_identifier_pattern',
215
+ ]);
216
+
217
+ /** A member or keyword name: spelled like a variable, naming none. */
218
+ function namesNoVariable(node) {
219
+ const parent = node.parent;
220
+ if (!parent) return false;
221
+ const member = parent.childForFieldName('attribute') || parent.childForFieldName('property') ||
222
+ (parent.type === 'keyword_argument' ? parent.childForFieldName('name') : null);
223
+ return !!member && member.id === node.id;
224
+ }
225
+
226
+ // A member access, and the field that holds the member's name.
227
+ const MEMBER_NODES = new Map([['member_expression', 'property'], ['attribute', 'attribute']]);
228
+
229
+ /** `this.a.b` as ['this', 'a', 'b'], or null when it is not a plain chain. */
230
+ function chainOf(node) {
231
+ if (node.type === 'this' || VARIABLE_LEAVES.has(node.type)) return [node.text];
232
+ const field = MEMBER_NODES.get(node.type);
233
+ if (!field) return null;
234
+ const object = node.childForFieldName('object');
235
+ const member = node.childForFieldName(field);
236
+ if (!object || !member || member.childCount > 0) return null;
237
+ const head = chainOf(object);
238
+ return head ? [...head, member.text] : null;
239
+ }
240
+
241
+ /** Do two references name the same thing, or one a part of the other? */
242
+ function overlaps(a, b) {
243
+ return a === b || a.startsWith(`${b}.`) || b.startsWith(`${a}.`);
244
+ }
245
+
246
+ /**
247
+ * A unit's own tokens and what it refers to: everything in the node except
248
+ * the statements it holds.
249
+ *
250
+ * A reference is a variable name, with one refinement. A member of the
251
+ * receiver (`this.count`, `self.count`) is its own reference: a type
252
+ * checker narrows and assigns `this.count` apart from `this.total`, and
253
+ * the receiver is no union a test of one member could narrow as a whole.
254
+ * Members of anything else stay under the variable (a test of `shape.kind`
255
+ * narrows `shape`, and with it `shape.radius`). A call on the receiver
256
+ * refers to the receiver itself: what it asserts about `this` is in its
257
+ * declaration.
258
+ *
259
+ * @param {object} [options] - { receiverMembers: the callable is a class
260
+ * member whose receiver is an instance of the class, selfName: the
261
+ * receiver parameter's name where the receiver is one, omit: a node
262
+ * whose references are not collected (the callee of a call shown to do
263
+ * nothing but return) }
264
+ */
265
+ function unitOf(node, options = {}) {
266
+ const { receiverMembers = false, selfName = null, omit = null } = options;
267
+ const tokens = [];
268
+ const names = new Set();
269
+ let yields = false;
270
+ const isReceiver = text => receiverMembers && (text === 'this' || (selfName !== null && text === selfName));
271
+ const visit = (candidate, top, quiet) => {
272
+ if (COMMENT_NODES.has(candidate.type)) return;
273
+ if (!top && candidate.isNamed && STATEMENT_SHAPE.test(candidate.type)) return;
274
+ if (YIELD_NODES.has(candidate.type)) yields = true;
275
+ if (!quiet && omit && sameNode(candidate, omit)) quiet = true;
276
+ if (!quiet && MEMBER_NODES.has(candidate.type)) {
277
+ const chain = chainOf(candidate);
278
+ if (chain && isReceiver(chain[0])) {
279
+ const parent = candidate.parent;
280
+ const called = !!parent && CALL_NODES.has(parent.type) &&
281
+ sameNode(parent.childForFieldName('function'), candidate);
282
+ const reach = called ? chain.slice(0, -1) : chain;
283
+ names.add(reach.slice(0, 2).join('.'));
284
+ quiet = true;
285
+ }
286
+ }
287
+ if (candidate.childCount === 0) {
288
+ tokens.push([candidate.type, candidate.text]);
289
+ if (!quiet && (candidate.type === 'this' ||
290
+ (VARIABLE_LEAVES.has(candidate.type) && !namesNoVariable(candidate)))) names.add(candidate.text);
291
+ return;
292
+ }
293
+ for (const child of candidate.children) visit(child, false, quiet);
294
+ };
295
+ visit(node, true, false);
296
+ return { tokens, names, yields };
297
+ }
298
+
299
+ /** The callee of a statement that is nothing but a call, or null. */
300
+ function bareCallee(statement) {
301
+ if (statement.type !== 'expression_statement' || statement.namedChildCount !== 1) return null;
302
+ let expression = statement.namedChildren[0];
303
+ if (AWAIT_NODES.has(expression.type)) expression = expression.namedChildren[0];
304
+ if (!expression || !CALL_NODES.has(expression.type)) return null;
305
+ return expression.childForFieldName('function');
306
+ }
307
+
308
+ /**
309
+ * Can a statement that only calls `callee` be shown to return to the next
310
+ * statement? A type checker ends the path at a call it is TOLD never
311
+ * returns (a declared `never` / `NoReturn`), so the question is what the
312
+ * callee is declared as:
313
+ * - a name the callable binds itself (a parameter, a local): unknown;
314
+ * - a function or an own method of the project whose every declaration
315
+ * says nothing of the kind (`facts.context.returning`);
316
+ * - a builtin of the language that returns (traits `ambientCallables`
317
+ * minus `ambientNeverCallables`; members of
318
+ * `ambientReturningNamespaces`), unless the file gives the name
319
+ * another meaning;
320
+ * - anything else (a member of a value, an import from outside the
321
+ * project): unknown, which is no.
322
+ */
323
+ function provablyReturns(callee, facts) {
324
+ const { traits, context, locals, ownerClass, selfName } = facts;
325
+ const shadowed = name => locals.has(name) || !!(context && context.fileNames && context.fileNames.has(name));
326
+ if (callee.type === 'identifier') {
327
+ const name = callee.text;
328
+ if (locals.has(name)) return false;
329
+ if (context && context.returning && context.returning(name, null)) return true;
330
+ return !!traits.ambientCallables && traits.ambientCallables.has(name) && !shadowed(name) &&
331
+ !(traits.ambientNeverCallables && traits.ambientNeverCallables.has(name));
332
+ }
333
+ const object = callee.childForFieldName('object');
334
+ const member = callee.childForFieldName('property') || callee.childForFieldName('attribute');
335
+ if (!object || !member) return false;
336
+ if (object.type === 'this' || (selfName && object.type === 'identifier' && object.text === selfName)) {
337
+ return !!(ownerClass && context && context.returning && context.returning(member.text, ownerClass));
338
+ }
339
+ let root = object;
340
+ while (root.childForFieldName('object')) root = root.childForFieldName('object');
341
+ return root.type === 'identifier' && !!traits.ambientReturningNamespaces &&
342
+ traits.ambientReturningNamespaces.has(root.text) && !shadowed(root.text);
343
+ }
344
+
345
+ /**
346
+ * Does this declaration say nothing that could make a call of it end the
347
+ * caller's path? Yes for a function or method with a body and either no
348
+ * return annotation or one made of keywords only (`void`, `None`) that is
349
+ * not a never type. No (unknown) for everything a checker could read a
350
+ * never type from: an annotation that names a type (it may be an alias of
351
+ * one), a decorator (it can replace the callable), a function held by a
352
+ * typed variable or field (it has that type), a declaration with no body
353
+ * (an overload signature).
354
+ *
355
+ * @param {string[]} neverTypes - trait `neverReturnTypes`
356
+ */
357
+ function declaresPlainReturn(root, symbol, neverTypes) {
358
+ if (!root || !symbol) return false;
359
+ const range = rangeOf(symbol);
360
+ const node = callableNodeOf(root, range);
361
+ if (!node || node.hasError || !node.childForFieldName('body')) return false;
362
+ // Everything the callable is written inside, up to the block or body
363
+ // that holds its declaration (a symbol's range can be the function
364
+ // value alone, without the variable it is assigned to).
365
+ for (let up = node; up && !CONTAINER_NODES.has(up.type); up = up.parent) {
366
+ if (up.type === 'decorated_definition' || up.namedChildren.some(child => child.type === 'decorator')) return false;
367
+ if (up.previousNamedSibling && LEADING_NODES.has(up.previousNamedSibling.type)) return false;
368
+ if (!sameNode(up, node) && up.childForFieldName('type')) return false;
369
+ }
370
+ const written = node.childForFieldName('return_type');
371
+ if (!written) return true;
372
+ return !tokensOf(written).some(([type, text]) =>
373
+ /identifier|string/.test(type) || (neverTypes || []).includes(text));
374
+ }
375
+
376
+ /**
377
+ * The return derivation of a body: what the type of its result can depend
378
+ * on, read conservatively from syntax.
379
+ *
380
+ * The result expressions come first, each with the control nesting it sits
381
+ * under (wrapping a `return` in an `if` lets the callable fall off its
382
+ * end). Then every statement connected to them through a variable name, in
383
+ * any position and transitively: a binding of a returned name, a call that
384
+ * mutates the object it names (`xs.push("text")` before `return xs`), a
385
+ * condition that narrows it (`typeof x === 'number'` before `return x`), a
386
+ * closure that assigns it, and whatever those statements name in turn;
387
+ * and every statement that sits under a condition of the slice. A type
388
+ * checker follows fewer of these; which ones is its business, and every
389
+ * one it does follow is here.
390
+ *
391
+ * Last, the statements that END a path, whatever they name: the result is
392
+ * the union of the ends that can be reached, so removing the `throw` at
393
+ * the end of a function gives it one more (falling off the end). These are
394
+ * `throw` / `raise`, `break` / `continue`, an assertion, a context
395
+ * manager, a condition that names nothing (`while True`, `if False`), and
396
+ * a statement that only calls something not shown to return
397
+ * (`provablyReturns`). Each is kept by kind and nesting, in source order
398
+ * with the slice: what is thrown, or what a call is passed, changes no
399
+ * type.
400
+ *
401
+ * Where a checker decides conditions from the names in them (trait
402
+ * `namedConditionsDecidable`: an interpreter version or platform test,
403
+ * `TYPE_CHECKING`, a constant its configuration defines), a condition is
404
+ * not opaque: the branch it rules out contributes nothing. There, every
405
+ * statement that holds a statement of the derivation is part of it, and so
406
+ * is every loop condition.
407
+ *
408
+ * A statement that shares no name with the slice and ends no path cannot
409
+ * reach the result.
410
+ *
411
+ * @param {object} [facts] - what `provablyReturns` asks; without it every
412
+ * bare call is one that may not return
413
+ */
414
+ function returnDerivation(body, facts = null) {
415
+ const units = [];
416
+ const decidable = !!(facts && facts.traits && facts.traits.namedConditionsDecidable);
417
+ const receiver = { receiverMembers: !!(facts && facts.receiverMembers), selfName: facts ? facts.selfName : null };
418
+ // An expression body is the result.
419
+ if (!body.namedChildren.some(child => STATEMENT_SHAPE.test(child.type)) &&
420
+ !['block', 'statement_block', 'compound_statement'].includes(body.type)) {
421
+ units.push({ at: body.startIndex, result: true, path: [], ...unitOf(body, receiver) });
422
+ }
423
+ const collect = (container, path, nested, holder) => {
424
+ for (const child of container.namedChildren) {
425
+ if (COMMENT_NODES.has(child.type)) continue;
426
+ const inner = nested || CALLABLE_NODES.has(child.type);
427
+ if (STATEMENT_SHAPE.test(child.type)) {
428
+ // A statement that only calls something shown to return:
429
+ // the call itself narrows nothing and ends nothing.
430
+ const callee = nested ? null : bareCallee(child);
431
+ const plainCall = !!(callee && facts && provablyReturns(callee, facts));
432
+ const unit = {
433
+ at: child.startIndex,
434
+ type: child.type,
435
+ path,
436
+ // The compound statement this one sits under.
437
+ holder,
438
+ ...unitOf(child, { ...receiver, omit: plainCall ? callee : null }),
439
+ };
440
+ // A `return` inside a closure is the closure's result, and
441
+ // what ends the closure's path ends only that.
442
+ unit.result = !nested && (RESULT_NODES.has(child.type) || unit.yields);
443
+ if (!nested && !unit.result) {
444
+ if (EXIT_NODES.has(child.type)) unit.exit = [EXIT_NODES.get(child.type)];
445
+ else if (ASSERT_NODES.has(child.type) || MANAGER_NODES.has(child.type)) unit.exit = [child.type, unit.tokens];
446
+ else if (CONDITION_NODES.has(child.type) && (unit.names.size === 0 ||
447
+ (decidable && LOOP_NODES.has(child.type) && child.childForFieldName('condition')))) {
448
+ unit.exit = ['static', unit.tokens];
449
+ }
450
+ else if (callee && !plainCall) unit.exit = ['call', callee.text];
451
+ }
452
+ units.push(unit);
453
+ collect(child, [...path, child.type], inner, unit);
454
+ } else {
455
+ collect(child, path, inner, holder);
456
+ }
457
+ }
458
+ };
459
+ collect(body, [], false, null);
460
+ const slice = new Set(units.filter(unit => unit.result));
461
+ const names = new Set();
462
+ for (const unit of slice) for (const name of unit.names) names.add(name);
463
+ const shares = unit => {
464
+ for (const mine of unit.names) for (const name of names) if (overlaps(mine, name)) return true;
465
+ return false;
466
+ };
467
+ // Data dependence: a shared variable name. Control dependence: a
468
+ // statement under a condition that is in the slice decides what that
469
+ // condition lets through (`if x is None: fail()` narrows `x` after it
470
+ // only while `fail()` never returns).
471
+ const held = unit => { for (let up = unit.holder; up; up = up.holder) if (slice.has(up)) return true; return false; };
472
+ for (let grew = slice.size > 0; grew;) {
473
+ grew = false;
474
+ for (const unit of units) {
475
+ if (slice.has(unit)) continue;
476
+ if (!held(unit) && !shares(unit)) continue;
477
+ slice.add(unit);
478
+ for (const name of unit.names) names.add(name);
479
+ grew = true;
480
+ }
481
+ }
482
+ const kept = new Set(units.filter(unit => slice.has(unit) || unit.exit));
483
+ // A `switch` that covers every value of its subject leaves no way past
484
+ // it, and a label narrows the subject inside its case: the subject and
485
+ // the labels of a switch that holds part of the derivation are part of
486
+ // it. Leaving the switch (`break`) is not such a part.
487
+ const switches = new Set();
488
+ for (const unit of kept) {
489
+ if (!slice.has(unit) && unit.exit && (unit.exit[0] === 'break' || unit.exit[0] === 'continue')) continue;
490
+ for (let up = unit.holder; up; up = up.holder) if (SWITCH_NODES.has(up.type)) switches.add(up);
491
+ }
492
+ for (const unit of units) {
493
+ if (switches.has(unit) || (CASE_NODES.has(unit.type) && switches.has(unit.holder))) kept.add(unit);
494
+ }
495
+ // Where a checker can decide a condition from the names in it, every
496
+ // statement that holds one of these decides whether it counts.
497
+ if (decidable) {
498
+ for (const unit of [...kept]) {
499
+ for (let up = unit.holder; up && !kept.has(up); up = up.holder) kept.add(up);
500
+ }
501
+ }
502
+ return JSON.stringify([...kept].sort((a, b) => a.at - b.at).map(unit => {
503
+ if (slice.has(unit)) return [unit.path, unit.tokens];
504
+ return unit.exit ? ['exit', unit.path, ...unit.exit] : ['guard', unit.path, unit.tokens];
505
+ }));
506
+ }
507
+
508
+ /**
509
+ * How the declaration states its return contract: 'declared' (a written
510
+ * type that is the contract), 'unannotated', 'placeholder' (a written type
511
+ * the compiler replaces with the body's), 'opaque' (a written type that
512
+ * still exposes the concrete value's properties).
513
+ */
514
+ function returnFormOf(node, declaration, symbol, traits) {
515
+ // A callable instantiated per use (a template, a member of a class
516
+ // template) is type-checked against each caller's arguments: its body
517
+ // is part of what a caller relies on.
518
+ if (symbol && (symbol.templateDependent || symbol.templateParams) &&
519
+ (traits.inferredReturnForms || []).includes('instantiated')) return 'instantiated';
520
+ // An async function returns an anonymous future built from its body,
521
+ // and where that future's properties leak to callers (auto traits),
522
+ // the body is part of the contract.
523
+ if (traits.asyncReturnIsOpaque && tokensOf(declaration, node.childForFieldName('body'))
524
+ .some(([type, text]) => type === 'async' || text === 'async')) return 'opaque';
525
+ // A callable the compiler may run (a constant-evaluated function) hands
526
+ // callers its value, not only its type: its body is the contract. The
527
+ // marker is one of the modifiers written before the name.
528
+ const markers = traits.compileTimeCallableMarkers;
529
+ if (markers && markers.length > 0) {
530
+ const name = node.childForFieldName('name') || node.childForFieldName('declarator');
531
+ const limit = name ? name.startIndex : node.startIndex;
532
+ const modifiers = [];
533
+ const collect = candidate => {
534
+ if (candidate.startIndex >= limit) return;
535
+ if (candidate.childCount === 0) { modifiers.push(candidate.text); return; }
536
+ for (const child of candidate.children) collect(child);
537
+ };
538
+ collect(declaration);
539
+ if (modifiers.some(text => markers.includes(text))) return 'evaluated';
540
+ }
541
+ const written = node.childForFieldName('return_type') || node.childForFieldName('result') ||
542
+ node.childForFieldName('type');
543
+ if (!written) return 'unannotated';
544
+ const text = written.text;
545
+ if (/\b(?:auto|decltype\s*\(\s*auto\s*\))(?![\w:])/.test(text) &&
546
+ !node.descendantsOfType('trailing_return_type').length) return 'placeholder';
547
+ if (/(?:^|[\s(<&])impl\s/.test(text)) return 'opaque';
548
+ return 'declared';
549
+ }
550
+
551
+ /**
552
+ * The contract of one callable in one revision of its file.
553
+ *
554
+ * @param {object} root - syntax tree root of that revision
555
+ * @param {string} language
556
+ * @param {object} symbol - { startLine, endLine, sourceSpan?, className? } in that revision
557
+ * @param {object} [context] - what the revision's file and the project say
558
+ * about callees: { fileNames: Set of names the file declares or imports,
559
+ * returning(name, ownerClass): every project declaration of the name
560
+ * (of the class's own method) is one a call of which returns }
561
+ * @returns {{header, generator, returnForm, inferred, derivation}|null}
562
+ */
563
+ function callableContract(root, language, symbol, context = null) {
564
+ if (!root || !symbol) return null;
565
+ const range = rangeOf(symbol);
566
+ const node = callableNodeOf(root, range);
567
+ if (!node) {
568
+ // A declaration with no body (a prototype, an interface method, an
569
+ // overload signature) is all header: every token in its range.
570
+ const header = [];
571
+ const collect = candidate => {
572
+ const startRow = candidate.startPosition.row + 1;
573
+ const endRow = candidate.endPosition.row + 1;
574
+ if (endRow < range.startLine || startRow > range.endLine) return;
575
+ if (candidate.childCount === 0) {
576
+ if (within(candidate, range) && !COMMENT_NODES.has(candidate.type)) header.push([candidate.type, candidate.text]);
577
+ return;
578
+ }
579
+ for (const child of candidate.children) collect(child);
580
+ };
581
+ collect(root);
582
+ if (header.length === 0) return null;
583
+ return {
584
+ header: JSON.stringify(header), generator: false, returnForm: 'declared', inferred: false,
585
+ derivation: null, memberBindings: null,
586
+ };
587
+ }
588
+ if (node.hasError) return null;
589
+ const body = node.childForFieldName('body');
590
+ const declaration = declarationOf(node, range);
591
+ const header = [];
592
+ // Decorators and attributes written before the declaration.
593
+ const leading = [];
594
+ for (let sibling = declaration.previousNamedSibling; sibling && LEADING_NODES.has(sibling.type);
595
+ sibling = sibling.previousNamedSibling) leading.unshift(sibling);
596
+ for (const item of leading) tokensOf(item, null, header);
597
+ tokensOf(declaration, body, header);
598
+
599
+ const traits = langTraits(language) || {};
600
+ const returnForm = returnFormOf(node, declaration, symbol, traits);
601
+ const inferred = (traits.inferredReturnForms || []).includes(returnForm);
602
+ let generator = false;
603
+ if (body) {
604
+ walkOwn(body, candidate => {
605
+ if (YIELD_NODES.has(candidate.type)) generator = true;
606
+ return true;
607
+ });
608
+ }
609
+ let derivation = null;
610
+ if (inferred && body) {
611
+ // Expression by expression where the form is "no annotation"; the
612
+ // whole body where the compiler deduces the type from all of it.
613
+ if (returnForm === 'unannotated') {
614
+ // Names the callable binds itself: its parameters and whatever
615
+ // its own statements assign.
616
+ const locals = new Set();
617
+ const parameters = node.childForFieldName('parameters') || node.childForFieldName('parameter');
618
+ if (parameters) identifiersOf(parameters, locals);
619
+ walkOwn(body, candidate => {
620
+ for (const target of bindingTargets(candidate)) identifiersOf(target, locals);
621
+ return true;
622
+ });
623
+ const leading = parameters ? parameters.namedChildren[0] : null;
624
+ const first = leading ? [...identifiersOf(leading)][0] : null;
625
+ const selfName = symbol.className && first && (traits.selfParam || []).includes(first) ? first : null;
626
+ // The receiver of a class member is an instance of the class.
627
+ // One whose type is WRITTEN (`this: A | B`, `self: Shape`) is
628
+ // whatever that says, a union included: a variable like any.
629
+ const typedReceiver = !!leading && (leading.descendantsOfType('this').length > 0 ||
630
+ (selfName !== null && leading.type !== 'identifier'));
631
+ derivation = returnDerivation(body, {
632
+ traits, context, locals, ownerClass: symbol.className || null, selfName,
633
+ receiverMembers: !!symbol.className && !typedReceiver,
634
+ });
635
+ } else {
636
+ derivation = JSON.stringify(tokensOf(body));
637
+ }
638
+ }
639
+ // Where a member's type is whatever the class's methods assign to it
640
+ // (trait `memberTypesFromAssignments`), every assignment to a member of
641
+ // the receiver is part of the class's contract, in whichever method it
642
+ // is written.
643
+ let memberBindings = null;
644
+ if (traits.memberTypesFromAssignments && body) {
645
+ const receiver = node.childForFieldName('parameters')?.namedChildren[0];
646
+ const receiverName = receiver ? [...identifiersOf(receiver)][0] : null;
647
+ const parts = [];
648
+ if (receiverName) {
649
+ walkOwn(body, candidate => {
650
+ for (const target of bindingTargets(candidate)) {
651
+ const members = target.type === 'attribute' ? [target] : target.descendantsOfType('attribute');
652
+ if (members.some(member => member.childForFieldName('object')?.text === receiverName)) {
653
+ parts.push(JSON.stringify(tokensOf(candidate)));
654
+ break;
655
+ }
656
+ }
657
+ return true;
658
+ });
659
+ }
660
+ memberBindings = JSON.stringify(parts.sort());
661
+ }
662
+ return { header: JSON.stringify(header), generator, returnForm, inferred, derivation, memberBindings };
663
+ }
664
+
665
+ /**
666
+ * Compare a callable's contract before and after a change.
667
+ *
668
+ * @returns {{kind: 'inert'|'contract'|'unknown', reasons: string[]}}
669
+ * reasons: 'header', 'callable-kind', 'inferred-return-contract',
670
+ * 'inferred-member-type', or 'contract-unreadable' for `unknown`.
671
+ */
672
+ function classifyChange(before, after, language) {
673
+ if (!before || !after) return { kind: 'unknown', reasons: ['contract-unreadable'] };
674
+ const traits = langTraits(language) || {};
675
+ const reasons = [];
676
+ if (before.header !== after.header) reasons.push('header');
677
+ if ((traits.callableKindFromBody || []).includes('generator') &&
678
+ before.generator !== after.generator) reasons.push('callable-kind');
679
+ if ((before.inferred || after.inferred) && before.derivation !== after.derivation) {
680
+ reasons.push('inferred-return-contract');
681
+ }
682
+ if ((before.memberBindings || null) !== (after.memberBindings || null)) reasons.push('inferred-member-type');
683
+ return { kind: reasons.length > 0 ? 'contract' : 'inert', reasons };
684
+ }
685
+
686
+ /** The declaration in the base revision that a changed one continues. */
687
+ function baseCounterpart(baseSymbols, symbol, isCallable) {
688
+ const identity = candidate => `${candidate.name}\0${candidate.className || ''}`;
689
+ const same = (baseSymbols || []).filter(candidate => isCallable(candidate) &&
690
+ identity(candidate) === identity(symbol));
691
+ if (same.length <= 1) return same[0] || null;
692
+ // Overloads: the one with the same written parameters, else the nearest.
693
+ const exact = same.filter(candidate => (candidate.params || '') === (symbol.params || ''));
694
+ const pool = exact.length > 0 ? exact : same;
695
+ return pool.reduce((best, candidate) =>
696
+ (Math.abs(candidate.startLine - symbol.startLine) < Math.abs(best.startLine - symbol.startLine) ? candidate : best));
697
+ }
698
+
699
+ // Top-level statements that bind or pass on names rather than run code.
700
+ const IMPORT_SHAPE = /import|export|^use_declaration$|^extern_crate|^preproc_include$|^package_clause$|^package_declaration$|^using_directive$|^mod_item$/;
701
+ const PREPROC_CONDITIONAL = /^preproc_(?:if|ifdef|elif|else)/;
702
+
703
+ /**
704
+ * The module-level statements of one revision that are not declarations:
705
+ * what binds or runs when the module loads. Declarations are compared as
706
+ * symbols; these are what a symbol diff never sees.
707
+ *
708
+ * @param {object} root - syntax tree root
709
+ * @param {object[]} symbols - the revision's indexed symbols (their ranges
710
+ * are the declarations)
711
+ * @param {object[]} [exportDetails] - the revision's export records: a
712
+ * statement on a line one of them names is an export, whatever its
713
+ * syntax (`module.exports = {...}` is an assignment)
714
+ * @param {object[]} [importDetails] - its import records, likewise
715
+ * (`const x = require('m')` is a declaration)
716
+ * @returns {{imports: string[], exports: string[], code: string[]}} one
717
+ * token signature per statement, sorted. What an export statement
718
+ * provides is compared by name (providedNames); what an import binds, by
719
+ * binding; `imports` and `code` are compared as written.
720
+ */
721
+ function moduleStatements(root, symbols, exportDetails, importDetails) {
722
+ const exportLines = new Set((exportDetails || []).map(detail => detail && detail.line).filter(Boolean));
723
+ // An import written as a call (`const x = require('m')`) is an import.
724
+ const importLines = new Set((importDetails || []).map(detail => detail && detail.line).filter(Boolean));
725
+ const importsHere = node => {
726
+ for (let line = node.startPosition.row + 1; line <= node.endPosition.row + 1; line++) {
727
+ if (importLines.has(line)) return true;
728
+ }
729
+ return false;
730
+ };
731
+ const exportsHere = node => {
732
+ for (let line = node.startPosition.row + 1; line <= node.endPosition.row + 1; line++) {
733
+ if (exportLines.has(line)) return true;
734
+ }
735
+ return false;
736
+ };
737
+ // Members count: a block that only holds members (an impl block, an
738
+ // extension) is not a symbol itself, and is still all declarations.
739
+ const declared = (symbols || []).map(symbol => [symbol.startLine, symbol.endLine || symbol.startLine]);
740
+ const overlapsDeclaration = node => {
741
+ const start = node.startPosition.row + 1;
742
+ const end = node.endPosition.row + 1;
743
+ return declared.some(([from, to]) => from <= end && to >= start);
744
+ };
745
+ const imports = [];
746
+ const exports = [];
747
+ const code = [];
748
+ const visit = container => {
749
+ for (const node of container.namedChildren) {
750
+ if (COMMENT_NODES.has(node.type)) continue;
751
+ if (PREPROC_CONDITIONAL.test(node.type)) {
752
+ // The condition decides what the build holds; the body is
753
+ // more module-level statements.
754
+ const first = node.namedChildren[0];
755
+ code.push(JSON.stringify([node.type, first ? tokensOf(first) : []]));
756
+ visit(node);
757
+ continue;
758
+ }
759
+ if (overlapsDeclaration(node)) continue;
760
+ const bucket = node.type.includes('export') || exportsHere(node) ? exports
761
+ : IMPORT_SHAPE.test(node.type) || importsHere(node) ? imports : code;
762
+ bucket.push(JSON.stringify(tokensOf(node)));
763
+ }
764
+ };
765
+ if (root) visit(root);
766
+ return { imports: imports.sort(), exports: exports.sort(), code: code.sort() };
767
+ }
768
+
769
+ /**
770
+ * Names a module lets another file import from it: what its exports name
771
+ * (the name an importer writes: the alias of a renamed export) and, where
772
+ * any module-level name can be imported (trait `moduleNamesImportable`),
773
+ * every module-level declaration and every name the module itself imports.
774
+ * `open` when the module passes on everything another module exports: the
775
+ * names it provides are not all written in it.
776
+ */
777
+ function providedNames(fileEntry, traits) {
778
+ const names = new Set();
779
+ let open = false;
780
+ for (const detail of (fileEntry && fileEntry.exportDetails) || []) {
781
+ if (!detail || !detail.name) continue;
782
+ if (detail.name === '*' || detail.type === 're-export-all') { open = true; continue; }
783
+ // The default export is imported under whatever name the importer
784
+ // picks: it is one provided thing, named `default` here.
785
+ if (detail.type === 'default') { names.add('default'); continue; }
786
+ names.add(detail.alias || detail.name);
787
+ }
788
+ if (traits && traits.moduleNamesImportable) {
789
+ for (const symbol of (fileEntry && fileEntry.symbols) || []) {
790
+ if (!symbol.className && symbol.lexicalScopeStartLine == null) names.add(symbol.name);
791
+ }
792
+ for (const binding of (fileEntry && fileEntry.importBindings) || []) {
793
+ if (binding.lexicalScopeStartLine == null) names.add(binding.alias || binding.name);
794
+ }
795
+ }
796
+ return { names, open };
797
+ }
798
+
799
+ /** Where each module-level import binding of a file comes from, by local name. */
800
+ function sourcesOf(fileEntry) {
801
+ const sources = new Map();
802
+ for (const binding of (fileEntry && fileEntry.importBindings) || []) {
803
+ if (binding.lexicalScopeStartLine != null) continue;
804
+ const local = binding.alias || binding.name;
805
+ const source = `${binding.module}\0${binding.name}\0${binding.kind || ''}`;
806
+ sources.set(local, sources.has(local) ? `${sources.get(local)}\n${source}` : source);
807
+ }
808
+ return sources;
809
+ }
810
+
811
+ /** Multiset difference: entries of `before` that `after` does not have. */
812
+ function removedFrom(before, after) {
813
+ const remaining = new Map();
814
+ for (const item of after) remaining.set(item, (remaining.get(item) || 0) + 1);
815
+ const removed = [];
816
+ for (const item of before) {
817
+ const count = remaining.get(item) || 0;
818
+ if (count > 0) remaining.set(item, count - 1);
819
+ else removed.push(item);
820
+ }
821
+ return removed;
822
+ }
823
+
824
+ /**
825
+ * What a change did to a module outside its declarations.
826
+ *
827
+ * @returns {{removedNames, removedExports, starExportRemoved, importsRemoved,
828
+ * importsAdded, addedBindings, removedBindings, reboundBindings, codeChanged}}
829
+ * removedNames: names importers could take from the module and no longer
830
+ * can; removedExports: those of them the module exported explicitly;
831
+ * removedBindings / addedBindings: local names its imports no longer /
832
+ * newly bind; removedBareImports: modules it imported without binding
833
+ * a name and no longer imports;
834
+ * importsRemoved / importsAdded: import statements that disappeared /
835
+ * appeared as written; codeChanged: loose module-level code differs.
836
+ */
837
+ function moduleChange(before, after, traits) {
838
+ const was = providedNames(before.fileEntry, traits);
839
+ const is = providedNames(after.fileEntry, traits);
840
+ const removedNames = is.open ? [] : [...was.names].filter(name => !is.names.has(name)).sort();
841
+ const statementsOf = side => moduleStatements(side.root, side.fileEntry && side.fileEntry.symbols,
842
+ side.fileEntry && side.fileEntry.exportDetails, side.fileEntry && side.fileEntry.importDetails);
843
+ const beforeStatements = statementsOf(before);
844
+ const afterStatements = statementsOf(after);
845
+ const locals = entry => new Set(((entry && entry.importBindings) || [])
846
+ .filter(binding => binding.lexicalScopeStartLine == null).map(binding => binding.alias || binding.name));
847
+ const hadLocals = locals(before.fileEntry);
848
+ const hasLocals = locals(after.fileEntry);
849
+ const explicit = entry => new Set(((entry && entry.exportDetails) || [])
850
+ .filter(detail => detail && detail.name && detail.name !== '*')
851
+ .map(detail => (detail.type === 'default' ? 'default' : detail.alias || detail.name)));
852
+ const exported = explicit(before.fileEntry);
853
+ return {
854
+ removedNames,
855
+ removedExports: removedNames.filter(name => exported.has(name)),
856
+ // The module passed on everything another module exports, and no
857
+ // longer does: which names that took away is not written here.
858
+ starExportRemoved: was.open && !is.open,
859
+ removedBindings: [...hadLocals].filter(name => !hasLocals.has(name)).sort(),
860
+ // Local names that are still imported, from somewhere else or as
861
+ // something else: every use of the name in the file now means it.
862
+ reboundBindings: [...hasLocals].filter(name => hadLocals.has(name) &&
863
+ sourcesOf(before.fileEntry).get(name) !== sourcesOf(after.fileEntry).get(name)).sort(),
864
+ // Modules the file imported or included, binding no name, and no
865
+ // longer does: they were there for what importing them does.
866
+ removedBareImports: removedFrom(
867
+ [...new Set((before.fileEntry && before.fileEntry.imports) || [])],
868
+ [...new Set((after.fileEntry && after.fileEntry.imports) || [])])
869
+ .filter(module => !((before.fileEntry && before.fileEntry.importBindings) || [])
870
+ .some(binding => binding.module === module)).sort(),
871
+ importsRemoved: removedFrom(beforeStatements.imports, afterStatements.imports).length,
872
+ importsAdded: removedFrom(afterStatements.imports, beforeStatements.imports).length,
873
+ addedBindings: [...hasLocals].filter(name => !hadLocals.has(name)).sort(),
874
+ codeChanged: removedFrom(beforeStatements.code, afterStatements.code).length > 0 ||
875
+ removedFrom(afterStatements.code, beforeStatements.code).length > 0,
876
+ };
877
+ }
878
+
879
+ function parseRevision(content, language) {
880
+ const parser = getParser(language);
881
+ if (!parser || content == null) return null;
882
+ try { return safeParse(parser, content).rootNode; } catch { return null; }
883
+ }
884
+
885
+ module.exports = {
886
+ callableContract, classifyChange, baseCounterpart, parseRevision,
887
+ declaresPlainReturn, moduleStatements, providedNames, moduleChange,
888
+ };