ucn 5.5.2 → 5.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +35 -12
- package/.claude/skills/ucn/references/commands.md +21 -17
- package/.claude/skills/ucn/references/resolution-rules.md +4 -0
- package/.claude/skills/ucn/references/trust-contract.md +4 -4
- package/CHANGELOG.md +474 -0
- package/README.md +71 -29
- package/cli/flags.js +184 -0
- package/cli/index.js +138 -324
- package/core/account.js +44 -87
- package/core/analysis.js +273 -107
- package/core/ast-analysis.js +2 -0
- package/core/bridge.js +29 -65
- package/core/cache.js +101 -16
- package/core/callers.js +352 -70
- package/core/capture.js +331 -0
- package/core/change-kinds.js +888 -0
- package/core/check.js +1117 -139
- package/core/client-receivers.js +189 -0
- package/core/command-contracts.js +7 -6
- package/core/constructed-class.js +1195 -0
- package/core/contract-membership.js +142 -0
- package/core/deadcode.js +388 -85
- package/core/deletion.js +401 -0
- package/core/discovery.js +104 -9
- package/core/entrypoints.js +12 -6
- package/core/evidence-bands.js +68 -0
- package/core/evidence-bands.json +44 -0
- package/core/execute.js +172 -161
- package/core/hot-summary.js +241 -0
- package/core/index-ir.js +3 -1
- package/core/inprocess-clients.js +41 -15
- package/core/ir.js +5 -1
- package/core/member-signatures.js +789 -0
- package/core/output/analysis-ext.js +20 -10
- package/core/output/analysis.js +98 -60
- package/core/output/check.js +98 -37
- package/core/output/doctor.js +34 -1
- package/core/output/find.js +2 -1
- package/core/output/lines.js +63 -3
- package/core/output/public.js +30 -11
- package/core/output/refactoring.js +32 -0
- package/core/output/reporting.js +21 -2
- package/core/output/search.js +11 -2
- package/core/output/tracing.js +35 -7
- package/core/output-budget.js +124 -57
- package/core/parallel-build.js +3 -3
- package/core/project.js +106 -23
- package/core/provenance-type.js +12 -5
- package/core/public-command.js +35 -9
- package/core/python-modules.js +6 -0
- package/core/reflection.js +166 -23
- package/core/registry.js +623 -72
- package/core/reporting.js +69 -34
- package/core/request-helpers.js +63 -7
- package/core/review-items.js +75 -0
- package/core/search.js +89 -128
- package/core/shared.js +0 -45
- package/core/snapshot.js +350 -0
- package/core/source-universe.js +707 -0
- package/core/test-units.js +3624 -0
- package/core/tracing.js +150 -96
- package/core/verify.js +27 -11
- package/languages/adapter.js +8 -3
- package/languages/c-family.js +12 -0
- package/languages/csharp.js +634 -0
- package/languages/go.js +120 -0
- package/languages/index.js +422 -4
- package/languages/java.js +712 -0
- package/languages/javascript.js +1460 -2
- package/languages/lexical-scope.js +68 -0
- package/languages/python.js +1672 -1
- package/languages/rust.js +91 -0
- package/mcp/server.js +44 -110
- package/package.json +11 -6
|
@@ -0,0 +1,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
|
+
};
|