ucn 5.4.1 → 5.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +58 -236
- package/.claude/skills/ucn/references/commands.md +144 -68
- package/.claude/skills/ucn/references/resolution-rules.md +171 -0
- package/.claude/skills/ucn/references/trust-contract.md +27 -33
- package/README.md +18 -12
- package/cli/index.js +42 -16
- package/core/accessors.js +118 -3
- package/core/account.js +30 -0
- package/core/analysis.js +508 -264
- package/core/ast-analysis.js +76 -117
- package/core/async-producers.js +1729 -0
- package/core/bridge.js +1877 -268
- package/core/brief.js +6 -0
- package/core/build-worker.js +78 -14
- package/core/cache.js +522 -31
- package/core/callers.js +12053 -1047
- package/core/check.js +30 -38
- package/core/class-identity.js +1028 -0
- package/core/command-contracts.js +6 -6
- package/core/configuration-items.js +86 -0
- package/core/contract-membership.js +2262 -0
- package/core/cpp-scope.js +532 -0
- package/core/deadcode.js +171 -49
- package/core/decorator-binding.js +260 -0
- package/core/discovery.js +281 -164
- package/core/errors.js +52 -0
- package/core/execute.js +21 -9
- package/core/external-macros.js +326 -0
- package/core/graph-build.js +277 -8
- package/core/graph.js +35 -2
- package/core/ignore-rules.js +472 -0
- package/core/imports.js +504 -42
- package/core/index-ir.js +94 -11
- package/core/inprocess-clients.js +636 -0
- package/core/ir.js +87 -8
- package/core/macro-expansion.js +1416 -0
- package/core/output/analysis-ext.js +6 -6
- package/core/output/analysis.js +18 -2
- package/core/output/brief.js +3 -0
- package/core/output/doctor.js +4 -0
- package/core/output/endpoints.js +15 -6
- package/core/output/find.js +11 -2
- package/core/output/lines.js +8 -2
- package/core/output/refactoring.js +121 -14
- package/core/output/reporting.js +31 -3
- package/core/output/shared.js +14 -0
- package/core/output-budget.js +316 -122
- package/core/parallel-build.js +124 -47
- package/core/parser.js +3 -2
- package/core/project.js +457 -60
- package/core/provenance-facts.js +156 -12
- package/core/provenance-overload.js +30 -3
- package/core/provenance.js +104 -30
- package/core/pytest-fixtures.js +434 -0
- package/core/python-fixture-flow.js +17 -3
- package/core/python-modules.js +173 -0
- package/core/reflection.js +877 -0
- package/core/registry.js +1 -1
- package/core/reporting.js +234 -52
- package/core/request-helpers.js +561 -0
- package/core/route-graph.js +1922 -0
- package/core/route-lists.js +2414 -0
- package/core/rust-macro-expansion.js +2113 -0
- package/core/rust-modules.js +185 -0
- package/core/search.js +121 -29
- package/core/shared.js +19 -1
- package/core/test-name-bindings.js +256 -0
- package/core/type-denotation.js +1161 -0
- package/core/type-references.js +3229 -0
- package/core/verify.js +3360 -672
- package/languages/adapter.js +31 -5
- package/languages/c-family.js +4061 -212
- package/languages/c-macro-invocations.js +380 -0
- package/languages/c-preprocessor.js +254 -0
- package/languages/csharp.js +1406 -49
- package/languages/go.js +1243 -70
- package/languages/index.js +728 -6
- package/languages/java.js +473 -61
- package/languages/javascript.js +1270 -197
- package/languages/lexical-scope.js +1280 -0
- package/languages/python.js +1557 -72
- package/languages/rust-macro-rules.js +1020 -0
- package/languages/rust-value-flow.js +416 -0
- package/languages/rust.js +2237 -115
- package/languages/utils.js +298 -4
- package/mcp/server.js +25 -11
- package/package.json +1 -1
|
@@ -0,0 +1,877 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Runtime reflection by NAME PATTERN (fix #363).
|
|
5
|
+
*
|
|
6
|
+
* A reflective member access names its target with a string. A literal string
|
|
7
|
+
* is an exact spelling; a string BUILT from literal fragments around a runtime
|
|
8
|
+
* part (`"_get_%s_permissions" % src`, f"_get_{p}", `"on" + evt`,
|
|
9
|
+
* `this[`handle${t}`]`, `getMethod("get" + n)`, `MethodByName("X" + y)`) is a
|
|
10
|
+
* pattern: literal fragments with wildcards. Patterns come from the AST of the
|
|
11
|
+
* name argument (plus one-hop local string bindings in the same scope); a name
|
|
12
|
+
* the pattern can spell is a possible runtime target when the reflective
|
|
13
|
+
* receiver could hold an instance of the candidate's class.
|
|
14
|
+
*
|
|
15
|
+
* Consumers:
|
|
16
|
+
* - deadcode withholds pattern-reachable candidates (counted per pattern);
|
|
17
|
+
* - findCallers (collectAccount) lists each reaching site as an unverified
|
|
18
|
+
* `reflection-pattern` caller, so impact/plan see the dependency.
|
|
19
|
+
* A pattern whose literal part names nothing specific (`"_%s" % x`, a bare
|
|
20
|
+
* parameter) stays a disclosed dynamic use, never a blanket withhold.
|
|
21
|
+
*
|
|
22
|
+
* Site extraction is syntax-only and runs in the same per-file parse as the
|
|
23
|
+
* computed-dispatch inventory (ast-analysis.js); the per-project matcher is
|
|
24
|
+
* memoized on the index and reset with it.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const { langTraits } = require('../languages');
|
|
28
|
+
|
|
29
|
+
const WILD = '\u0000*';
|
|
30
|
+
const MODULE_RECEIVER = Object.freeze({ kind: 'module' });
|
|
31
|
+
const CALL_NODE_TYPES = ['call', 'call_expression', 'invocation_expression', 'method_invocation'];
|
|
32
|
+
// Computed-member keys that build a name in place (`'on' + evt`, `\`h${t}\``).
|
|
33
|
+
const COMPUTED_PATTERN_KEYS = new Set(['binary_expression', 'template_string']);
|
|
34
|
+
const MAX_ALTERNATIVES = 8;
|
|
35
|
+
const MAX_RESOLVE_DEPTH = 3;
|
|
36
|
+
const EXPRESSION_CAP = 160;
|
|
37
|
+
// Letters/digits a wildcard pattern must spell before it identifies anything:
|
|
38
|
+
// `_*`, `*s` or `*` name every member and stay disclosed dynamic uses.
|
|
39
|
+
const MIN_SPECIFIC_CHARS = 2;
|
|
40
|
+
|
|
41
|
+
const STRING_NODES = new Set([
|
|
42
|
+
'string', 'string_literal', 'raw_string_literal', 'verbatim_string_literal',
|
|
43
|
+
'interpreted_string_literal', 'template_string', 'interpolated_string_expression',
|
|
44
|
+
]);
|
|
45
|
+
const STRING_CONTENT_NODES = new Set([
|
|
46
|
+
'string_content', 'string_fragment', 'string_literal_content',
|
|
47
|
+
'interpreted_string_literal_content', 'raw_string_literal_content',
|
|
48
|
+
'raw_string_content', 'verbatim_string_literal_content',
|
|
49
|
+
]);
|
|
50
|
+
const INTERPOLATION_NODES = new Set([
|
|
51
|
+
'interpolation', 'template_substitution',
|
|
52
|
+
]);
|
|
53
|
+
const SCOPE_NODES = new Set([
|
|
54
|
+
'function_declaration', 'generator_function_declaration',
|
|
55
|
+
'function_expression', 'generator_function', 'arrow_function',
|
|
56
|
+
'method_definition', 'function_definition', 'lambda',
|
|
57
|
+
'method_declaration', 'func_literal', 'constructor_declaration',
|
|
58
|
+
'lambda_expression', 'local_function_statement', 'anonymous_method_expression',
|
|
59
|
+
'module', 'program', 'source_file', 'compilation_unit',
|
|
60
|
+
]);
|
|
61
|
+
const CLASS_NODES = new Set([
|
|
62
|
+
'class_definition', 'class_declaration', 'class', 'class_expression',
|
|
63
|
+
'abstract_class_declaration', 'struct_declaration', 'record_declaration',
|
|
64
|
+
'enum_declaration', 'interface_declaration',
|
|
65
|
+
]);
|
|
66
|
+
// A non-arrow JS function rebinds `this`; arrows and class members keep it.
|
|
67
|
+
const THIS_REBINDING_FUNCTIONS = new Set([
|
|
68
|
+
'function_declaration', 'function_expression', 'generator_function',
|
|
69
|
+
'generator_function_declaration', 'function',
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// String patterns
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
function _concat(a, b) {
|
|
77
|
+
const out = [];
|
|
78
|
+
for (const x of a) {
|
|
79
|
+
for (const y of b) {
|
|
80
|
+
out.push([...x, ...y]);
|
|
81
|
+
if (out.length > MAX_ALTERNATIVES) return [[WILD]];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function _expandFormat(alternatives, style) {
|
|
88
|
+
const re = style === 'brace'
|
|
89
|
+
? /\{\{|\}\}|\{[^{}]*\}/g
|
|
90
|
+
: /%%|%(?:\([^)]*\)|\[\d+\])?[-#0 +]*(?:\d+|\*)?(?:\.(?:\d+|\*))?[A-Za-z]/g;
|
|
91
|
+
return alternatives.map(alt => {
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const piece of alt) {
|
|
94
|
+
if (piece === WILD) { out.push(WILD); continue; }
|
|
95
|
+
let last = 0;
|
|
96
|
+
let m;
|
|
97
|
+
re.lastIndex = 0;
|
|
98
|
+
while ((m = re.exec(piece)) !== null) {
|
|
99
|
+
out.push(piece.slice(last, m.index));
|
|
100
|
+
if (m[0] === '%%') out.push('%');
|
|
101
|
+
else if (m[0] === '{{') out.push('{');
|
|
102
|
+
else if (m[0] === '}}') out.push('}');
|
|
103
|
+
else out.push(WILD);
|
|
104
|
+
last = m.index + m[0].length;
|
|
105
|
+
}
|
|
106
|
+
out.push(piece.slice(last));
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function _unwrap(node) {
|
|
113
|
+
let cur = node;
|
|
114
|
+
for (let i = 0; cur && i < 4; i++) {
|
|
115
|
+
if (cur.type === 'argument' || cur.type === 'parenthesized_expression') {
|
|
116
|
+
cur = cur.namedChildren.find(c => c.type !== 'comment') || null;
|
|
117
|
+
} else break;
|
|
118
|
+
}
|
|
119
|
+
return cur;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function _stringLiteral(node) {
|
|
123
|
+
const pieces = [];
|
|
124
|
+
let sawContent = false;
|
|
125
|
+
for (const child of node.children || []) {
|
|
126
|
+
if (STRING_CONTENT_NODES.has(child.type)) {
|
|
127
|
+
pieces.push(child.text);
|
|
128
|
+
sawContent = true;
|
|
129
|
+
} else if (INTERPOLATION_NODES.has(child.type)) {
|
|
130
|
+
pieces.push(WILD);
|
|
131
|
+
} else if (child.type === 'escape_sequence') {
|
|
132
|
+
// An escaped character never spells an identifier fragment.
|
|
133
|
+
pieces.push('\\');
|
|
134
|
+
}
|
|
135
|
+
// Delimiters, prefixes and quote tokens carry no name text.
|
|
136
|
+
}
|
|
137
|
+
if (!sawContent && pieces.length === 0) {
|
|
138
|
+
// Grammars that keep simple literals as one token (`"abc"`, `'abc'`).
|
|
139
|
+
const raw = String(node.text || '');
|
|
140
|
+
const m = /^[A-Za-z@$]*(["'`])([\s\S]*)\1$/.exec(raw);
|
|
141
|
+
if (m) pieces.push(m[2]);
|
|
142
|
+
}
|
|
143
|
+
return [pieces];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Pattern alternatives of a string-valued expression, or null when the
|
|
148
|
+
* expression is not string-shaped. A runtime part becomes WILD.
|
|
149
|
+
* @param {object} node
|
|
150
|
+
* @param {object} ctx - { formatCalls: {callee: style}, resolve(identNode) }
|
|
151
|
+
*/
|
|
152
|
+
function stringAlternatives(node, ctx, depth = 0) {
|
|
153
|
+
node = _unwrap(node);
|
|
154
|
+
if (!node || depth > 12) return null;
|
|
155
|
+
if (STRING_NODES.has(node.type)) return _stringLiteral(node);
|
|
156
|
+
switch (node.type) {
|
|
157
|
+
case 'concatenated_string': {
|
|
158
|
+
let acc = [[]];
|
|
159
|
+
for (const child of node.namedChildren) {
|
|
160
|
+
const part = stringAlternatives(child, ctx, depth + 1) || [[WILD]];
|
|
161
|
+
acc = _concat(acc, part);
|
|
162
|
+
}
|
|
163
|
+
return acc;
|
|
164
|
+
}
|
|
165
|
+
case 'binary_operator':
|
|
166
|
+
case 'binary_expression': {
|
|
167
|
+
const op = node.childForFieldName('operator')?.text ||
|
|
168
|
+
(node.children || []).find(c => !c.isNamed)?.text;
|
|
169
|
+
const left = node.childForFieldName('left') || node.namedChild(0);
|
|
170
|
+
const right = node.childForFieldName('right') || node.namedChild(1);
|
|
171
|
+
if (op === '+') {
|
|
172
|
+
const l = stringAlternatives(left, ctx, depth + 1);
|
|
173
|
+
const r = stringAlternatives(right, ctx, depth + 1);
|
|
174
|
+
if (!l && !r) return null;
|
|
175
|
+
return _concat(l || [[WILD]], r || [[WILD]]);
|
|
176
|
+
}
|
|
177
|
+
if (op === '%') {
|
|
178
|
+
// Python printf-style formatting: only a string left operand.
|
|
179
|
+
const l = _unwrap(left);
|
|
180
|
+
if (!l || !(STRING_NODES.has(l.type) || l.type === 'concatenated_string')) return null;
|
|
181
|
+
return _expandFormat(stringAlternatives(l, ctx, depth + 1), 'percent');
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
case 'call':
|
|
186
|
+
case 'call_expression':
|
|
187
|
+
case 'invocation_expression':
|
|
188
|
+
case 'method_invocation': {
|
|
189
|
+
const fn = node.childForFieldName('function') || node.childForFieldName('name');
|
|
190
|
+
const argsNode = node.childForFieldName('arguments') ||
|
|
191
|
+
(node.namedChildren || []).find(c => c.type === 'argument_list' || c.type === 'arguments');
|
|
192
|
+
const args = (argsNode?.namedChildren || []).filter(c => c.type !== 'comment');
|
|
193
|
+
// `"lit{}".format(x)` — a format method on a string receiver.
|
|
194
|
+
if (fn?.type === 'attribute' &&
|
|
195
|
+
fn.childForFieldName('attribute')?.text === 'format') {
|
|
196
|
+
const recv = _unwrap(fn.childForFieldName('object'));
|
|
197
|
+
if (recv && (STRING_NODES.has(recv.type) || recv.type === 'concatenated_string')) {
|
|
198
|
+
return _expandFormat(stringAlternatives(recv, ctx, depth + 1), 'brace');
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
// Formatting functions from the language vocabulary
|
|
203
|
+
// (fmt.Sprintf, String.format, string.Format).
|
|
204
|
+
let calleeText;
|
|
205
|
+
if (node.type === 'method_invocation') {
|
|
206
|
+
const obj = node.childForFieldName('object');
|
|
207
|
+
calleeText = `${obj ? obj.text + '.' : ''}${node.childForFieldName('name')?.text || ''}`;
|
|
208
|
+
} else {
|
|
209
|
+
calleeText = fn?.text || '';
|
|
210
|
+
}
|
|
211
|
+
const style = ctx.formatCalls && Object.prototype.hasOwnProperty.call(ctx.formatCalls, calleeText)
|
|
212
|
+
? ctx.formatCalls[calleeText] : null;
|
|
213
|
+
if (style && args[0]) {
|
|
214
|
+
const fmt = stringAlternatives(args[0], ctx, depth + 1);
|
|
215
|
+
return fmt ? _expandFormat(fmt, style) : null;
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
case 'identifier': {
|
|
220
|
+
if (!ctx.resolve || (ctx.resolveDepth || 0) >= MAX_RESOLVE_DEPTH) return null;
|
|
221
|
+
const values = ctx.resolve(node);
|
|
222
|
+
if (!values || values.length === 0) return null;
|
|
223
|
+
let out = [];
|
|
224
|
+
for (const value of values) {
|
|
225
|
+
const alt = stringAlternatives(value,
|
|
226
|
+
{ ...ctx, resolveDepth: (ctx.resolveDepth || 0) + 1 }, depth + 1);
|
|
227
|
+
if (!alt) return null; // one non-string binding: unknown
|
|
228
|
+
out.push(...alt);
|
|
229
|
+
if (out.length > MAX_ALTERNATIVES) return [[WILD]];
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
default:
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Normalize one alternative: merged literals and wildcards; null if it
|
|
239
|
+
* cannot spell an identifier. */
|
|
240
|
+
function _normalize(alt) {
|
|
241
|
+
const out = [];
|
|
242
|
+
for (const piece of alt) {
|
|
243
|
+
if (piece === WILD) {
|
|
244
|
+
if (out[out.length - 1] !== WILD) out.push(WILD);
|
|
245
|
+
} else if (piece.length > 0) {
|
|
246
|
+
if (out.length > 0 && out[out.length - 1] !== WILD) out[out.length - 1] += piece;
|
|
247
|
+
else out.push(piece);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (out.length === 0) return null; // the empty string names nothing
|
|
251
|
+
for (const piece of out) {
|
|
252
|
+
if (piece !== WILD && !/^[A-Za-z0-9_$]+$/.test(piece)) return null;
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Glob string (`_get_*_permissions`) of a normalized alternative. */
|
|
258
|
+
function patternText(pieces) {
|
|
259
|
+
return pieces.map(p => (p === WILD ? '*' : p)).join('');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function _specific(pieces) {
|
|
263
|
+
let chars = 0;
|
|
264
|
+
for (const p of pieces) if (p !== WILD) chars += (p.match(/[A-Za-z0-9]/g) || []).length;
|
|
265
|
+
return chars >= MIN_SPECIFIC_CHARS;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Classify a name expression.
|
|
270
|
+
* @returns {{name?: string, patterns?: string[], dynamic: boolean}}
|
|
271
|
+
* name - one exact spelling; patterns - specific glob patterns (sorted);
|
|
272
|
+
* dynamic - some alternative is unknown or names anything.
|
|
273
|
+
*/
|
|
274
|
+
function classifyNameExpression(node, ctx) {
|
|
275
|
+
const alts = stringAlternatives(node, ctx);
|
|
276
|
+
if (!alts) return { dynamic: true };
|
|
277
|
+
const exact = new Set();
|
|
278
|
+
const patterns = new Set();
|
|
279
|
+
let dynamic = false;
|
|
280
|
+
for (const raw of alts) {
|
|
281
|
+
const pieces = _normalize(raw);
|
|
282
|
+
if (!pieces) continue; // cannot spell a member name
|
|
283
|
+
if (!pieces.includes(WILD)) exact.add(pieces[0]);
|
|
284
|
+
else if (_specific(pieces)) patterns.add(patternText(pieces));
|
|
285
|
+
else dynamic = true;
|
|
286
|
+
}
|
|
287
|
+
if (exact.size === 0 && patterns.size === 0) return { dynamic: true };
|
|
288
|
+
if (exact.size === 1 && patterns.size === 0 && !dynamic) {
|
|
289
|
+
return { name: [...exact][0], dynamic: false };
|
|
290
|
+
}
|
|
291
|
+
// Several exact spellings behave as one-literal patterns.
|
|
292
|
+
return { patterns: [...exact, ...patterns].sort(), dynamic };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
// Local string bindings (one hop, same scope)
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
function _enclosingScope(node) {
|
|
300
|
+
let cur = node.parent;
|
|
301
|
+
while (cur && !SCOPE_NODES.has(cur.type)) cur = cur.parent;
|
|
302
|
+
return cur;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function _bindingPairs(node) {
|
|
306
|
+
switch (node.type) {
|
|
307
|
+
case 'assignment': // Python
|
|
308
|
+
case 'assignment_expression': { // JS, C#
|
|
309
|
+
const left = node.childForFieldName('left');
|
|
310
|
+
const right = node.childForFieldName('right');
|
|
311
|
+
return left?.type === 'identifier' && right ? [[left.text, right]] : [];
|
|
312
|
+
}
|
|
313
|
+
case 'augmented_assignment':
|
|
314
|
+
case 'augmented_assignment_expression':
|
|
315
|
+
case 'compound_assignment_expr': {
|
|
316
|
+
const left = node.childForFieldName('left');
|
|
317
|
+
return left?.type === 'identifier' ? [[left.text, null]] : [];
|
|
318
|
+
}
|
|
319
|
+
case 'variable_declarator': { // JS, Java, C#
|
|
320
|
+
const name = node.childForFieldName('name') ||
|
|
321
|
+
node.namedChildren.find(c => c.type === 'identifier');
|
|
322
|
+
let value = node.childForFieldName('value');
|
|
323
|
+
if (!value) {
|
|
324
|
+
const named = node.namedChildren.filter(c => c !== name && c.type !== 'comment');
|
|
325
|
+
const clause = named.find(c => c.type === 'equals_value_clause');
|
|
326
|
+
value = clause ? clause.namedChild(0) : (named.length > 0 ? named[named.length - 1] : null);
|
|
327
|
+
if (value && (value.type === 'bracketed_argument_list' || value.type.endsWith('_type'))) value = null;
|
|
328
|
+
}
|
|
329
|
+
return name?.type === 'identifier' && value ? [[name.text, value]] : [];
|
|
330
|
+
}
|
|
331
|
+
case 'short_var_declaration': // Go
|
|
332
|
+
case 'assignment_statement': {
|
|
333
|
+
const left = node.childForFieldName('left');
|
|
334
|
+
const right = node.childForFieldName('right');
|
|
335
|
+
const ls = left?.namedChildren || [];
|
|
336
|
+
const rs = right?.namedChildren || [];
|
|
337
|
+
if (ls.length !== rs.length) {
|
|
338
|
+
return ls.filter(l => l.type === 'identifier').map(l => [l.text, null]);
|
|
339
|
+
}
|
|
340
|
+
return ls.map((l, i) => (l.type === 'identifier' ? [l.text, rs[i]] : null)).filter(Boolean);
|
|
341
|
+
}
|
|
342
|
+
case 'var_spec':
|
|
343
|
+
case 'const_spec': {
|
|
344
|
+
const names = node.namedChildren.filter(c => c.type === 'identifier');
|
|
345
|
+
const value = node.childForFieldName('value');
|
|
346
|
+
const rs = value?.namedChildren || [];
|
|
347
|
+
if (names.length !== rs.length) return names.map(n => [n.text, null]);
|
|
348
|
+
return names.map((n, i) => [n.text, rs[i]]);
|
|
349
|
+
}
|
|
350
|
+
default:
|
|
351
|
+
return [];
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function makeResolver() {
|
|
356
|
+
const scopes = new Map();
|
|
357
|
+
const bindingsOf = scope => {
|
|
358
|
+
const key = `${scope.startIndex}:${scope.endIndex}`;
|
|
359
|
+
let map = scopes.get(key);
|
|
360
|
+
if (map) return map;
|
|
361
|
+
map = new Map();
|
|
362
|
+
scopes.set(key, map);
|
|
363
|
+
const visit = n => {
|
|
364
|
+
for (const child of n.namedChildren || []) {
|
|
365
|
+
// Nested callables own their own bindings.
|
|
366
|
+
if (SCOPE_NODES.has(child.type)) continue;
|
|
367
|
+
for (const [name, value] of _bindingPairs(child)) {
|
|
368
|
+
if (!map.has(name)) map.set(name, []);
|
|
369
|
+
map.get(name).push(value);
|
|
370
|
+
}
|
|
371
|
+
visit(child);
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
visit(scope.type === 'method_definition' || scope.type === 'function_definition' ||
|
|
375
|
+
scope.type === 'method_declaration'
|
|
376
|
+
? (scope.childForFieldName('body') || scope) : scope);
|
|
377
|
+
return map;
|
|
378
|
+
};
|
|
379
|
+
return ident => {
|
|
380
|
+
const scope = _enclosingScope(ident);
|
|
381
|
+
if (!scope) return null;
|
|
382
|
+
const values = bindingsOf(scope).get(ident.text);
|
|
383
|
+
// A parameter/captured name (no binding here) or any non-value
|
|
384
|
+
// rebinding (augmented, tuple split) leaves the name unknown.
|
|
385
|
+
if (!values || values.some(v => !v)) return null;
|
|
386
|
+
return values;
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
// Receivers
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
|
|
394
|
+
function _enclosingClass(node, stopAtThisRebinding) {
|
|
395
|
+
let cur = node.parent;
|
|
396
|
+
while (cur) {
|
|
397
|
+
if (CLASS_NODES.has(cur.type)) return cur;
|
|
398
|
+
if (stopAtThisRebinding && (THIS_REBINDING_FUNCTIONS.has(cur.type) ||
|
|
399
|
+
// An object-literal method binds `this` to the literal.
|
|
400
|
+
(cur.type === 'method_definition' && cur.parent?.type === 'object'))) return null;
|
|
401
|
+
cur = cur.parent;
|
|
402
|
+
}
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function _classInfo(classNode) {
|
|
407
|
+
const nameNode = classNode.childForFieldName('name') ||
|
|
408
|
+
classNode.namedChildren.find(c => c.type === 'identifier' || c.type === 'type_identifier');
|
|
409
|
+
if (!nameNode) return null;
|
|
410
|
+
return { name: nameNode.text, line: classNode.startPosition.row + 1 };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Is `ident` the self parameter of the method it appears in? */
|
|
414
|
+
function _selfParamClass(ident, selfNames) {
|
|
415
|
+
if (!selfNames || !selfNames.includes(ident.text)) return null;
|
|
416
|
+
let fn = ident.parent;
|
|
417
|
+
while (fn && fn.type !== 'function_definition') fn = fn.parent;
|
|
418
|
+
if (!fn) return null;
|
|
419
|
+
const params = fn.childForFieldName('parameters');
|
|
420
|
+
const first = params?.namedChildren?.find(c => c.type !== 'comment');
|
|
421
|
+
const firstName = first?.type === 'identifier' ? first.text
|
|
422
|
+
: first?.childForFieldName?.('name')?.text || null;
|
|
423
|
+
if (firstName !== ident.text) return null;
|
|
424
|
+
// Decorated methods sit in decorated_definition inside the class block.
|
|
425
|
+
let owner = fn.parent;
|
|
426
|
+
if (owner?.type === 'decorated_definition') owner = owner.parent;
|
|
427
|
+
if (owner?.type !== 'block' || owner.parent?.type !== 'class_definition') return null;
|
|
428
|
+
return _classInfo(owner.parent);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Receiver facts of a reflective access. Returns null (untyped: any class) or
|
|
433
|
+
* { kind: 'self', className, classLine } - self/this/getClass()/GetType();
|
|
434
|
+
* { kind: 'name', name } - an identifier that may name a class (resolved
|
|
435
|
+
* against the index at query time; anything else stays untyped);
|
|
436
|
+
* { kind: 'type', name } - a class literal (`Foo.class`, `typeof(Foo)`).
|
|
437
|
+
*/
|
|
438
|
+
function classifyReceiver(node, language) {
|
|
439
|
+
node = _unwrap(node);
|
|
440
|
+
if (!node) return null;
|
|
441
|
+
const traits = langTraits(language) || {};
|
|
442
|
+
const api = traits.reflectionApi || {};
|
|
443
|
+
const selfNames = Array.isArray(traits.selfParam) ? traits.selfParam : [];
|
|
444
|
+
const selfOf = (anchor, stop) => {
|
|
445
|
+
const cls = _enclosingClass(anchor, stop);
|
|
446
|
+
const info = cls && _classInfo(cls);
|
|
447
|
+
return info ? { kind: 'self', className: info.name, classLine: info.line } : null;
|
|
448
|
+
};
|
|
449
|
+
if (node.type === 'this' || node.type === 'this_expression' ||
|
|
450
|
+
(node.type === 'identifier' && node.text === 'this' && selfNames.includes('this'))) {
|
|
451
|
+
return selfOf(node, true);
|
|
452
|
+
}
|
|
453
|
+
if (node.type === 'identifier') {
|
|
454
|
+
const info = _selfParamClass(node, selfNames);
|
|
455
|
+
if (info) return { kind: 'self', className: info.name, classLine: info.line };
|
|
456
|
+
return { kind: 'name', name: node.text };
|
|
457
|
+
}
|
|
458
|
+
// type(self) / self.__class__ / getClass() / this.GetType()
|
|
459
|
+
if (node.type === 'call' || node.type === 'method_invocation' || node.type === 'invocation_expression') {
|
|
460
|
+
const fn = node.childForFieldName('function') || node.childForFieldName('name');
|
|
461
|
+
const obj = node.type === 'method_invocation' ? node.childForFieldName('object') : null;
|
|
462
|
+
const argsNode = node.childForFieldName('arguments') ||
|
|
463
|
+
node.namedChildren.find(c => c.type === 'argument_list');
|
|
464
|
+
const args = (argsNode?.namedChildren || []).filter(c => c.type !== 'comment');
|
|
465
|
+
const fnName = fn?.type === 'member_access_expression'
|
|
466
|
+
? fn.childForFieldName('name')?.text : fn?.text;
|
|
467
|
+
const fnObj = fn?.type === 'member_access_expression' ? fn.childForFieldName('expression') : obj;
|
|
468
|
+
if ((api.selfTypeCalls || []).includes(fnName) && args.length === 0 &&
|
|
469
|
+
(!fnObj || fnObj.type === 'this' || fnObj.type === 'this_expression')) {
|
|
470
|
+
return selfOf(node, true);
|
|
471
|
+
}
|
|
472
|
+
if (fnName === 'type' && args.length === 1 && args[0].type === 'identifier') {
|
|
473
|
+
const info = _selfParamClass(args[0], selfNames);
|
|
474
|
+
if (info) return { kind: 'self', className: info.name, classLine: info.line };
|
|
475
|
+
}
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
if (node.type === 'attribute' && node.childForFieldName('attribute')?.text === '__class__') {
|
|
479
|
+
const obj = node.childForFieldName('object');
|
|
480
|
+
const info = obj?.type === 'identifier' && _selfParamClass(obj, selfNames);
|
|
481
|
+
if (info) return { kind: 'self', className: info.name, classLine: info.line };
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
// Foo.class (Java) / typeof(Foo) (C#): certainly a type.
|
|
485
|
+
if ((api.typeLiteralNodes || []).includes(node.type)) {
|
|
486
|
+
const t = node.namedChildren.find(c => c.type !== 'comment');
|
|
487
|
+
const name = t ? String(t.text).replace(/<.*$/s, '').split('.').pop() : null;
|
|
488
|
+
// C# `typeof(Outcome<>)` / `typeof(Outcome<,>)`: the written arity
|
|
489
|
+
// selects one of the same-name types (fix #389).
|
|
490
|
+
let arity;
|
|
491
|
+
if (t?.type === 'generic_name') {
|
|
492
|
+
const list = t.namedChildren.find(c => c.type === 'type_argument_list');
|
|
493
|
+
const args = list ? list.namedChildren.filter(c => c.type !== 'comment') : [];
|
|
494
|
+
arity = args.length > 0 ? args.length
|
|
495
|
+
: list ? list.children.filter(c => c.type === ',').length + 1 : 0;
|
|
496
|
+
} else if (t?.type === 'identifier' && node.type === 'typeof_expression') {
|
|
497
|
+
arity = 0;
|
|
498
|
+
}
|
|
499
|
+
return name ? { kind: 'type', name, ...(arity !== undefined && { arity }) } : null;
|
|
500
|
+
}
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ---------------------------------------------------------------------------
|
|
505
|
+
// Site extraction
|
|
506
|
+
// ---------------------------------------------------------------------------
|
|
507
|
+
|
|
508
|
+
// Member-shaped callee node -> [member-name field, receiver field].
|
|
509
|
+
const MEMBER_CALLEE_FIELDS = {
|
|
510
|
+
selector_expression: ['field', 'operand'], // Go
|
|
511
|
+
member_expression: ['property', 'object'], // JS/TS
|
|
512
|
+
member_access_expression: ['name', 'expression'], // C#
|
|
513
|
+
attribute: ['attribute', 'object'], // Python
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
/** Callee text, member name and receiver of a call node (no arguments). */
|
|
517
|
+
function _calleeParts(node, wantMembers, wantQualified) {
|
|
518
|
+
if (node.type === 'method_invocation') {
|
|
519
|
+
if (!wantMembers) return null;
|
|
520
|
+
const name = node.childForFieldName('name')?.text || '';
|
|
521
|
+
return { calleeText: name, member: name, object: node.childForFieldName('object') };
|
|
522
|
+
}
|
|
523
|
+
const fn = node.childForFieldName('function') || node.namedChild(0);
|
|
524
|
+
if (!fn) return null;
|
|
525
|
+
if (fn.type === 'identifier') {
|
|
526
|
+
return { calleeText: fn.text || '', member: fn.text, object: null, bare: true };
|
|
527
|
+
}
|
|
528
|
+
if (!wantMembers) return wantQualified ? { calleeText: null, fn, member: null, object: null } : null;
|
|
529
|
+
const fields = MEMBER_CALLEE_FIELDS[fn.type];
|
|
530
|
+
if (!fields) return wantQualified ? { calleeText: null, fn, member: null, object: null } : null;
|
|
531
|
+
return {
|
|
532
|
+
calleeText: null,
|
|
533
|
+
fn,
|
|
534
|
+
member: fn.childForFieldName(fields[0])?.text || null,
|
|
535
|
+
object: fn.childForFieldName(fields[1]),
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function _callArgs(node) {
|
|
540
|
+
const argsNode = node.childForFieldName('arguments') ||
|
|
541
|
+
(node.namedChildren || []).find(c =>
|
|
542
|
+
c.type === 'argument_list' || c.type === 'arguments');
|
|
543
|
+
return argsNode ? argsNode.namedChildren.filter(c => c.type !== 'comment') : null;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Reflection sites of one parsed file.
|
|
548
|
+
* Each site: { kind, line, expression, dynamic, name?, patterns?, receiver?,
|
|
549
|
+
* membersOnly? }.
|
|
550
|
+
*/
|
|
551
|
+
function reflectionSitesInTree(tree, language, code = null) {
|
|
552
|
+
const traits = langTraits(language) || {};
|
|
553
|
+
const api = traits.reflectionApi;
|
|
554
|
+
if (!api || !tree) return [];
|
|
555
|
+
const resolve = makeResolver();
|
|
556
|
+
const ctx = { formatCalls: api.formatCalls || null, resolve };
|
|
557
|
+
const sites = [];
|
|
558
|
+
const push = (node, kind, nameNode, receiverNode, receiverFact) => {
|
|
559
|
+
const cls = classifyNameExpression(nameNode, ctx);
|
|
560
|
+
const expression = String(node.text || '');
|
|
561
|
+
sites.push({
|
|
562
|
+
kind,
|
|
563
|
+
line: node.startPosition.row + 1,
|
|
564
|
+
expression: expression.length > EXPRESSION_CAP
|
|
565
|
+
? expression.slice(0, EXPRESSION_CAP) + '...' : expression,
|
|
566
|
+
...(cls.name && { name: cls.name }),
|
|
567
|
+
...(cls.patterns && { patterns: cls.patterns }),
|
|
568
|
+
dynamic: cls.dynamic,
|
|
569
|
+
...(receiverFact ? { receiver: receiverFact }
|
|
570
|
+
: receiverNode && { receiver: classifyReceiver(receiverNode, language) }),
|
|
571
|
+
...(api.membersOnly && { membersOnly: true }),
|
|
572
|
+
});
|
|
573
|
+
};
|
|
574
|
+
const ns = api.namespaceLookups;
|
|
575
|
+
// Candidate nodes are located natively: reflective calls from the
|
|
576
|
+
// vocabulary's spelling positions (descendantForIndex, walking up to the
|
|
577
|
+
// enclosing call), computed member keys by node type. No JS-side walk of
|
|
578
|
+
// the whole tree.
|
|
579
|
+
const hintPattern = api.hintPattern || null;
|
|
580
|
+
const useHints = code != null && hintPattern;
|
|
581
|
+
const types = [];
|
|
582
|
+
if (!useHints) {
|
|
583
|
+
types.push(...CALL_NODE_TYPES);
|
|
584
|
+
if (ns?.subscriptOf) types.push('subscript');
|
|
585
|
+
}
|
|
586
|
+
if (api.computedMembers) types.push('subscript_expression');
|
|
587
|
+
const wantMembers = (api.members || []).length > 0;
|
|
588
|
+
// Qualified function callees (`Reflect.get`) need the callee text of
|
|
589
|
+
// member-shaped calls; bare vocabularies (`getattr`) never do.
|
|
590
|
+
const wantQualified = (api.functions || []).some(rule => rule.qualified);
|
|
591
|
+
const visitNode = node => {
|
|
592
|
+
if (node.type === 'subscript_expression') {
|
|
593
|
+
// obj["on" + evt] / this[`handle${t}`] (JS/TS computed member
|
|
594
|
+
// access): only a key built in place from literal fragments
|
|
595
|
+
// counts; opaque keys stay the computed-dispatch disclosure.
|
|
596
|
+
const index = node.childForFieldName('index');
|
|
597
|
+
const inner = _unwrap(index);
|
|
598
|
+
if (!inner || !COMPUTED_PATTERN_KEYS.has(inner.type)) return;
|
|
599
|
+
const cls = classifyNameExpression(index, ctx);
|
|
600
|
+
if (cls.patterns && cls.patterns.length > 0) {
|
|
601
|
+
push(node, 'computed', index, node.childForFieldName('object'));
|
|
602
|
+
}
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (node.type === 'subscript') {
|
|
606
|
+
// `globals()[name]`: a lookup in this file's module namespace.
|
|
607
|
+
const value = node.childForFieldName('value');
|
|
608
|
+
const key = node.childForFieldName('subscript');
|
|
609
|
+
if (value && key && ns.subscriptOf.test(String(value.text || '').replace(/\s+/g, ''))) {
|
|
610
|
+
push(node, 'namespace', key, null, MODULE_RECEIVER);
|
|
611
|
+
}
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
const call = _calleeParts(node, wantMembers, wantQualified);
|
|
615
|
+
if (!call) return;
|
|
616
|
+
// Match on the callee spelling first; arguments are read only for a
|
|
617
|
+
// vocabulary hit (most calls in a hinted file are unrelated).
|
|
618
|
+
const calleeText = () => (call.calleeText ??= call.fn.text || '');
|
|
619
|
+
for (const rule of api.functions || []) {
|
|
620
|
+
if (!rule.callee.test(calleeText())) continue;
|
|
621
|
+
const args = _callArgs(node);
|
|
622
|
+
if (args && args[rule.nameArg]) {
|
|
623
|
+
push(node, rule.kind || calleeText().split('.').pop(),
|
|
624
|
+
args[rule.nameArg], args[rule.receiverArg]);
|
|
625
|
+
}
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (call.member && call.object) {
|
|
629
|
+
for (const rule of api.members || []) {
|
|
630
|
+
if (!rule.member.test(call.member)) continue;
|
|
631
|
+
const args = _callArgs(node);
|
|
632
|
+
if (args && args[rule.nameArg]) push(node, call.member, args[rule.nameArg], call.object);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
// `eval(cmd)`: the receiver is this file's own top-level namespace,
|
|
637
|
+
// so even a fully dynamic name is bounded to it.
|
|
638
|
+
if (ns?.calls && call.bare && ns.calls.test(call.calleeText)) {
|
|
639
|
+
const args = _callArgs(node);
|
|
640
|
+
if (args && args[0]) push(node, call.calleeText, args[0], null, MODULE_RECEIVER);
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
try {
|
|
644
|
+
const candidates = [];
|
|
645
|
+
if (useHints) {
|
|
646
|
+
const seen = new Set();
|
|
647
|
+
const re = new RegExp(hintPattern.source, 'g');
|
|
648
|
+
let m;
|
|
649
|
+
while ((m = re.exec(code)) !== null) {
|
|
650
|
+
let node = tree.rootNode.descendantForIndex(m.index);
|
|
651
|
+
for (let i = 0; node && i < 6; i++, node = node.parent) {
|
|
652
|
+
if (!CALL_NODE_TYPES.includes(node.type)) continue;
|
|
653
|
+
if (!seen.has(node.startIndex)) { seen.add(node.startIndex); candidates.push(node); }
|
|
654
|
+
// `globals()[name]`: the lookup is the enclosing subscript.
|
|
655
|
+
const parent = node.parent;
|
|
656
|
+
if (ns?.subscriptOf && parent?.type === 'subscript' &&
|
|
657
|
+
!seen.has(-parent.startIndex - 1)) {
|
|
658
|
+
seen.add(-parent.startIndex - 1);
|
|
659
|
+
candidates.push(parent);
|
|
660
|
+
}
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (types.length > 0) candidates.push(...tree.rootNode.descendantsOfType(types));
|
|
666
|
+
candidates.sort((a, b) => a.startIndex - b.startIndex || (a.endIndex - b.endIndex));
|
|
667
|
+
for (const node of candidates) visitNode(node);
|
|
668
|
+
} catch (_) { /* partial inventory is disclosed as-is */ }
|
|
669
|
+
return sites;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// ---------------------------------------------------------------------------
|
|
673
|
+
// Project matcher
|
|
674
|
+
// ---------------------------------------------------------------------------
|
|
675
|
+
|
|
676
|
+
function globMatches(pattern, name) {
|
|
677
|
+
const parts = pattern.split('*');
|
|
678
|
+
if (parts.length === 1) return pattern === name;
|
|
679
|
+
const first = parts[0];
|
|
680
|
+
const last = parts[parts.length - 1];
|
|
681
|
+
if (!name.startsWith(first) || !name.endsWith(last) ||
|
|
682
|
+
name.length < first.length + last.length) return false;
|
|
683
|
+
let pos = first.length;
|
|
684
|
+
const end = name.length - last.length;
|
|
685
|
+
for (let i = 1; i < parts.length - 1; i++) {
|
|
686
|
+
const at = name.indexOf(parts[i], pos);
|
|
687
|
+
if (at < 0 || at + parts[i].length > end) return false;
|
|
688
|
+
pos = at + parts[i].length;
|
|
689
|
+
}
|
|
690
|
+
return true;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function _longestFragment(pattern) {
|
|
694
|
+
let best = '';
|
|
695
|
+
for (const p of pattern.split('*')) if (p.length > best.length) best = p;
|
|
696
|
+
return best;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function _family(language) {
|
|
700
|
+
return langTraits(language)?.reflectionApi?.family || language;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Memoized project matcher over the reflection inventory: pattern entries
|
|
705
|
+
* grouped by their longest literal fragment, so a name probes only the
|
|
706
|
+
* fragments it contains (one Map lookup per substring length).
|
|
707
|
+
*/
|
|
708
|
+
function reflectionPatternIndex(index) {
|
|
709
|
+
if (index._reflectionPatternIndex) return index._reflectionPatternIndex;
|
|
710
|
+
const { projectReflectionSites } = require('./ast-analysis');
|
|
711
|
+
const byFile = projectReflectionSites(index);
|
|
712
|
+
const byFragment = new Map();
|
|
713
|
+
const lengths = new Set();
|
|
714
|
+
let entryCount = 0;
|
|
715
|
+
const moduleScoped = new Map();
|
|
716
|
+
for (const [file, sites] of byFile) {
|
|
717
|
+
const fe = index.files.get(file);
|
|
718
|
+
if (!fe) continue;
|
|
719
|
+
for (const site of sites) {
|
|
720
|
+
if (site.receiver?.kind === 'module') {
|
|
721
|
+
// Bounded to this file's top-level names: a fully dynamic
|
|
722
|
+
// name reaches every one of them.
|
|
723
|
+
const patterns = site.dynamic ? ['*'] : (site.patterns || (site.name ? [site.name] : []));
|
|
724
|
+
for (const pattern of patterns) {
|
|
725
|
+
if (!moduleScoped.has(file)) moduleScoped.set(file, []);
|
|
726
|
+
moduleScoped.get(file).push({ pattern, file, relativePath: fe.relativePath,
|
|
727
|
+
language: fe.language, family: _family(fe.language), site });
|
|
728
|
+
}
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
if (!site.patterns) continue;
|
|
732
|
+
for (const pattern of site.patterns) {
|
|
733
|
+
const frag = _longestFragment(pattern);
|
|
734
|
+
if (!frag) continue;
|
|
735
|
+
lengths.add(frag.length);
|
|
736
|
+
if (!byFragment.has(frag)) byFragment.set(frag, []);
|
|
737
|
+
byFragment.get(frag).push({ pattern, file, relativePath: fe.relativePath,
|
|
738
|
+
language: fe.language, family: _family(fe.language), site });
|
|
739
|
+
entryCount++;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
const result = { byFragment, lengths: [...lengths].sort((a, b) => a - b), entryCount, moduleScoped };
|
|
744
|
+
index._reflectionPatternIndex = result;
|
|
745
|
+
return result;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/** Pattern entries whose glob spells `name` (same language family as `language`). */
|
|
749
|
+
function patternEntriesFor(index, name, language, file = null) {
|
|
750
|
+
const pidx = reflectionPatternIndex(index);
|
|
751
|
+
if (!name) return [];
|
|
752
|
+
const out = [];
|
|
753
|
+
if (file && pidx.moduleScoped.has(file)) {
|
|
754
|
+
for (const entry of pidx.moduleScoped.get(file)) {
|
|
755
|
+
if (globMatches(entry.pattern, name)) out.push(entry);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (pidx.entryCount === 0) return out;
|
|
759
|
+
const family = language ? _family(language) : null;
|
|
760
|
+
const seenFrag = new Set();
|
|
761
|
+
for (const len of pidx.lengths) {
|
|
762
|
+
if (len > name.length) break;
|
|
763
|
+
for (let i = 0; i + len <= name.length; i++) {
|
|
764
|
+
const frag = name.substr(i, len);
|
|
765
|
+
if (seenFrag.has(frag)) continue;
|
|
766
|
+
seenFrag.add(frag);
|
|
767
|
+
const entries = pidx.byFragment.get(frag);
|
|
768
|
+
if (!entries) continue;
|
|
769
|
+
for (const entry of entries) {
|
|
770
|
+
if (family && entry.family !== family) continue;
|
|
771
|
+
if (globMatches(entry.pattern, name)) out.push(entry);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return out;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function _classRefFor(index, name, file, line, arity) {
|
|
779
|
+
const ci = require('./class-identity');
|
|
780
|
+
const info = ci.classDefsNamed(index, name);
|
|
781
|
+
if (info.entries.length === 0) return null;
|
|
782
|
+
if (line != null) {
|
|
783
|
+
const hit = info.entries.find(e => e.def.file === file &&
|
|
784
|
+
e.def.startLine <= line && (e.def.endLine || e.def.startLine) >= line);
|
|
785
|
+
if (hit) return { name, key: hit.key, def: hit.def };
|
|
786
|
+
}
|
|
787
|
+
return ci.resolveClassRef(index, name, file, Number.isInteger(arity) ? { arity } : {});
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function _reachesKey(index, fromRef, targetKey, targetName) {
|
|
791
|
+
const ci = require('./class-identity');
|
|
792
|
+
const visited = new Set();
|
|
793
|
+
const queue = [fromRef];
|
|
794
|
+
while (queue.length > 0) {
|
|
795
|
+
const cur = queue.shift();
|
|
796
|
+
const vk = cur.key || `n:${cur.name}`;
|
|
797
|
+
if (visited.has(vk) || visited.size > 256) continue;
|
|
798
|
+
visited.add(vk);
|
|
799
|
+
if (cur.key && cur.key === targetKey) return true;
|
|
800
|
+
if (!cur.key && cur.name === targetName && !cur.external) return true; // unresolved: maybe
|
|
801
|
+
if (cur.external) continue;
|
|
802
|
+
if (cur.key || cur.def) queue.push(...ci.parentRefsOf(index, cur));
|
|
803
|
+
}
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Could the reflective receiver of `entry` hold an instance whose class has
|
|
809
|
+
* member `def`? Untyped receivers answer yes; self/type receivers answer yes
|
|
810
|
+
* for the same class, an ancestor or descendant, and (multiple inheritance /
|
|
811
|
+
* mixins) classes sharing a descendant. Unresolved identities answer yes.
|
|
812
|
+
*/
|
|
813
|
+
function reflectionEntryReaches(index, entry, def) {
|
|
814
|
+
const site = entry.site;
|
|
815
|
+
if (site.membersOnly && !def.className && !def.receiver) return false;
|
|
816
|
+
const recv = site.receiver;
|
|
817
|
+
if (!recv) return true;
|
|
818
|
+
if (recv.kind === 'module') {
|
|
819
|
+
return def.file === entry.file && !def.className && !def.receiver;
|
|
820
|
+
}
|
|
821
|
+
let recvRef = null;
|
|
822
|
+
if (recv.kind === 'self') {
|
|
823
|
+
recvRef = _classRefFor(index, recv.className, entry.file, recv.classLine);
|
|
824
|
+
} else if (recv.kind === 'name') {
|
|
825
|
+
// An identifier naming a project class is a class receiver; any other
|
|
826
|
+
// identifier is an untyped value.
|
|
827
|
+
recvRef = _classRefFor(index, recv.name, entry.file, null);
|
|
828
|
+
if (!recvRef) return true;
|
|
829
|
+
} else if (recv.kind === 'type') {
|
|
830
|
+
recvRef = _classRefFor(index, recv.name, entry.file, null, recv.arity);
|
|
831
|
+
if (!recvRef) {
|
|
832
|
+
// An out-of-project type (`typeof(Convert)`): its members are not
|
|
833
|
+
// project code, but invoking a member it declares dispatches into
|
|
834
|
+
// project classes deriving from it (`typeof(IConvertible)`).
|
|
835
|
+
const { ownerDerivesFromExternal } = require('./contract-membership');
|
|
836
|
+
return ownerDerivesFromExternal(index, def, [recv.name]);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (!recvRef || !recvRef.key) return true;
|
|
840
|
+
const ci = require('./class-identity');
|
|
841
|
+
const ownerRef = ci.ownerRefOf(index, def);
|
|
842
|
+
// A class-level receiver never reaches a free function.
|
|
843
|
+
if (!ownerRef) return false;
|
|
844
|
+
if (!ownerRef.key) return true;
|
|
845
|
+
if (ownerRef.key === recvRef.key) return true;
|
|
846
|
+
if (_reachesKey(index, recvRef, ownerRef.key, ownerRef.name)) return true; // inherited member
|
|
847
|
+
if (_reachesKey(index, ownerRef, recvRef.key, recvRef.name)) return true; // subclass member
|
|
848
|
+
return ci.shareDescendant(index, recvRef, [def]);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/** Reaching pattern entries for one definition (sorted file, line). */
|
|
852
|
+
function reflectionPatternReferences(index, def) {
|
|
853
|
+
const lang = index.files.get(def.file)?.language;
|
|
854
|
+
const entries = patternEntriesFor(index, def.name, lang, def.file)
|
|
855
|
+
.filter(entry => reflectionEntryReaches(index, entry, def));
|
|
856
|
+
entries.sort((a, b) => (a.relativePath < b.relativePath ? -1 : a.relativePath > b.relativePath ? 1 : 0) ||
|
|
857
|
+
a.site.line - b.site.line || (a.pattern < b.pattern ? -1 : a.pattern > b.pattern ? 1 : 0));
|
|
858
|
+
return entries;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function resetReflectionIndex(index) {
|
|
862
|
+
index._reflectionPatternIndex = null;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
module.exports = {
|
|
866
|
+
WILD,
|
|
867
|
+
stringAlternatives,
|
|
868
|
+
classifyNameExpression,
|
|
869
|
+
classifyReceiver,
|
|
870
|
+
reflectionSitesInTree,
|
|
871
|
+
globMatches,
|
|
872
|
+
patternEntriesFor,
|
|
873
|
+
reflectionEntryReaches,
|
|
874
|
+
reflectionPatternReferences,
|
|
875
|
+
reflectionPatternIndex,
|
|
876
|
+
resetReflectionIndex,
|
|
877
|
+
};
|