ucn 5.5.3 → 5.6.1

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 (77) hide show
  1. package/.claude/skills/ucn/SKILL.md +37 -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 +485 -0
  6. package/README.md +66 -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 +274 -108
  11. package/core/ast-analysis.js +2 -0
  12. package/core/bridge.js +14 -0
  13. package/core/cache.js +103 -16
  14. package/core/call-fit.js +130 -0
  15. package/core/callers.js +353 -71
  16. package/core/capture.js +363 -0
  17. package/core/change-kinds.js +1107 -0
  18. package/core/check.js +1150 -139
  19. package/core/command-contracts.js +8 -7
  20. package/core/constructed-class.js +1195 -0
  21. package/core/contract-membership.js +142 -0
  22. package/core/deadcode.js +455 -106
  23. package/core/deletion.js +421 -0
  24. package/core/discovery.js +104 -9
  25. package/core/entrypoint-toml.js +110 -0
  26. package/core/entrypoints.js +12 -6
  27. package/core/evidence-bands.js +68 -0
  28. package/core/evidence-bands.json +44 -0
  29. package/core/execute.js +172 -161
  30. package/core/hot-summary.js +241 -0
  31. package/core/index-ir.js +5 -3
  32. package/core/ir.js +6 -2
  33. package/core/member-signatures.js +789 -0
  34. package/core/outgoing-check.js +116 -0
  35. package/core/output/analysis-ext.js +20 -10
  36. package/core/output/analysis.js +98 -60
  37. package/core/output/check.js +101 -37
  38. package/core/output/doctor.js +34 -1
  39. package/core/output/find.js +2 -1
  40. package/core/output/lines.js +63 -3
  41. package/core/output/public.js +29 -11
  42. package/core/output/refactoring.js +32 -0
  43. package/core/output/reporting.js +19 -2
  44. package/core/output/search.js +14 -2
  45. package/core/output/tracing.js +36 -8
  46. package/core/output-budget.js +146 -61
  47. package/core/parallel-build.js +3 -3
  48. package/core/project.js +106 -23
  49. package/core/provenance-type.js +12 -5
  50. package/core/public-command.js +35 -9
  51. package/core/python-binding-identity.js +707 -0
  52. package/core/python-modules.js +6 -0
  53. package/core/reflection.js +248 -31
  54. package/core/registry.js +623 -72
  55. package/core/reporting.js +69 -34
  56. package/core/review-items.js +75 -0
  57. package/core/runtime-entrypoints.js +159 -0
  58. package/core/search.js +148 -164
  59. package/core/shared.js +0 -45
  60. package/core/snapshot.js +350 -0
  61. package/core/source-universe.js +707 -0
  62. package/core/test-units.js +3698 -0
  63. package/core/tracing.js +270 -658
  64. package/core/verify.js +63 -22
  65. package/languages/adapter.js +8 -3
  66. package/languages/c-family.js +12 -0
  67. package/languages/csharp.js +634 -0
  68. package/languages/go.js +120 -0
  69. package/languages/index.js +466 -4
  70. package/languages/java.js +712 -0
  71. package/languages/javascript.js +1498 -6
  72. package/languages/lexical-scope.js +68 -0
  73. package/languages/python.js +1708 -3
  74. package/languages/rust.js +209 -104
  75. package/languages/static-js.js +88 -0
  76. package/mcp/server.js +44 -110
  77. package/package.json +11 -5
@@ -0,0 +1,1107 @@
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
+ // Invocation syntax across the parsers; member references are not calls.
58
+ const OUTGOING_CALL_NODES = new Set(['call_expression', 'call', 'method_invocation',
59
+ 'invocation_expression', 'object_creation_expression', 'new_expression']);
60
+ const AWAIT_NODES = new Set(['await_expression', 'await']);
61
+
62
+ // Nodes that hold declarations rather than being one: a callable's
63
+ // declaration never grows past them.
64
+ const CONTAINER_NODES = new Set([
65
+ 'program', 'module', 'source_file', 'translation_unit', 'compilation_unit',
66
+ 'class_body', 'interface_body', 'enum_body', 'declaration_list', 'field_declaration_list',
67
+ 'block', 'statement_block', 'compound_statement', 'object', 'object_type',
68
+ 'class_declaration', 'class_definition', 'class_specifier', 'struct_specifier',
69
+ 'impl_item', 'trait_item', 'namespace_definition', 'namespace_declaration',
70
+ 'arguments', 'argument_list', 'array', 'list', 'tuple', 'dictionary',
71
+ ]);
72
+
73
+ // Written before the declaration, not inside it.
74
+ const LEADING_NODES = new Set([
75
+ 'decorator', 'attribute_item', 'annotation', 'marker_annotation', 'attribute_list',
76
+ ]);
77
+
78
+ // Nodes that bind a name, and the field that holds what they bind. Only the
79
+ // languages whose return contract is read expression by expression need a
80
+ // row; the others compare the whole body.
81
+ const BINDING_FIELDS = Object.freeze({
82
+ // Python
83
+ assignment: ['left'],
84
+ augmented_assignment: ['left'],
85
+ for_statement: ['left'],
86
+ for_in_clause: ['left'],
87
+ as_pattern: ['alias'],
88
+ named_expression: ['name'],
89
+ function_definition: ['name'],
90
+ class_definition: ['name'],
91
+ // JavaScript / TypeScript
92
+ variable_declarator: ['name'],
93
+ assignment_expression: ['left'],
94
+ augmented_assignment_expression: ['left'],
95
+ update_expression: ['argument'],
96
+ for_in_statement: ['left'],
97
+ function_declaration: ['name'],
98
+ generator_function_declaration: ['name'],
99
+ class_declaration: ['name'],
100
+ catch_clause: ['parameter'],
101
+ });
102
+ // Statements that bind every name they list.
103
+ const BINDING_LISTS = new Set([
104
+ 'global_statement', 'nonlocal_statement', 'delete_statement',
105
+ 'import_statement', 'import_from_statement', 'except_clause',
106
+ ]);
107
+ // A binding form with no row above still binds: its type says so.
108
+ const BINDING_SHAPE = /assignment|declarator|pattern|binding/;
109
+ const BINDING_SHAPE_FIELDS = ['left', 'name', 'pattern', 'declarator', 'alias', 'argument'];
110
+
111
+ function sameNode(a, b) {
112
+ return !!a && !!b && a.id === b.id;
113
+ }
114
+
115
+ /** Leaf tokens of a subtree, comments left out, `skip` not entered. */
116
+ function tokensOf(node, skip, out = []) {
117
+ if (!node || COMMENT_NODES.has(node.type) || (skip && sameNode(node, skip))) return out;
118
+ if (node.childCount === 0) {
119
+ out.push([node.type, node.text]);
120
+ return out;
121
+ }
122
+ for (const child of node.children) tokensOf(child, skip, out);
123
+ return out;
124
+ }
125
+
126
+ // A baseline invocation is reused only when the whole callable still has
127
+ // the same written context, apart from direct primitive call arguments.
128
+ // This is a preservation premise, not a proof that an arbitrary new call
129
+ // is available (access, generics, receiver modes and result use included).
130
+ const DIRECT_LITERAL_NODES = new Set(['number', 'integer', 'float', 'number_literal',
131
+ 'integer_literal', 'int_literal', 'decimal_integer_literal', 'float_literal', 'real_literal',
132
+ 'string', 'string_literal', 'true', 'false', 'boolean_literal']);
133
+ function invocationTokens(node, out = []) {
134
+ if (!node || COMMENT_NODES.has(node.type)) return out;
135
+ const parent = node.parent;
136
+ const list = parent?.type === 'argument' && parent.childCount === 1 ? parent.parent : parent;
137
+ if (DIRECT_LITERAL_NODES.has(node.type) && list &&
138
+ ['arguments', 'argument_list'].includes(list.type)) {
139
+ const kind = ['true', 'false', 'boolean_literal'].includes(node.type) ? 'boolean'
140
+ : ['string', 'string_literal'].includes(node.type) && ['"', "'"].includes(node.text[0]) ? 'string'
141
+ : /^\d+$/.test(node.text) && Number(node.text) <= 127 ? 'smallInteger' : null;
142
+ if (kind) { out.push(['argument-literal', kind]); return out; }
143
+ }
144
+ if (node.childCount === 0) { out.push([node.type, node.text]); return out; }
145
+ for (const child of node.children) invocationTokens(child, out);
146
+ return out;
147
+ }
148
+
149
+ function identifiersOf(node, out = new Set()) {
150
+ if (!node) return out;
151
+ if (node.childCount === 0) {
152
+ if (node.type.includes('identifier')) out.add(node.text);
153
+ return out;
154
+ }
155
+ for (const child of node.children) identifiersOf(child, out);
156
+ return out;
157
+ }
158
+
159
+ function within(node, range) {
160
+ const startRow = node.startPosition.row + 1;
161
+ const endRow = node.endPosition.row + 1;
162
+ if (startRow < range.startLine || endRow > range.endLine) return false;
163
+ if (range.startColumn != null && startRow === range.startLine &&
164
+ node.startPosition.column < range.startColumn) return false;
165
+ if (range.endColumn != null && endRow === range.endLine &&
166
+ node.endPosition.column > range.endColumn) return false;
167
+ return true;
168
+ }
169
+
170
+ /** The rows, and where the parser recorded them the columns, of a symbol. */
171
+ function rangeOf(symbol) {
172
+ const span = symbol.sourceSpan;
173
+ // A span is trusted only when it has all four coordinates: a decorated
174
+ // definition's span and its symbol's lines can disagree on where the
175
+ // declaration starts, and the wider of the two is the declaration.
176
+ const exact = span && [span.startLine, span.endLine, span.startColumn, span.endColumn]
177
+ .every(Number.isInteger);
178
+ return {
179
+ startLine: exact ? Math.min(symbol.startLine, span.startLine) : symbol.startLine,
180
+ endLine: exact ? Math.max(symbol.endLine || symbol.startLine, span.endLine)
181
+ : symbol.endLine || symbol.startLine,
182
+ ...(exact && span.startLine <= symbol.startLine && { startColumn: span.startColumn }),
183
+ ...(exact && span.endLine >= (symbol.endLine || symbol.startLine) && { endColumn: span.endColumn }),
184
+ };
185
+ }
186
+
187
+ /** The callable node a symbol's range holds: the outermost one in it. */
188
+ function callableNodeOf(root, range) {
189
+ let found = null;
190
+ const visit = node => {
191
+ const startRow = node.startPosition.row + 1;
192
+ const endRow = node.endPosition.row + 1;
193
+ if (endRow < range.startLine || startRow > range.endLine) return;
194
+ if (CALLABLE_NODES.has(node.type) && within(node, range)) {
195
+ if (!found || (node.endIndex - node.startIndex) > (found.endIndex - found.startIndex)) found = node;
196
+ return; // nested callables are inside this one
197
+ }
198
+ for (const child of node.namedChildren) visit(child);
199
+ };
200
+ visit(root);
201
+ return found;
202
+ }
203
+
204
+ /**
205
+ * The whole declaration a callable node belongs to: the node, grown through
206
+ * every wrapper that is part of the same declaration (a decorated or
207
+ * exported definition, a template head, the field or variable a function
208
+ * value is assigned to) and never into what merely contains it.
209
+ */
210
+ function declarationOf(node, range) {
211
+ let declaration = node;
212
+ while (declaration.parent && !CONTAINER_NODES.has(declaration.parent.type) &&
213
+ within(declaration.parent, range)) declaration = declaration.parent;
214
+ return declaration;
215
+ }
216
+
217
+ /** Own nodes of a body: nested callables are seen and not entered. */
218
+ function walkOwn(node, visit, path = []) {
219
+ for (const child of node.namedChildren) {
220
+ if (visit(child, path) === false) continue;
221
+ if (CALLABLE_NODES.has(child.type)) continue;
222
+ path.push(child.type);
223
+ walkOwn(child, visit, path);
224
+ path.pop();
225
+ }
226
+ }
227
+
228
+ function bindingTargets(node) {
229
+ const fields = BINDING_FIELDS[node.type] ||
230
+ (BINDING_SHAPE.test(node.type) ? BINDING_SHAPE_FIELDS : null);
231
+ if (!fields) return BINDING_LISTS.has(node.type) ? [node] : [];
232
+ return fields.map(field => node.childForFieldName(field)).filter(Boolean);
233
+ }
234
+
235
+ // Statement-level nodes: the units a body is made of. A compound statement
236
+ // is the unit of its own header (a condition, a loop target, an `except`
237
+ // pattern); the statements it holds are units of their own.
238
+ const STATEMENT_SHAPE = /(?:_statement|_declaration|_definition|_clause|^switch_case|^switch_default)$/;
239
+ const VARIABLE_LEAVES = new Set([
240
+ 'identifier', 'shorthand_property_identifier', 'shorthand_property_identifier_pattern',
241
+ ]);
242
+
243
+ /** A member or keyword name: spelled like a variable, naming none. */
244
+ function namesNoVariable(node) {
245
+ const parent = node.parent;
246
+ if (!parent) return false;
247
+ const member = parent.childForFieldName('attribute') || parent.childForFieldName('property') ||
248
+ (parent.type === 'keyword_argument' ? parent.childForFieldName('name') : null);
249
+ return !!member && member.id === node.id;
250
+ }
251
+
252
+ // A member access, and the field that holds the member's name.
253
+ const MEMBER_NODES = new Map([['member_expression', 'property'], ['attribute', 'attribute']]);
254
+
255
+ /** `this.a.b` as ['this', 'a', 'b'], or null when it is not a plain chain. */
256
+ function chainOf(node) {
257
+ if (node.type === 'this' || VARIABLE_LEAVES.has(node.type)) return [node.text];
258
+ const field = MEMBER_NODES.get(node.type);
259
+ if (!field) return null;
260
+ const object = node.childForFieldName('object');
261
+ const member = node.childForFieldName(field);
262
+ if (!object || !member || member.childCount > 0) return null;
263
+ const head = chainOf(object);
264
+ return head ? [...head, member.text] : null;
265
+ }
266
+
267
+ /** Do two references name the same thing, or one a part of the other? */
268
+ function overlaps(a, b) {
269
+ return a === b || a.startsWith(`${b}.`) || b.startsWith(`${a}.`);
270
+ }
271
+
272
+ /**
273
+ * A unit's own tokens and what it refers to: everything in the node except
274
+ * the statements it holds.
275
+ *
276
+ * A reference is a variable name, with one refinement. A member of the
277
+ * receiver (`this.count`, `self.count`) is its own reference: a type
278
+ * checker narrows and assigns `this.count` apart from `this.total`, and
279
+ * the receiver is no union a test of one member could narrow as a whole.
280
+ * Members of anything else stay under the variable (a test of `shape.kind`
281
+ * narrows `shape`, and with it `shape.radius`). A call on the receiver
282
+ * refers to the receiver itself: what it asserts about `this` is in its
283
+ * declaration.
284
+ *
285
+ * @param {object} [options] - { receiverMembers: the callable is a class
286
+ * member whose receiver is an instance of the class, selfName: the
287
+ * receiver parameter's name where the receiver is one, omit: a node
288
+ * whose references are not collected (the callee of a call shown to do
289
+ * nothing but return) }
290
+ */
291
+ function unitOf(node, options = {}) {
292
+ const { receiverMembers = false, selfName = null, omit = null } = options;
293
+ const tokens = [];
294
+ const names = new Set();
295
+ let yields = false;
296
+ const isReceiver = text => receiverMembers && (text === 'this' || (selfName !== null && text === selfName));
297
+ const visit = (candidate, top, quiet) => {
298
+ if (COMMENT_NODES.has(candidate.type)) return;
299
+ if (!top && candidate.isNamed && STATEMENT_SHAPE.test(candidate.type)) return;
300
+ if (YIELD_NODES.has(candidate.type)) yields = true;
301
+ if (!quiet && omit && sameNode(candidate, omit)) quiet = true;
302
+ if (!quiet && MEMBER_NODES.has(candidate.type)) {
303
+ const chain = chainOf(candidate);
304
+ if (chain && isReceiver(chain[0])) {
305
+ const parent = candidate.parent;
306
+ const called = !!parent && CALL_NODES.has(parent.type) &&
307
+ sameNode(parent.childForFieldName('function'), candidate);
308
+ const reach = called ? chain.slice(0, -1) : chain;
309
+ names.add(reach.slice(0, 2).join('.'));
310
+ quiet = true;
311
+ }
312
+ }
313
+ if (candidate.childCount === 0) {
314
+ tokens.push([candidate.type, candidate.text]);
315
+ if (!quiet && (candidate.type === 'this' ||
316
+ (VARIABLE_LEAVES.has(candidate.type) && !namesNoVariable(candidate)))) names.add(candidate.text);
317
+ return;
318
+ }
319
+ for (const child of candidate.children) visit(child, false, quiet);
320
+ };
321
+ visit(node, true, false);
322
+ return { tokens, names, yields };
323
+ }
324
+
325
+ /** The callee of a statement that is nothing but a call, or null. */
326
+ function bareCallee(statement) {
327
+ if (statement.type !== 'expression_statement' || statement.namedChildCount !== 1) return null;
328
+ let expression = statement.namedChildren[0];
329
+ if (AWAIT_NODES.has(expression.type)) expression = expression.namedChildren[0];
330
+ if (!expression || !CALL_NODES.has(expression.type)) return null;
331
+ return expression.childForFieldName('function');
332
+ }
333
+
334
+ /**
335
+ * Can a statement that only calls `callee` be shown to return to the next
336
+ * statement? A type checker ends the path at a call it is TOLD never
337
+ * returns (a declared `never` / `NoReturn`), so the question is what the
338
+ * callee is declared as:
339
+ * - a name the callable binds itself (a parameter, a local): unknown;
340
+ * - a function or an own method of the project whose every declaration
341
+ * says nothing of the kind (`facts.context.returning`);
342
+ * - a builtin of the language that returns (traits `ambientCallables`
343
+ * minus `ambientNeverCallables`; members of
344
+ * `ambientReturningNamespaces`), unless the file gives the name
345
+ * another meaning;
346
+ * - anything else (a member of a value, an import from outside the
347
+ * project): unknown, which is no.
348
+ */
349
+ function provablyReturns(callee, facts) {
350
+ const { traits, context, locals, ownerClass, selfName } = facts;
351
+ const shadowed = name => locals.has(name) || !!(context && context.fileNames && context.fileNames.has(name));
352
+ if (callee.type === 'identifier') {
353
+ const name = callee.text;
354
+ if (locals.has(name)) return false;
355
+ if (context && context.returning && context.returning(name, null)) return true;
356
+ return !!traits.ambientCallables && traits.ambientCallables.has(name) && !shadowed(name) &&
357
+ !(traits.ambientNeverCallables && traits.ambientNeverCallables.has(name));
358
+ }
359
+ const object = callee.childForFieldName('object');
360
+ const member = callee.childForFieldName('property') || callee.childForFieldName('attribute');
361
+ if (!object || !member) return false;
362
+ if (object.type === 'this' || (selfName && object.type === 'identifier' && object.text === selfName)) {
363
+ return !!(ownerClass && context && context.returning && context.returning(member.text, ownerClass));
364
+ }
365
+ let root = object;
366
+ while (root.childForFieldName('object')) root = root.childForFieldName('object');
367
+ return root.type === 'identifier' && !!traits.ambientReturningNamespaces &&
368
+ traits.ambientReturningNamespaces.has(root.text) && !shadowed(root.text);
369
+ }
370
+
371
+ /**
372
+ * Does this declaration say nothing that could make a call of it end the
373
+ * caller's path? Yes for a function or method with a body and either no
374
+ * return annotation or one made of keywords only (`void`, `None`) that is
375
+ * not a never type. No (unknown) for everything a checker could read a
376
+ * never type from: an annotation that names a type (it may be an alias of
377
+ * one), a decorator (it can replace the callable), a function held by a
378
+ * typed variable or field (it has that type), a declaration with no body
379
+ * (an overload signature).
380
+ *
381
+ * @param {string[]} neverTypes - trait `neverReturnTypes`
382
+ */
383
+ function declaresPlainReturn(root, symbol, neverTypes) {
384
+ if (!root || !symbol) return false;
385
+ const range = rangeOf(symbol);
386
+ const node = callableNodeOf(root, range);
387
+ if (!node || node.hasError || !node.childForFieldName('body')) return false;
388
+ // Everything the callable is written inside, up to the block or body
389
+ // that holds its declaration (a symbol's range can be the function
390
+ // value alone, without the variable it is assigned to).
391
+ for (let up = node; up && !CONTAINER_NODES.has(up.type); up = up.parent) {
392
+ if (up.type === 'decorated_definition' || up.namedChildren.some(child => child.type === 'decorator')) return false;
393
+ if (up.previousNamedSibling && LEADING_NODES.has(up.previousNamedSibling.type)) return false;
394
+ if (!sameNode(up, node) && up.childForFieldName('type')) return false;
395
+ }
396
+ const written = node.childForFieldName('return_type');
397
+ if (!written) return true;
398
+ return !tokensOf(written).some(([type, text]) =>
399
+ /identifier|string/.test(type) || (neverTypes || []).includes(text));
400
+ }
401
+
402
+ /**
403
+ * The return derivation of a body: what the type of its result can depend
404
+ * on, read conservatively from syntax.
405
+ *
406
+ * The result expressions come first, each with the control nesting it sits
407
+ * under (wrapping a `return` in an `if` lets the callable fall off its
408
+ * end). Then every statement connected to them through a variable name, in
409
+ * any position and transitively: a binding of a returned name, a call that
410
+ * mutates the object it names (`xs.push("text")` before `return xs`), a
411
+ * condition that narrows it (`typeof x === 'number'` before `return x`), a
412
+ * closure that assigns it, and whatever those statements name in turn;
413
+ * and every statement that sits under a condition of the slice. A type
414
+ * checker follows fewer of these; which ones is its business, and every
415
+ * one it does follow is here.
416
+ *
417
+ * Last, the statements that END a path, whatever they name: the result is
418
+ * the union of the ends that can be reached, so removing the `throw` at
419
+ * the end of a function gives it one more (falling off the end). These are
420
+ * `throw` / `raise`, `break` / `continue`, an assertion, a context
421
+ * manager, a condition that names nothing (`while True`, `if False`), and
422
+ * a statement that only calls something not shown to return
423
+ * (`provablyReturns`). Each is kept by kind and nesting, in source order
424
+ * with the slice: what is thrown, or what a call is passed, changes no
425
+ * type.
426
+ *
427
+ * Where a checker decides conditions from the names in them (trait
428
+ * `namedConditionsDecidable`: an interpreter version or platform test,
429
+ * `TYPE_CHECKING`, a constant its configuration defines), a condition is
430
+ * not opaque: the branch it rules out contributes nothing. There, every
431
+ * statement that holds a statement of the derivation is part of it, and so
432
+ * is every loop condition.
433
+ *
434
+ * A statement that shares no name with the slice and ends no path cannot
435
+ * reach the result.
436
+ *
437
+ * @param {object} [facts] - what `provablyReturns` asks; without it every
438
+ * bare call is one that may not return
439
+ */
440
+ function returnDerivation(body, facts = null) {
441
+ const units = [];
442
+ const decidable = !!(facts && facts.traits && facts.traits.namedConditionsDecidable);
443
+ const receiver = { receiverMembers: !!(facts && facts.receiverMembers), selfName: facts ? facts.selfName : null };
444
+ // An expression body is the result.
445
+ if (!body.namedChildren.some(child => STATEMENT_SHAPE.test(child.type)) &&
446
+ !['block', 'statement_block', 'compound_statement'].includes(body.type)) {
447
+ units.push({ at: body.startIndex, result: true, path: [], ...unitOf(body, receiver) });
448
+ }
449
+ const collect = (container, path, nested, holder) => {
450
+ for (const child of container.namedChildren) {
451
+ if (COMMENT_NODES.has(child.type)) continue;
452
+ const inner = nested || CALLABLE_NODES.has(child.type);
453
+ if (STATEMENT_SHAPE.test(child.type)) {
454
+ // A statement that only calls something shown to return:
455
+ // the call itself narrows nothing and ends nothing.
456
+ const callee = nested ? null : bareCallee(child);
457
+ const plainCall = !!(callee && facts && provablyReturns(callee, facts));
458
+ const unit = {
459
+ at: child.startIndex,
460
+ type: child.type,
461
+ path,
462
+ // The compound statement this one sits under.
463
+ holder,
464
+ ...unitOf(child, { ...receiver, omit: plainCall ? callee : null }),
465
+ };
466
+ // A `return` inside a closure is the closure's result, and
467
+ // what ends the closure's path ends only that.
468
+ unit.result = !nested && (RESULT_NODES.has(child.type) || unit.yields);
469
+ if (!nested && !unit.result) {
470
+ if (EXIT_NODES.has(child.type)) unit.exit = [EXIT_NODES.get(child.type)];
471
+ else if (ASSERT_NODES.has(child.type) || MANAGER_NODES.has(child.type)) unit.exit = [child.type, unit.tokens];
472
+ else if (CONDITION_NODES.has(child.type) && (unit.names.size === 0 ||
473
+ (decidable && LOOP_NODES.has(child.type) && child.childForFieldName('condition')))) {
474
+ unit.exit = ['static', unit.tokens];
475
+ }
476
+ else if (callee && !plainCall) unit.exit = ['call', callee.text];
477
+ }
478
+ units.push(unit);
479
+ collect(child, [...path, child.type], inner, unit);
480
+ } else {
481
+ collect(child, path, inner, holder);
482
+ }
483
+ }
484
+ };
485
+ collect(body, [], false, null);
486
+ const slice = new Set(units.filter(unit => unit.result));
487
+ const names = new Set();
488
+ for (const unit of slice) for (const name of unit.names) names.add(name);
489
+ const shares = unit => {
490
+ for (const mine of unit.names) for (const name of names) if (overlaps(mine, name)) return true;
491
+ return false;
492
+ };
493
+ // Data dependence: a shared variable name. Control dependence: a
494
+ // statement under a condition that is in the slice decides what that
495
+ // condition lets through (`if x is None: fail()` narrows `x` after it
496
+ // only while `fail()` never returns).
497
+ const held = unit => { for (let up = unit.holder; up; up = up.holder) if (slice.has(up)) return true; return false; };
498
+ for (let grew = slice.size > 0; grew;) {
499
+ grew = false;
500
+ for (const unit of units) {
501
+ if (slice.has(unit)) continue;
502
+ if (!held(unit) && !shares(unit)) continue;
503
+ slice.add(unit);
504
+ for (const name of unit.names) names.add(name);
505
+ grew = true;
506
+ }
507
+ }
508
+ const kept = new Set(units.filter(unit => slice.has(unit) || unit.exit));
509
+ // A `switch` that covers every value of its subject leaves no way past
510
+ // it, and a label narrows the subject inside its case: the subject and
511
+ // the labels of a switch that holds part of the derivation are part of
512
+ // it. Leaving the switch (`break`) is not such a part.
513
+ const switches = new Set();
514
+ for (const unit of kept) {
515
+ if (!slice.has(unit) && unit.exit && (unit.exit[0] === 'break' || unit.exit[0] === 'continue')) continue;
516
+ for (let up = unit.holder; up; up = up.holder) if (SWITCH_NODES.has(up.type)) switches.add(up);
517
+ }
518
+ for (const unit of units) {
519
+ if (switches.has(unit) || (CASE_NODES.has(unit.type) && switches.has(unit.holder))) kept.add(unit);
520
+ }
521
+ // Where a checker can decide a condition from the names in it, every
522
+ // statement that holds one of these decides whether it counts.
523
+ if (decidable) {
524
+ for (const unit of [...kept]) {
525
+ for (let up = unit.holder; up && !kept.has(up); up = up.holder) kept.add(up);
526
+ }
527
+ }
528
+ return JSON.stringify([...kept].sort((a, b) => a.at - b.at).map(unit => {
529
+ if (slice.has(unit)) return [unit.path, unit.tokens];
530
+ return unit.exit ? ['exit', unit.path, ...unit.exit] : ['guard', unit.path, unit.tokens];
531
+ }));
532
+ }
533
+
534
+ /**
535
+ * How the declaration states its return contract: 'declared' (a written
536
+ * type that is the contract), 'unannotated', 'placeholder' (a written type
537
+ * the compiler replaces with the body's), 'opaque' (a written type that
538
+ * still exposes the concrete value's properties).
539
+ */
540
+ function returnFormOf(node, declaration, symbol, traits) {
541
+ // A callable instantiated per use (a template, a member of a class
542
+ // template) is type-checked against each caller's arguments: its body
543
+ // is part of what a caller relies on.
544
+ if (symbol && (symbol.templateDependent || symbol.templateParams) &&
545
+ (traits.inferredReturnForms || []).includes('instantiated')) return 'instantiated';
546
+ // An async function returns an anonymous future built from its body,
547
+ // and where that future's properties leak to callers (auto traits),
548
+ // the body is part of the contract.
549
+ if (traits.asyncReturnIsOpaque && tokensOf(declaration, node.childForFieldName('body'))
550
+ .some(([type, text]) => type === 'async' || text === 'async')) return 'opaque';
551
+ // A callable the compiler may run (a constant-evaluated function) hands
552
+ // callers its value, not only its type: its body is the contract. The
553
+ // marker is one of the modifiers written before the name.
554
+ const markers = traits.compileTimeCallableMarkers;
555
+ if (markers && markers.length > 0) {
556
+ const name = node.childForFieldName('name') || node.childForFieldName('declarator');
557
+ const limit = name ? name.startIndex : node.startIndex;
558
+ const modifiers = [];
559
+ const collect = candidate => {
560
+ if (candidate.startIndex >= limit) return;
561
+ if (candidate.childCount === 0) { modifiers.push(candidate.text); return; }
562
+ for (const child of candidate.children) collect(child);
563
+ };
564
+ collect(declaration);
565
+ if (modifiers.some(text => markers.includes(text))) return 'evaluated';
566
+ }
567
+ const written = node.childForFieldName('return_type') || node.childForFieldName('result') ||
568
+ node.childForFieldName('type');
569
+ if (!written) return 'unannotated';
570
+ const text = written.text;
571
+ if (/\b(?:auto|decltype\s*\(\s*auto\s*\))(?![\w:])/.test(text) &&
572
+ !node.descendantsOfType('trailing_return_type').length) return 'placeholder';
573
+ if (/(?:^|[\s(<&])impl\s/.test(text)) return 'opaque';
574
+ return 'declared';
575
+ }
576
+
577
+ /**
578
+ * The contract of one callable in one revision of its file.
579
+ *
580
+ * @param {object} root - syntax tree root of that revision
581
+ * @param {string} language
582
+ * @param {object} symbol - { startLine, endLine, sourceSpan?, className? } in that revision
583
+ * @param {object} [context] - what the revision's file and the project say
584
+ * about callees: { fileNames: Set of names the file declares or imports,
585
+ * returning(name, ownerClass): every project declaration of the name
586
+ * (of the class's own method) is one a call of which returns }
587
+ * @returns {{header, generator, returnForm, inferred, derivation}|null}
588
+ */
589
+ function callableContract(root, language, symbol, context = null) {
590
+ if (!root || !symbol) return null;
591
+ const range = rangeOf(symbol);
592
+ const node = callableNodeOf(root, range);
593
+ if (!node) {
594
+ // A declaration with no body (a prototype, an interface method, an
595
+ // overload signature) is all header: every token in its range.
596
+ const header = [];
597
+ const collect = candidate => {
598
+ const startRow = candidate.startPosition.row + 1;
599
+ const endRow = candidate.endPosition.row + 1;
600
+ if (endRow < range.startLine || startRow > range.endLine) return;
601
+ if (candidate.childCount === 0) {
602
+ if (within(candidate, range) && !COMMENT_NODES.has(candidate.type)) header.push([candidate.type, candidate.text]);
603
+ return;
604
+ }
605
+ for (const child of candidate.children) collect(child);
606
+ };
607
+ collect(root);
608
+ if (header.length === 0) return null;
609
+ return {
610
+ header: JSON.stringify(header), generator: false, returnForm: 'declared', inferred: false,
611
+ derivation: null, memberBindings: null,
612
+ };
613
+ }
614
+ if (node.hasError) return null;
615
+ const body = node.childForFieldName('body');
616
+ const declaration = declarationOf(node, range);
617
+ const header = [];
618
+ // Decorators and attributes written before the declaration.
619
+ const leading = [];
620
+ for (let sibling = declaration.previousNamedSibling; sibling && LEADING_NODES.has(sibling.type);
621
+ sibling = sibling.previousNamedSibling) leading.unshift(sibling);
622
+ for (const item of leading) tokensOf(item, null, header);
623
+ tokensOf(declaration, body, header);
624
+
625
+ const traits = langTraits(language) || {};
626
+ const returnForm = returnFormOf(node, declaration, symbol, traits);
627
+ const inferred = (traits.inferredReturnForms || []).includes(returnForm);
628
+ let generator = false;
629
+ if (body) {
630
+ walkOwn(body, candidate => {
631
+ if (YIELD_NODES.has(candidate.type)) generator = true;
632
+ return true;
633
+ });
634
+ }
635
+ let derivation = null;
636
+ if (inferred && body) {
637
+ // Expression by expression where the form is "no annotation"; the
638
+ // whole body where the compiler deduces the type from all of it.
639
+ if (returnForm === 'unannotated') {
640
+ // Names the callable binds itself: its parameters and whatever
641
+ // its own statements assign.
642
+ const locals = new Set();
643
+ const parameters = node.childForFieldName('parameters') || node.childForFieldName('parameter');
644
+ if (parameters) identifiersOf(parameters, locals);
645
+ walkOwn(body, candidate => {
646
+ for (const target of bindingTargets(candidate)) identifiersOf(target, locals);
647
+ return true;
648
+ });
649
+ const leading = parameters ? parameters.namedChildren[0] : null;
650
+ const first = leading ? [...identifiersOf(leading)][0] : null;
651
+ const selfName = symbol.className && first && (traits.selfParam || []).includes(first) ? first : null;
652
+ // The receiver of a class member is an instance of the class.
653
+ // One whose type is WRITTEN (`this: A | B`, `self: Shape`) is
654
+ // whatever that says, a union included: a variable like any.
655
+ const typedReceiver = !!leading && (leading.descendantsOfType('this').length > 0 ||
656
+ (selfName !== null && leading.type !== 'identifier'));
657
+ derivation = returnDerivation(body, {
658
+ traits, context, locals, ownerClass: symbol.className || null, selfName,
659
+ receiverMembers: !!symbol.className && !typedReceiver,
660
+ });
661
+ } else {
662
+ derivation = JSON.stringify(tokensOf(body));
663
+ }
664
+ }
665
+ // Where a member's type is whatever the class's methods assign to it
666
+ // (trait `memberTypesFromAssignments`), every assignment to a member of
667
+ // the receiver is part of the class's contract, in whichever method it
668
+ // is written.
669
+ let memberBindings = null;
670
+ if (traits.memberTypesFromAssignments && body) {
671
+ const receiver = node.childForFieldName('parameters')?.namedChildren[0];
672
+ const receiverName = receiver ? [...identifiersOf(receiver)][0] : null;
673
+ const parts = [];
674
+ if (receiverName) {
675
+ walkOwn(body, candidate => {
676
+ for (const target of bindingTargets(candidate)) {
677
+ const members = target.type === 'attribute' ? [target] : target.descendantsOfType('attribute');
678
+ if (members.some(member => member.childForFieldName('object')?.text === receiverName)) {
679
+ parts.push(JSON.stringify(tokensOf(candidate)));
680
+ break;
681
+ }
682
+ }
683
+ return true;
684
+ });
685
+ }
686
+ memberBindings = JSON.stringify(parts.sort());
687
+ }
688
+ const outgoingCalls = [];
689
+ const contextStatements = [];
690
+ // Native comment queries preserve extra-node identity. The binding's
691
+ // ordinary children wrappers can expose a Python inline-suite comment
692
+ // as a zero-child block, so type-testing that walk loses directives.
693
+ const bodyComments = body ? body.descendantsOfType([...COMMENT_NODES]) : [];
694
+ const commentSpans = new Set(bodyComments.map(comment => `${comment.startIndex}:${comment.endIndex}`));
695
+ const comments = bodyComments.filter(comment => traits.checkDirectiveComment?.(comment.text));
696
+ const visit = candidate => {
697
+ if (OUTGOING_CALL_NODES.has(candidate.type)) outgoingCalls.push({ node: candidate,
698
+ start: candidate.startIndex, end: candidate.endIndex });
699
+ if (!sameNode(candidate, body) && (bindingTargets(candidate).length || STATEMENT_SHAPE.test(candidate.type))) {
700
+ contextStatements.push(candidate);
701
+ }
702
+ return true;
703
+ };
704
+ if (body) { visit(body); walkOwn(body, visit); }
705
+ const parameters = node.childForFieldName('parameters') || node.childForFieldName('parameter');
706
+ // A discard occupies syntax but introduces no binding. Its spelling
707
+ // must not connect otherwise independent statements (Go `_ = 0`).
708
+ // Keep all invocation/RHS tokens: discarding a result does not erase
709
+ // the call that produces it.
710
+ const dependencyNames = candidate => [...identifiersOf(candidate)]
711
+ .filter(name => !(traits.discardIdentifierNames || []).includes(name));
712
+ const contextFacts = contextStatements.map(statement => ({
713
+ names: dependencyNames(statement), tokens: tokensOf(statement),
714
+ }));
715
+ const statementsByName = new Map();
716
+ contextFacts.forEach((fact, position) => {
717
+ for (const name of fact.names) {
718
+ if (!statementsByName.has(name)) statementsByName.set(name, []);
719
+ statementsByName.get(name).push(position);
720
+ }
721
+ });
722
+ // Checker comment position is measured against code tokens, so blank
723
+ // lines and ordinary comments do not change the call's applicability.
724
+ const sourceTokens = [];
725
+ const collectTokens = candidate => {
726
+ if (!candidate || COMMENT_NODES.has(candidate.type) ||
727
+ commentSpans.has(`${candidate.startIndex}:${candidate.endIndex}`)) return;
728
+ if (candidate.childCount === 0) { sourceTokens.push(candidate); return; }
729
+ for (const child of candidate.children) collectTokens(child);
730
+ };
731
+ if (comments.length) collectTokens(body);
732
+ const tokenPosition = offset => {
733
+ let low = 0;
734
+ let high = sourceTokens.length;
735
+ while (low < high) {
736
+ const mid = (low + high) >>> 1;
737
+ if (sourceTokens[mid].startIndex < offset) low = mid + 1;
738
+ else high = mid;
739
+ }
740
+ return low;
741
+ };
742
+ const commentFacts = comments.map(comment => {
743
+ const at = tokenPosition(comment.startIndex);
744
+ return [comment.text, at,
745
+ sourceTokens[at - 1]?.endPosition.row === comment.startPosition.row,
746
+ sourceTokens[at]?.startPosition.row === comment.endPosition.row];
747
+ });
748
+ const invocationContext = JSON.stringify([invocationTokens(declaration), commentFacts]);
749
+ for (const call of outgoingCalls) {
750
+ // A call's own tokens do not establish its meaning. Follow the
751
+ // written bindings of its callee, receiver and arguments (including
752
+ // aliases), and retain the checker comments around it.
753
+ const names = dependencyNames(call.node);
754
+ const seenNames = new Set(names);
755
+ const selected = new Set();
756
+ for (let at = 0; at < names.length; at++) {
757
+ for (const position of statementsByName.get(names[at]) || []) {
758
+ if (selected.has(position)) continue;
759
+ selected.add(position);
760
+ for (const name of contextFacts[position].names) {
761
+ if (!seenNames.has(name)) { seenNames.add(name); names.push(name); }
762
+ }
763
+ }
764
+ }
765
+ const nesting = [];
766
+ for (let parent = call.node.parent; parent && !sameNode(parent, body); parent = parent.parent) {
767
+ nesting.push(parent.type);
768
+ }
769
+ call.invocationKey = JSON.stringify(invocationTokens(call.node));
770
+ const relevantParameters = (parameters?.namedChildren || []).filter(parameter =>
771
+ dependencyNames(parameter).some(name => seenNames.has(name)));
772
+ const returnsValue = sameNode(call.node, body) || nesting.some(type => RESULT_NODES.has(type));
773
+ call.key = JSON.stringify([tokensOf(call.node), relevantParameters.map(parameter => tokensOf(parameter)),
774
+ returnsValue ? symbol.returnType || null : null, nesting,
775
+ [...selected].sort((a, b) => a - b).map(position => contextFacts[position].tokens),
776
+ commentFacts.map(([text, at, prior, next]) => [text, at - tokenPosition(call.node.startIndex), prior, next])]);
777
+ delete call.node;
778
+ }
779
+ const contract = { header: JSON.stringify(header), generator, returnForm, inferred, derivation, memberBindings };
780
+ Object.defineProperties(contract, {
781
+ outgoingCalls: { value: outgoingCalls }, invocationContext: { value: invocationContext },
782
+ });
783
+ return contract;
784
+ }
785
+
786
+ /**
787
+ * Compare a callable's contract before and after a change.
788
+ *
789
+ * @returns {{kind: 'inert'|'contract'|'unknown', reasons: string[]}}
790
+ * reasons: 'header', 'callable-kind', 'inferred-return-contract',
791
+ * 'inferred-member-type', or 'contract-unreadable' for `unknown`.
792
+ */
793
+ function classifyChange(before, after, language) {
794
+ if (!before || !after) return { kind: 'unknown', reasons: ['contract-unreadable'] };
795
+ const traits = langTraits(language) || {};
796
+ const reasons = [];
797
+ if (before.header !== after.header) reasons.push('header');
798
+ if ((traits.callableKindFromBody || []).includes('generator') &&
799
+ before.generator !== after.generator) reasons.push('callable-kind');
800
+ if ((before.inferred || after.inferred) && before.derivation !== after.derivation) {
801
+ reasons.push('inferred-return-contract');
802
+ }
803
+ if ((before.memberBindings || null) !== (after.memberBindings || null)) reasons.push('inferred-member-type');
804
+ const change = { kind: reasons.length > 0 ? 'contract' : 'inert', reasons };
805
+ // Moving or formatting a call does not make it a new call. Repeated
806
+ // calls are counted, so an extra occurrence is still checked.
807
+ const prior = new Map();
808
+ for (const call of before.outgoingCalls || []) prior.set(call.key, (prior.get(call.key) || 0) + 1);
809
+ const preserved = new Set((before.outgoingCalls || []).map(call => call.invocationKey));
810
+ const changedCalls = (after.outgoingCalls || []).filter(call => {
811
+ const left = prior.get(call.key) || 0;
812
+ if (left) { prior.set(call.key, left - 1); return false; }
813
+ call.stableInvocation = before.invocationContext === after.invocationContext && preserved.has(call.invocationKey);
814
+ return true;
815
+ });
816
+ Object.defineProperty(change, 'changedCalls', { value: changedCalls });
817
+ return change;
818
+ }
819
+
820
+ /** The declaration in the base revision that a changed one continues. */
821
+ function baseCounterpart(baseSymbols, symbol, isCallable) {
822
+ const identity = candidate => `${candidate.name}\0${candidate.className || ''}`;
823
+ const same = (baseSymbols || []).filter(candidate => isCallable(candidate) &&
824
+ identity(candidate) === identity(symbol));
825
+ if (same.length <= 1) return same[0] || null;
826
+ // Overloads: the one with the same written parameters, else the nearest.
827
+ const exact = same.filter(candidate => (candidate.params || '') === (symbol.params || ''));
828
+ const pool = exact.length > 0 ? exact : same;
829
+ return pool.reduce((best, candidate) =>
830
+ (Math.abs(candidate.startLine - symbol.startLine) < Math.abs(best.startLine - symbol.startLine) ? candidate : best));
831
+ }
832
+
833
+ // Top-level statements that bind or pass on names rather than run code.
834
+ const IMPORT_SHAPE = /import|export|^use_declaration$|^extern_crate|^preproc_include$|^package_clause$|^package_declaration$|^using_directive$|^mod_item$/;
835
+ const PREPROC_CONDITIONAL = /^preproc_(?:if|ifdef|elif|else)/;
836
+
837
+ /**
838
+ * The module-level statements of one revision that are not declarations:
839
+ * what binds or runs when the module loads. Declarations are compared as
840
+ * symbols; these are what a symbol diff never sees.
841
+ *
842
+ * @param {object} root - syntax tree root
843
+ * @param {object[]} symbols - the revision's indexed symbols (their ranges
844
+ * are the declarations)
845
+ * @param {object[]} [exportDetails] - the revision's export records: a
846
+ * statement on a line one of them names is an export, whatever its
847
+ * syntax (`module.exports = {...}` is an assignment)
848
+ * @param {object[]} [importDetails] - its import records, likewise
849
+ * (`const x = require('m')` is a declaration)
850
+ * @returns {{imports: string[], exports: string[], code: string[]}} one
851
+ * token signature per statement, sorted. What an export statement
852
+ * provides is compared by name (providedNames); what an import binds, by
853
+ * binding; `imports` and `code` are compared as written.
854
+ */
855
+ function moduleStatements(root, symbols, exportDetails, importDetails) {
856
+ const exportLines = new Set((exportDetails || []).map(detail => detail && detail.line).filter(Boolean));
857
+ // An import written as a call (`const x = require('m')`) is an import.
858
+ const importLines = new Set((importDetails || []).map(detail => detail && detail.line).filter(Boolean));
859
+ const importsHere = node => {
860
+ for (let line = node.startPosition.row + 1; line <= node.endPosition.row + 1; line++) {
861
+ if (importLines.has(line)) return true;
862
+ }
863
+ return false;
864
+ };
865
+ const exportsHere = node => {
866
+ for (let line = node.startPosition.row + 1; line <= node.endPosition.row + 1; line++) {
867
+ if (exportLines.has(line)) return true;
868
+ }
869
+ return false;
870
+ };
871
+ // Members count: a block that only holds members (an impl block, an
872
+ // extension) is not a symbol itself, and is still all declarations.
873
+ const declared = (symbols || []).map(symbol => [symbol.startLine, symbol.endLine || symbol.startLine]);
874
+ const overlapsDeclaration = node => {
875
+ const start = node.startPosition.row + 1;
876
+ const end = node.endPosition.row + 1;
877
+ return declared.some(([from, to]) => from <= end && to >= start);
878
+ };
879
+ const imports = [];
880
+ const exports = [];
881
+ const code = [];
882
+ const visit = container => {
883
+ for (const node of container.namedChildren) {
884
+ if (COMMENT_NODES.has(node.type)) continue;
885
+ if (PREPROC_CONDITIONAL.test(node.type)) {
886
+ // The condition decides what the build holds; the body is
887
+ // more module-level statements.
888
+ const first = node.namedChildren[0];
889
+ code.push(JSON.stringify([node.type, first ? tokensOf(first) : []]));
890
+ visit(node);
891
+ continue;
892
+ }
893
+ if (overlapsDeclaration(node)) continue;
894
+ const bucket = node.type.includes('export') || exportsHere(node) ? exports
895
+ : IMPORT_SHAPE.test(node.type) || importsHere(node) ? imports : code;
896
+ bucket.push(JSON.stringify(tokensOf(node)));
897
+ }
898
+ };
899
+ if (root) visit(root);
900
+ return { imports: imports.sort(), exports: exports.sort(), code: code.sort() };
901
+ }
902
+
903
+ /**
904
+ * Names a module lets another file import from it: what its exports name
905
+ * (the name an importer writes: the alias of a renamed export) and, where
906
+ * any module-level name can be imported (trait `moduleNamesImportable`),
907
+ * every module-level declaration and every name the module itself imports.
908
+ * `open` when the module passes on everything another module exports: the
909
+ * names it provides are not all written in it.
910
+ */
911
+ function providedNames(fileEntry, traits) {
912
+ const names = new Set();
913
+ let open = false;
914
+ for (const detail of (fileEntry && fileEntry.exportDetails) || []) {
915
+ if (!detail || !detail.name) continue;
916
+ if (detail.name === '*' || detail.type === 're-export-all') { open = true; continue; }
917
+ // The default export is imported under whatever name the importer
918
+ // picks: it is one provided thing, named `default` here.
919
+ if (detail.type === 'default') { names.add('default'); continue; }
920
+ names.add(detail.alias || detail.name);
921
+ }
922
+ if (traits && traits.moduleNamesImportable) {
923
+ for (const symbol of (fileEntry && fileEntry.symbols) || []) {
924
+ if (!symbol.className && symbol.lexicalScopeStartLine == null) names.add(symbol.name);
925
+ }
926
+ for (const binding of (fileEntry && fileEntry.importBindings) || []) {
927
+ if (binding.lexicalScopeStartLine == null) names.add(binding.alias || binding.name);
928
+ }
929
+ }
930
+ return { names, open };
931
+ }
932
+
933
+ /** Where each module-level import binding of a file comes from, by local name. */
934
+ function sourcesOf(fileEntry) {
935
+ const sources = new Map();
936
+ for (const binding of (fileEntry && fileEntry.importBindings) || []) {
937
+ if (binding.lexicalScopeStartLine != null) continue;
938
+ const local = binding.alias || binding.name;
939
+ const source = `${binding.module}\0${binding.name}\0${binding.kind || ''}`;
940
+ sources.set(local, sources.has(local) ? `${sources.get(local)}\n${source}` : source);
941
+ }
942
+ return sources;
943
+ }
944
+
945
+ /** Multiset difference: entries of `before` that `after` does not have. */
946
+ function removedFrom(before, after) {
947
+ const remaining = new Map();
948
+ for (const item of after) remaining.set(item, (remaining.get(item) || 0) + 1);
949
+ const removed = [];
950
+ for (const item of before) {
951
+ const count = remaining.get(item) || 0;
952
+ if (count > 0) remaining.set(item, count - 1);
953
+ else removed.push(item);
954
+ }
955
+ return removed;
956
+ }
957
+
958
+ /** Checker comments carry semantics even when no declaration changed. */
959
+ function checkingContext(root, traits) {
960
+ if (!root || !traits.checkDirectiveComment) return null;
961
+ const all = root.descendantsOfType([...COMMENT_NODES]);
962
+ const comments = all.filter(node => traits.checkDirectiveComment(node.text));
963
+ if (comments.length === 0) return null;
964
+ const barriers = all.filter(node => !traits.checkDirectiveComment(node.text) && traits.checkDirectiveBarrier?.(node.text));
965
+ const spans = new Set(all.map(node => `${node.startIndex}:${node.endIndex}`));
966
+ const code = [];
967
+ const scopeCounts = new Map();
968
+ const walk = (node, scope = []) => {
969
+ if (spans.has(`${node.startIndex}:${node.endIndex}`)) return;
970
+ if (CALLABLE_NODES.has(node.type) || ['class', 'class_declaration', 'class_definition'].includes(node.type)) {
971
+ const body = node.childForFieldName('body');
972
+ if (body) {
973
+ const header = tokensOf(node, body);
974
+ const key = JSON.stringify([scope, node.type, header]);
975
+ const ordinal = scopeCounts.get(key) || 0;
976
+ scopeCounts.set(key, ordinal + 1);
977
+ scope = [...scope, [node.type, header, ordinal]];
978
+ }
979
+ }
980
+ if (node.childCount === 0) code.push({ node, scope });
981
+ else for (const child of node.children) walk(child, scope);
982
+ };
983
+ walk(root);
984
+ // Identify the neighboring code by its line tokens and declaration,
985
+ // not by an absolute token offset. An inert edit in an earlier body
986
+ // cannot move a later directive onto different code. Repeated equal
987
+ // lines retain an occurrence number, so moving a suppression between
988
+ // identical calls still changes its attachment.
989
+ const lines = [];
990
+ for (const entry of code) {
991
+ let line = lines[lines.length - 1];
992
+ if (!line || line.row !== entry.node.startPosition.row) {
993
+ line = { row: entry.node.startPosition.row, entries: [] };
994
+ lines.push(line);
995
+ }
996
+ entry.line = line;
997
+ entry.column = line.entries.length;
998
+ line.entries.push(entry);
999
+ }
1000
+ const lineCounts = new Map();
1001
+ for (const line of lines) {
1002
+ line.key = JSON.stringify(line.entries.map(({ node, scope }) => [node.type, node.text, scope]));
1003
+ line.ordinal = lineCounts.get(line.key) || 0;
1004
+ lineCounts.set(line.key, line.ordinal + 1);
1005
+ }
1006
+ const anchor = entry => entry ? [entry.line.key, entry.line.ordinal, entry.column] : null;
1007
+ const position = node => {
1008
+ let low = 0;
1009
+ let high = code.length;
1010
+ while (low < high) {
1011
+ const middle = (low + high) >>> 1;
1012
+ if (code[middle].node.startIndex < node.startIndex) low = middle + 1;
1013
+ else high = middle;
1014
+ }
1015
+ const at = [anchor(code[low - 1]), anchor(code[low]),
1016
+ code[low - 1]?.node.endPosition.row === node.startPosition.row,
1017
+ code[low]?.node.startPosition.row === node.endPosition.row];
1018
+ return { at, nextRow: code[low]?.node.startPosition.row };
1019
+ };
1020
+ return JSON.stringify([
1021
+ comments.map(node => {
1022
+ const mode = traits.checkFileDirectiveComment?.(node.text) || 'local';
1023
+ const { at, nextRow } = position(node);
1024
+ let attachment;
1025
+ if (mode === 'column-zero-file') attachment = node.startPosition.column === 0;
1026
+ else if (mode === 'local' || mode === 'leading-or-local' && at[0] !== null) {
1027
+ if (traits.checkDirectiveTarget === 'next-line') {
1028
+ // A line directive applies forward. Earlier code is
1029
+ // not its target; a block comment matters only if it
1030
+ // occupies a line between the directive and its code.
1031
+ const blocked = barriers.some(barrier =>
1032
+ barrier.startPosition.row < nextRow &&
1033
+ barrier.endPosition.row > node.endPosition.row);
1034
+ attachment = [at[1], at[2], at[3], blocked];
1035
+ } else attachment = at;
1036
+ } else attachment = mode === 'file' || at[0] === null;
1037
+ return [node.text, mode, attachment];
1038
+ }),
1039
+ ]);
1040
+ }
1041
+
1042
+ /**
1043
+ * What a change did to a module outside its declarations.
1044
+ *
1045
+ * @returns {{removedNames, removedExports, starExportRemoved, importsRemoved,
1046
+ * importsAdded, addedBindings, removedBindings, reboundBindings, codeChanged}}
1047
+ * removedNames: names importers could take from the module and no longer
1048
+ * can; removedExports: those of them the module exported explicitly;
1049
+ * removedBindings / addedBindings: local names its imports no longer /
1050
+ * newly bind; removedBareImports: modules it imported without binding
1051
+ * a name and no longer imports;
1052
+ * importsRemoved / importsAdded: import statements that disappeared /
1053
+ * appeared as written; codeChanged: loose module-level code differs.
1054
+ */
1055
+ function moduleChange(before, after, traits) {
1056
+ const was = providedNames(before.fileEntry, traits);
1057
+ const is = providedNames(after.fileEntry, traits);
1058
+ const removedNames = is.open ? [] : [...was.names].filter(name => !is.names.has(name)).sort();
1059
+ const statementsOf = side => moduleStatements(side.root, side.fileEntry && side.fileEntry.symbols,
1060
+ side.fileEntry && side.fileEntry.exportDetails, side.fileEntry && side.fileEntry.importDetails);
1061
+ const beforeStatements = statementsOf(before);
1062
+ const afterStatements = statementsOf(after);
1063
+ const locals = entry => new Set(((entry && entry.importBindings) || [])
1064
+ .filter(binding => binding.lexicalScopeStartLine == null).map(binding => binding.alias || binding.name));
1065
+ const hadLocals = locals(before.fileEntry);
1066
+ const hasLocals = locals(after.fileEntry);
1067
+ const explicit = entry => new Set(((entry && entry.exportDetails) || [])
1068
+ .filter(detail => detail && detail.name && detail.name !== '*')
1069
+ .map(detail => (detail.type === 'default' ? 'default' : detail.alias || detail.name)));
1070
+ const exported = explicit(before.fileEntry);
1071
+ return {
1072
+ removedNames,
1073
+ checkingChanged: checkingContext(before.root, traits) !== checkingContext(after.root, traits),
1074
+ removedExports: removedNames.filter(name => exported.has(name)),
1075
+ // The module passed on everything another module exports, and no
1076
+ // longer does: which names that took away is not written here.
1077
+ starExportRemoved: was.open && !is.open,
1078
+ removedBindings: [...hadLocals].filter(name => !hasLocals.has(name)).sort(),
1079
+ // Local names that are still imported, from somewhere else or as
1080
+ // something else: every use of the name in the file now means it.
1081
+ reboundBindings: [...hasLocals].filter(name => hadLocals.has(name) &&
1082
+ sourcesOf(before.fileEntry).get(name) !== sourcesOf(after.fileEntry).get(name)).sort(),
1083
+ // Modules the file imported or included, binding no name, and no
1084
+ // longer does: they were there for what importing them does.
1085
+ removedBareImports: removedFrom(
1086
+ [...new Set((before.fileEntry && before.fileEntry.imports) || [])],
1087
+ [...new Set((after.fileEntry && after.fileEntry.imports) || [])])
1088
+ .filter(module => !((before.fileEntry && before.fileEntry.importBindings) || [])
1089
+ .some(binding => binding.module === module)).sort(),
1090
+ importsRemoved: removedFrom(beforeStatements.imports, afterStatements.imports).length,
1091
+ importsAdded: removedFrom(afterStatements.imports, beforeStatements.imports).length,
1092
+ addedBindings: [...hasLocals].filter(name => !hadLocals.has(name)).sort(),
1093
+ codeChanged: removedFrom(beforeStatements.code, afterStatements.code).length > 0 ||
1094
+ removedFrom(afterStatements.code, beforeStatements.code).length > 0,
1095
+ };
1096
+ }
1097
+
1098
+ function parseRevision(content, language) {
1099
+ const parser = getParser(language);
1100
+ if (!parser || content == null) return null;
1101
+ try { return safeParse(parser, content).rootNode; } catch { return null; }
1102
+ }
1103
+
1104
+ module.exports = {
1105
+ callableContract, classifyChange, baseCounterpart, parseRevision,
1106
+ declaresPlainReturn, moduleStatements, providedNames, moduleChange,
1107
+ };