ucn 5.4.2 → 5.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +27 -16
- package/cli/index.js +45 -14
- package/core/accessors.js +118 -3
- package/core/account.js +62 -11
- package/core/analysis.js +517 -266
- 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 +529 -31
- package/core/callers.js +12228 -921
- 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.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 +7 -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 +305 -122
- package/core/parallel-build.js +124 -47
- package/core/parser.js +3 -2
- package/core/project.js +472 -66
- 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 +3026 -0
- package/core/rust-modules.js +203 -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 +1185 -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 +2265 -112
- package/languages/utils.js +298 -4
- package/mcp/server.js +22 -8
- package/package.json +1 -1
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/cpp-scope.js - C++ qualified-name resolution for call qualifiers.
|
|
3
|
+
*
|
|
4
|
+
* A path call `Q::f(...)` names a member of a class when Q denotes a type and
|
|
5
|
+
* a namespace-scope function when Q denotes a namespace. The two AST shapes
|
|
6
|
+
* are identical (qualified_identifier), so the caller contract must decide
|
|
7
|
+
* what Q denotes before it may confirm or exclude anything:
|
|
8
|
+
*
|
|
9
|
+
* - `type`: Q resolves (through `using`/`typedef` aliases, template
|
|
10
|
+
* arguments stripped) to project class definitions
|
|
11
|
+
* - `namespace`: Q names a namespace the project declares and no visible
|
|
12
|
+
* type of that spelling exists
|
|
13
|
+
* - `unknown`: anything else (template parameters, external types,
|
|
14
|
+
* unresolved aliases); callers route such sites UNVERIFIED
|
|
15
|
+
*
|
|
16
|
+
* Namespaces come from the AST (`namespace a { ... }`) plus namespaces opened
|
|
17
|
+
* by object-like macros whose replacement list is a namespace opener
|
|
18
|
+
* (`#define LIB_BEGIN namespace lib { inline namespace v1 {`): the parser
|
|
19
|
+
* records each macro's namespace effect from its replacement tokens and each
|
|
20
|
+
* standalone macro line at namespace scope; this module replays those
|
|
21
|
+
* markers per file to give every definition its effective namespace.
|
|
22
|
+
*
|
|
23
|
+
* Everything is memoized on the index (`_cppScope`) and reset at build/load.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const { codeUnitCompare } = require('./shared');
|
|
27
|
+
|
|
28
|
+
const TYPE_KINDS = new Set(['class', 'struct', 'union', 'enum', 'interface']);
|
|
29
|
+
const MAX_ALIAS_HOPS = 8;
|
|
30
|
+
|
|
31
|
+
function _state(index) {
|
|
32
|
+
let state = index._cppScope;
|
|
33
|
+
if (!state) {
|
|
34
|
+
state = {
|
|
35
|
+
macroEffects: new Map(),
|
|
36
|
+
filePrefixes: new Map(),
|
|
37
|
+
inventory: null,
|
|
38
|
+
resolved: new Map(),
|
|
39
|
+
};
|
|
40
|
+
index._cppScope = state;
|
|
41
|
+
}
|
|
42
|
+
return state;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resetCppScope(index) {
|
|
46
|
+
index._cppScope = null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function _isCppFile(index, file) {
|
|
50
|
+
return index.files.get(file)?.language === 'cpp';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Files a translation unit rooted at `file` sees through its includes. */
|
|
54
|
+
function includeClosure(index, file) {
|
|
55
|
+
const state = _state(index);
|
|
56
|
+
if (!state.closures) state.closures = new Map();
|
|
57
|
+
const cached = state.closures.get(file);
|
|
58
|
+
if (cached) return cached;
|
|
59
|
+
const visible = new Set([file]);
|
|
60
|
+
const queue = [file];
|
|
61
|
+
while (queue.length > 0) {
|
|
62
|
+
const current = queue.shift();
|
|
63
|
+
for (const imported of index.importGraph?.get(current) || []) {
|
|
64
|
+
if (visible.has(imported)) continue;
|
|
65
|
+
visible.add(imported);
|
|
66
|
+
queue.push(imported);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
state.closures.set(file, visible);
|
|
70
|
+
return visible;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Namespace effect of macro NAME as seen from `file`: the definitions in the
|
|
75
|
+
* file or its include closure must agree on one effect (a project may
|
|
76
|
+
* redefine the macro for another configuration elsewhere). Disagreeing or
|
|
77
|
+
* unmodeled definitions are never scope evidence.
|
|
78
|
+
*/
|
|
79
|
+
function _macroEffect(index, name, file) {
|
|
80
|
+
const state = _state(index);
|
|
81
|
+
const cacheKey = `${name}\0${file}`;
|
|
82
|
+
if (state.macroEffects.has(cacheKey)) return state.macroEffects.get(cacheKey);
|
|
83
|
+
const visible = includeClosure(index, file);
|
|
84
|
+
let effect = null;
|
|
85
|
+
let key = null;
|
|
86
|
+
for (const definition of index.symbols.get(name) || []) {
|
|
87
|
+
if (definition.type !== 'macro' || !visible.has(definition.file)) continue;
|
|
88
|
+
const scope = definition.namespaceScope;
|
|
89
|
+
const scopeKey = scope ? JSON.stringify(scope) : '';
|
|
90
|
+
if (key === null) {
|
|
91
|
+
key = scopeKey;
|
|
92
|
+
effect = scope || null;
|
|
93
|
+
} else if (key !== scopeKey) {
|
|
94
|
+
effect = null;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (effect && (effect.opens || []).length > 0) {
|
|
99
|
+
effect = { ...effect, opens: effect.opens.map(group =>
|
|
100
|
+
group.flatMap(name => _expandedNamespaceName(index, name, visible))) };
|
|
101
|
+
}
|
|
102
|
+
state.macroEffects.set(cacheKey, effect);
|
|
103
|
+
return effect;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* A namespace name an opener spells with an object-like macro (`namespace
|
|
108
|
+
* LIB_NAMESPACE {` beside `#define LIB_NAMESPACE lib`) names the namespace the
|
|
109
|
+
* macro expands to (fix #396): the segments of its replacement list when every
|
|
110
|
+
* definition the file sees agrees on a (qualified) identifier. Otherwise the
|
|
111
|
+
* spelling itself.
|
|
112
|
+
*/
|
|
113
|
+
function _expandedNamespaceName(index, name, visible, depth = 0) {
|
|
114
|
+
if (depth > 4) return [name];
|
|
115
|
+
let body = null;
|
|
116
|
+
for (const definition of index.symbols.get(name) || []) {
|
|
117
|
+
if (definition.type !== 'macro' || !visible.has(definition.file)) continue;
|
|
118
|
+
if (Array.isArray(definition.ppParams) || typeof definition.ppBody !== 'string') return [name];
|
|
119
|
+
const text = definition.ppBody.replace(/\s+/g, '');
|
|
120
|
+
if (!/^[A-Za-z_]\w*(?:::[A-Za-z_]\w*)*$/.test(text)) return [name];
|
|
121
|
+
if (body === null) body = text;
|
|
122
|
+
else if (body !== text) return [name];
|
|
123
|
+
}
|
|
124
|
+
if (body === null || body === name) return [name];
|
|
125
|
+
return body.split('::').flatMap(segment => _expandedNamespaceName(index, segment, visible, depth + 1));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Line-ordered namespace prefixes opened by macro markers in `file`:
|
|
130
|
+
* `[{ line, prefix }]` where `prefix` applies to lines after `line`. Null
|
|
131
|
+
* when the markers do not balance (a close with nothing open): the file's
|
|
132
|
+
* macro scopes are then unknown and contribute nothing.
|
|
133
|
+
*/
|
|
134
|
+
function _filePrefixes(index, file) {
|
|
135
|
+
const state = _state(index);
|
|
136
|
+
if (state.filePrefixes.has(file)) return state.filePrefixes.get(file);
|
|
137
|
+
const entry = index.files.get(file);
|
|
138
|
+
const markers = entry?.macroScopeMarkers || [];
|
|
139
|
+
let transitions = [];
|
|
140
|
+
const stack = [];
|
|
141
|
+
for (const marker of markers) {
|
|
142
|
+
const effect = _macroEffect(index, marker.name, file);
|
|
143
|
+
if (!effect) continue;
|
|
144
|
+
if (effect.closes > 0) {
|
|
145
|
+
if (effect.closes > stack.length) {
|
|
146
|
+
transitions = null;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
stack.length -= effect.closes;
|
|
150
|
+
}
|
|
151
|
+
for (const group of effect.opens || []) stack.push(group);
|
|
152
|
+
transitions.push({ line: marker.line, prefix: stack.flat().join('::') });
|
|
153
|
+
}
|
|
154
|
+
state.filePrefixes.set(file, transitions);
|
|
155
|
+
return transitions;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function _macroPrefixAt(index, file, line) {
|
|
159
|
+
const transitions = _filePrefixes(index, file);
|
|
160
|
+
if (!transitions) return '';
|
|
161
|
+
let prefix = '';
|
|
162
|
+
for (const transition of transitions) {
|
|
163
|
+
if (transition.line >= line) break;
|
|
164
|
+
prefix = transition.prefix;
|
|
165
|
+
}
|
|
166
|
+
return prefix;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Effective namespace of a definition: macro-opened prefix + AST namespace. */
|
|
170
|
+
function effectiveNamespace(index, definition) {
|
|
171
|
+
if (!definition?.file) return definition?.namespace || '';
|
|
172
|
+
const prefix = _macroPrefixAt(index, definition.file, definition.startLine);
|
|
173
|
+
const own = definition.namespace || '';
|
|
174
|
+
return prefix && own ? `${prefix}::${own}` : prefix || own;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Effective namespace enclosing an arbitrary source position. */
|
|
178
|
+
function namespaceAt(index, file, line) {
|
|
179
|
+
const prefix = _macroPrefixAt(index, file, line);
|
|
180
|
+
const entry = index.files.get(file);
|
|
181
|
+
let own = '';
|
|
182
|
+
let ownSpan = Infinity;
|
|
183
|
+
// The innermost definition containing the line carries the AST
|
|
184
|
+
// namespace of that position.
|
|
185
|
+
for (const symbol of entry?.symbols || []) {
|
|
186
|
+
if (symbol.startLine > line || symbol.endLine < line) continue;
|
|
187
|
+
const span = symbol.endLine - symbol.startLine;
|
|
188
|
+
if (span < ownSpan) {
|
|
189
|
+
ownSpan = span;
|
|
190
|
+
own = symbol.namespace || '';
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return prefix && own ? `${prefix}::${own}` : prefix || own;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function _inventory(index) {
|
|
197
|
+
const state = _state(index);
|
|
198
|
+
if (state.inventory) return state.inventory;
|
|
199
|
+
const namespaces = new Set();
|
|
200
|
+
for (const entry of index.files.values()) {
|
|
201
|
+
if (entry.language !== 'cpp') continue;
|
|
202
|
+
for (const symbol of entry.symbols || []) {
|
|
203
|
+
const namespace = effectiveNamespace(index, symbol);
|
|
204
|
+
if (!namespace) continue;
|
|
205
|
+
const parts = namespace.split('::');
|
|
206
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
207
|
+
namespaces.add(parts.slice(0, i).join('::'));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
state.inventory = namespaces;
|
|
212
|
+
return namespaces;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Remove template argument lists (depth-aware) and whitespace. */
|
|
216
|
+
function stripTemplateArguments(text) {
|
|
217
|
+
let plain = '';
|
|
218
|
+
let depth = 0;
|
|
219
|
+
for (const character of String(text || '')) {
|
|
220
|
+
if (character === '<') {
|
|
221
|
+
depth++;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (character === '>') {
|
|
225
|
+
depth = Math.max(0, depth - 1);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (depth === 0) plain += character;
|
|
229
|
+
}
|
|
230
|
+
// A leading keyword is a separate token (`typename T::x`, `struct S`);
|
|
231
|
+
// a name that merely starts with its letters (`classify_object`,
|
|
232
|
+
// `constant`) is not (fix #386).
|
|
233
|
+
return plain.replace(/^\s*(?:typename|class|struct|const)\s+/, '').replace(/\s+/g, '');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** `A<x::y>::B` -> ['A<x::y>', 'B']: `::` outside template arguments. */
|
|
237
|
+
function _splitScope(text) {
|
|
238
|
+
const parts = [];
|
|
239
|
+
let depth = 0;
|
|
240
|
+
let current = '';
|
|
241
|
+
const value = String(text || '');
|
|
242
|
+
for (let i = 0; i < value.length; i++) {
|
|
243
|
+
const character = value[i];
|
|
244
|
+
if (character === '<') depth++;
|
|
245
|
+
else if (character === '>') depth = Math.max(0, depth - 1);
|
|
246
|
+
if (depth === 0 && character === ':' && value[i + 1] === ':') {
|
|
247
|
+
if (current) parts.push(current.trim());
|
|
248
|
+
current = '';
|
|
249
|
+
i++;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
current += character;
|
|
253
|
+
}
|
|
254
|
+
if (current.trim()) parts.push(current.trim());
|
|
255
|
+
return parts;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function _enclosingPrefixes(namespace) {
|
|
259
|
+
const parts = namespace ? namespace.split('::') : [];
|
|
260
|
+
const prefixes = [];
|
|
261
|
+
for (let i = parts.length; i >= 0; i--) prefixes.push(parts.slice(0, i).join('::'));
|
|
262
|
+
return prefixes;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function _join(left, right) {
|
|
266
|
+
return left && right ? `${left}::${right}` : left || right;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function _scopeOf(index, definition) {
|
|
270
|
+
return _join(effectiveNamespace(index, definition), definition.enclosingType || '');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function _typeCandidates(index, name) {
|
|
274
|
+
return (index.symbols.get(name) || []).filter(definition =>
|
|
275
|
+
_isCppFile(index, definition.file) &&
|
|
276
|
+
(TYPE_KINDS.has(definition.type) ||
|
|
277
|
+
(definition.type === 'type' && definition.aliasOf)));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Using-declarations, using-directives and namespace aliases in scope at a
|
|
282
|
+
* source position: the position's own file (earlier lines, block scope
|
|
283
|
+
* respected) plus namespace-scope facts of every header it includes.
|
|
284
|
+
*/
|
|
285
|
+
function _usingsInScope(index, context, visibleFiles) {
|
|
286
|
+
const out = [];
|
|
287
|
+
const take = (file, fact) => {
|
|
288
|
+
if (file === context.file) {
|
|
289
|
+
if (fact.line > context.line) return;
|
|
290
|
+
if (fact.scopeStartLine != null &&
|
|
291
|
+
(context.line < fact.scopeStartLine || context.line > fact.scopeEndLine)) return;
|
|
292
|
+
} else if (fact.scopeStartLine != null) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const state = _state(index);
|
|
296
|
+
if (!state.expandedFacts) state.expandedFacts = new WeakMap();
|
|
297
|
+
let expanded = state.expandedFacts.get(fact);
|
|
298
|
+
if (!expanded) {
|
|
299
|
+
expanded = _withExpandedTarget(index, { ...fact, file });
|
|
300
|
+
state.expandedFacts.set(fact, expanded);
|
|
301
|
+
}
|
|
302
|
+
out.push(expanded);
|
|
303
|
+
};
|
|
304
|
+
for (const fact of index.files.get(context.file)?.cppUsings || []) take(context.file, fact);
|
|
305
|
+
for (const file of visibleFiles || []) {
|
|
306
|
+
if (file === context.file) continue;
|
|
307
|
+
for (const fact of index.files.get(file)?.cppUsings || []) take(file, fact);
|
|
308
|
+
}
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* A using fact whose target spells a namespace through an object-like macro
|
|
314
|
+
* (`using namespace LIB_NAMESPACE;`, `using LIB_NAMESPACE::f;`) names what
|
|
315
|
+
* the macro expands to where the directive is written (fix #396). The last
|
|
316
|
+
* segment of a using-declaration is the declared name, never expanded.
|
|
317
|
+
*/
|
|
318
|
+
function _withExpandedTarget(index, fact) {
|
|
319
|
+
const target = typeof fact.target === 'string' ? fact.target : null;
|
|
320
|
+
if (!target || !/[A-Za-z_]/.test(target)) return fact;
|
|
321
|
+
const global = target.startsWith('::');
|
|
322
|
+
const segments = target.replace(/^::/, '').split('::').filter(Boolean);
|
|
323
|
+
if (segments.some(part => !/^[A-Za-z_]\w*$/.test(part))) return fact;
|
|
324
|
+
const keepLast = fact.kind === 'declaration' && segments.length > 1;
|
|
325
|
+
const head = keepLast ? segments.slice(0, -1) : segments;
|
|
326
|
+
if (!head.some(part => (index.symbols.get(part) || []).some(definition => definition.type === 'macro'))) {
|
|
327
|
+
return fact;
|
|
328
|
+
}
|
|
329
|
+
const visible = includeClosure(index, fact.file);
|
|
330
|
+
const expanded = [...head.flatMap(part => _expandedNamespaceName(index, part, visible)),
|
|
331
|
+
...(keepLast ? [segments[segments.length - 1]] : [])];
|
|
332
|
+
if (expanded.join('::') === segments.join('::')) return fact;
|
|
333
|
+
return { ...fact, target: `${global ? '::' : ''}${expanded.join('::')}` };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Can `::name` (qualified lookup in the global namespace) reach a
|
|
338
|
+
* declaration in namespace `targetNamespace` (fix #393)? Only through a
|
|
339
|
+
* using-directive or using-declaration at global namespace scope visible at
|
|
340
|
+
* the site: 'no' when none can nominate it, 'maybe' when one may.
|
|
341
|
+
*/
|
|
342
|
+
function globalQualifiedReaches(index, context, targetNamespace, name, visibleFiles) {
|
|
343
|
+
if (!targetNamespace) return 'yes';
|
|
344
|
+
const target = targetNamespace.split('::');
|
|
345
|
+
for (const fact of _usingsInScope(index, context, visibleFiles)) {
|
|
346
|
+
if (fact.scopeStartLine != null) continue;
|
|
347
|
+
const where = _join(_macroPrefixAt(index, fact.file, fact.line), fact.namespace || '');
|
|
348
|
+
if (where) continue;
|
|
349
|
+
const spelled = String(fact.target || '').replace(/^::/, '').split('::').filter(Boolean);
|
|
350
|
+
if (fact.kind === 'directive') {
|
|
351
|
+
// `using namespace a::b;` nominates the namespace its (possibly
|
|
352
|
+
// partial) spelling resolves to: any suffix of the target.
|
|
353
|
+
const tail = target.slice(-spelled.length);
|
|
354
|
+
if (spelled.length > 0 && tail.join('::') === spelled.join('::')) return 'maybe';
|
|
355
|
+
} else if (fact.kind === 'declaration' && fact.name === name) {
|
|
356
|
+
return 'maybe';
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return 'no';
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function _factContext(index, fact, origin) {
|
|
363
|
+
return {
|
|
364
|
+
file: fact.file,
|
|
365
|
+
line: fact.line,
|
|
366
|
+
namespace: _join(_macroPrefixAt(index, fact.file, fact.line), fact.namespace || ''),
|
|
367
|
+
className: null,
|
|
368
|
+
origin,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Resolve a (possibly qualified) type spelling from a source context to the
|
|
374
|
+
* class definitions it denotes.
|
|
375
|
+
* @returns {{kind:'type', classes:object[], visible:boolean}|{kind:'namespace', namespace:string}|{kind:'unknown', why:string}}
|
|
376
|
+
*/
|
|
377
|
+
function resolveQualifier(index, context, spelling, visibleFiles, depth = 0, options = {}) {
|
|
378
|
+
// `declarations` (fix #386): report the declarations name lookup finds
|
|
379
|
+
// for the last segment (a class, or a typedef/alias of that spelling)
|
|
380
|
+
// instead of the classes an alias denotes.
|
|
381
|
+
const declarationsOnly = options.declarations === true;
|
|
382
|
+
const plain = stripTemplateArguments(spelling);
|
|
383
|
+
const global = plain.startsWith('::');
|
|
384
|
+
let segments = plain.replace(/^::/, '').split('::').filter(Boolean);
|
|
385
|
+
if (segments.length === 0 || segments.some(part => !/^[A-Za-z_]\w*$/.test(part))) {
|
|
386
|
+
return { kind: 'unknown', why: 'unparsed-qualifier' };
|
|
387
|
+
}
|
|
388
|
+
// A qualifier segment spelled by an object-like macro
|
|
389
|
+
// (`LIB_NAMESPACE::detail::X`) is the name the macro expands to, as in
|
|
390
|
+
// macro-opened namespaces (fix #396).
|
|
391
|
+
if (segments.length > 1 && visibleFiles && segments.slice(0, -1).some(part =>
|
|
392
|
+
(index.symbols.get(part) || []).some(definition => definition.type === 'macro'))) {
|
|
393
|
+
segments = [...segments.slice(0, -1).flatMap(part => _expandedNamespaceName(index, part, visibleFiles)),
|
|
394
|
+
segments[segments.length - 1]];
|
|
395
|
+
}
|
|
396
|
+
if (depth > MAX_ALIAS_HOPS) return { kind: 'unknown', why: 'alias-depth' };
|
|
397
|
+
const origin = context.origin || context.file;
|
|
398
|
+
const cacheKey = `${origin}\0${context.file}\0${context.line}\0${context.className || ''}\0${plain}` +
|
|
399
|
+
`${declarationsOnly ? '\0d' : ''}${context.namespace != null ? `\0ns:${context.namespace}` : ''}`;
|
|
400
|
+
const state = _state(index);
|
|
401
|
+
if (state.resolved.has(cacheKey)) return state.resolved.get(cacheKey);
|
|
402
|
+
const finish = result => {
|
|
403
|
+
state.resolved.set(cacheKey, result);
|
|
404
|
+
return result;
|
|
405
|
+
};
|
|
406
|
+
const name = segments[segments.length - 1];
|
|
407
|
+
const qualifier = segments.slice(0, -1).join('::');
|
|
408
|
+
const contextNamespace = context.namespace ??
|
|
409
|
+
namespaceAt(index, context.file, context.line);
|
|
410
|
+
const usings = global ? [] : _usingsInScope(index, context, visibleFiles);
|
|
411
|
+
|
|
412
|
+
// A using-declaration or namespace alias that introduces the first
|
|
413
|
+
// segment rewrites it to the introduced entity, resolved where the
|
|
414
|
+
// declaration sits.
|
|
415
|
+
const introducers = usings.filter(fact =>
|
|
416
|
+
(fact.kind === 'declaration' || fact.kind === 'namespace-alias') &&
|
|
417
|
+
fact.name === segments[0]);
|
|
418
|
+
if (introducers.length > 0) {
|
|
419
|
+
const targets = new Set(introducers.map(fact => fact.target));
|
|
420
|
+
if (targets.size !== 1) return finish({ kind: 'unknown', why: 'ambiguous-using' });
|
|
421
|
+
const fact = introducers[0];
|
|
422
|
+
const rewritten = [fact.target, ...segments.slice(1)].join('::');
|
|
423
|
+
return finish(resolveQualifier(index, _factContext(index, fact, origin),
|
|
424
|
+
rewritten, visibleFiles, depth + 1, options));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const scopes = global ? [''] : _enclosingPrefixes(contextNamespace);
|
|
428
|
+
if (!global && context.className) {
|
|
429
|
+
// Member types of the enclosing class are found before namespace
|
|
430
|
+
// scope, then those of each lexically enclosing class, innermost
|
|
431
|
+
// first (fix #396: a nested class body or an out-of-line member of
|
|
432
|
+
// `SkipList<K, C>::Node` sees Node's and SkipList's member types). A
|
|
433
|
+
// declaration's scope names its immediate class; out-of-line template
|
|
434
|
+
// members spell that class with arguments.
|
|
435
|
+
const classScopes = [_join(contextNamespace, context.className)];
|
|
436
|
+
const chain = _splitScope(context.className);
|
|
437
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
438
|
+
classScopes.push(_join(contextNamespace, chain[i]));
|
|
439
|
+
const plainClass = stripTemplateArguments(chain[i]);
|
|
440
|
+
if (plainClass) classScopes.push(_join(contextNamespace, plainClass));
|
|
441
|
+
}
|
|
442
|
+
scopes.unshift(...new Set(classScopes));
|
|
443
|
+
}
|
|
444
|
+
// using-directives make a namespace's members visible to unqualified
|
|
445
|
+
// lookup of the first segment.
|
|
446
|
+
for (const fact of usings) {
|
|
447
|
+
if (fact.kind !== 'directive') continue;
|
|
448
|
+
const target = fact.target.replace(/^::/, '');
|
|
449
|
+
const factNamespace = _factContext(index, fact, origin).namespace;
|
|
450
|
+
for (const prefix of _enclosingPrefixes(factNamespace)) {
|
|
451
|
+
const full = _join(prefix, target);
|
|
452
|
+
if (!scopes.includes(full)) scopes.push(full);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const candidates = _typeCandidates(index, name);
|
|
456
|
+
let matched = [];
|
|
457
|
+
// Innermost scope wins (C++ unqualified and qualified lookup both stop
|
|
458
|
+
// at the first scope that declares the name).
|
|
459
|
+
for (const scope of scopes) {
|
|
460
|
+
const want = _join(scope, qualifier);
|
|
461
|
+
matched = candidates.filter(definition => {
|
|
462
|
+
if (_scopeOf(index, definition) !== want) return false;
|
|
463
|
+
// Block-scope aliases are visible only inside their own block.
|
|
464
|
+
if (definition.lexicalScopeStartLine != null &&
|
|
465
|
+
definition.lexicalScopeEndLine != null &&
|
|
466
|
+
!(definition.type === 'type' && definition.enclosingType)) {
|
|
467
|
+
return definition.file === context.file &&
|
|
468
|
+
definition.lexicalScopeStartLine <= context.line &&
|
|
469
|
+
context.line <= definition.lexicalScopeEndLine;
|
|
470
|
+
}
|
|
471
|
+
return true;
|
|
472
|
+
});
|
|
473
|
+
if (matched.length > 0) break;
|
|
474
|
+
}
|
|
475
|
+
if (matched.length === 0) {
|
|
476
|
+
const full = segments.join('::');
|
|
477
|
+
const scope = scopes.find(candidate => _inventory(index).has(_join(candidate, full)));
|
|
478
|
+
if (scope !== undefined) {
|
|
479
|
+
// `resolvedNamespace` (fix #393): the namespace the spelling
|
|
480
|
+
// names from this scope (innermost first).
|
|
481
|
+
return finish({ kind: 'namespace', namespace: full, resolvedNamespace: _join(scope, full) });
|
|
482
|
+
}
|
|
483
|
+
return finish({ kind: 'unknown', why: 'no-declaration' });
|
|
484
|
+
}
|
|
485
|
+
// Only declarations the translation unit can see are in scope. A
|
|
486
|
+
// qualified spelling may still name a declaration behind an include
|
|
487
|
+
// the resolver could not follow; that match is reported as not visible.
|
|
488
|
+
const visible = matched.filter(definition =>
|
|
489
|
+
definition.file === context.file || definition.file === origin ||
|
|
490
|
+
visibleFiles?.has(definition.file));
|
|
491
|
+
if (visible.length === 0 && !qualifier) {
|
|
492
|
+
return finish({ kind: 'unknown', why: 'not-visible' });
|
|
493
|
+
}
|
|
494
|
+
const chosen = visible.length > 0 ? visible : matched;
|
|
495
|
+
if (declarationsOnly) {
|
|
496
|
+
return finish({ kind: 'type', declarations: chosen, visible: visible.length > 0 });
|
|
497
|
+
}
|
|
498
|
+
const classes = [];
|
|
499
|
+
for (const definition of chosen) {
|
|
500
|
+
if (definition.type !== 'type') {
|
|
501
|
+
classes.push(definition);
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
const target = resolveQualifier(index, {
|
|
505
|
+
file: definition.file,
|
|
506
|
+
line: definition.startLine,
|
|
507
|
+
namespace: effectiveNamespace(index, definition),
|
|
508
|
+
className: definition.enclosingType || null,
|
|
509
|
+
origin,
|
|
510
|
+
}, definition.aliasOf, visibleFiles, depth + 1);
|
|
511
|
+
if (target.kind !== 'type') {
|
|
512
|
+
return finish({ kind: 'unknown', why: `alias-${target.why || target.kind}` });
|
|
513
|
+
}
|
|
514
|
+
classes.push(...target.classes);
|
|
515
|
+
}
|
|
516
|
+
const unique = [...new Map(classes.map(definition =>
|
|
517
|
+
[`${definition.file}\0${definition.startLine}\0${definition.name}`, definition])).values()]
|
|
518
|
+
.sort((a, b) => codeUnitCompare(a.file, b.file) || a.startLine - b.startLine);
|
|
519
|
+
return finish({ kind: 'type', classes: unique, visible: visible.length > 0 });
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
module.exports = {
|
|
523
|
+
macroPrefixAt: _macroPrefixAt,
|
|
524
|
+
globalQualifiedReaches,
|
|
525
|
+
resetCppScope,
|
|
526
|
+
includeClosure,
|
|
527
|
+
macroNamespacePrefixAt: _macroPrefixAt,
|
|
528
|
+
effectiveNamespace,
|
|
529
|
+
namespaceAt,
|
|
530
|
+
resolveQualifier,
|
|
531
|
+
stripTemplateArguments,
|
|
532
|
+
};
|