ucn 5.5.1 → 5.5.2
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 -9
- package/.claude/skills/ucn/references/commands.md +38 -16
- package/.claude/skills/ucn/references/resolution-rules.md +126 -20
- package/.claude/skills/ucn/references/trust-contract.md +5 -3
- package/README.md +30 -21
- package/cli/index.js +1 -1
- package/core/accessors.js +341 -54
- package/core/account.js +74 -20
- package/core/analysis.js +257 -125
- package/core/ast-analysis.js +12 -3
- package/core/brief.js +8 -4
- package/core/build-worker.js +2 -1
- package/core/cache.js +137 -6
- package/core/call-name-cache.js +134 -0
- package/core/callable-scope.js +21 -0
- package/core/callers.js +4726 -940
- package/core/check.js +22 -5
- package/core/class-identity.js +399 -12
- package/core/constructor-calls.js +831 -0
- package/core/constructor-hierarchy.js +147 -0
- package/core/constructor-protocol.js +196 -0
- package/core/contract-membership.js +346 -140
- package/core/cpp-conversion-constraints.js +123 -0
- package/core/cpp-conversion-syntax.js +216 -0
- package/core/cpp-conversions.js +414 -0
- package/core/cpp-scope.js +245 -44
- package/core/deadcode.js +35 -15
- package/core/decorator-binding.js +137 -3
- package/core/enum-reference-syntax.js +134 -0
- package/core/execute.js +17 -1
- package/core/external-macros.js +42 -10
- package/core/go-packages.js +171 -0
- package/core/graph-build.js +2 -0
- package/core/graph.js +75 -59
- package/core/ignore-rules.js +2 -1
- package/core/imports.js +145 -19
- package/core/index-ir.js +41 -4
- package/core/ir.js +27 -6
- package/core/macro-expansion.js +38 -2
- package/core/out-of-class-members.js +182 -0
- package/core/output/analysis-ext.js +4 -2
- package/core/output/analysis.js +89 -20
- package/core/output/graph.js +12 -3
- package/core/output/lines.js +4 -3
- package/core/output/public.js +1 -0
- package/core/output/reporting.js +6 -1
- package/core/output/shared.js +2 -0
- package/core/output/tracing.js +28 -22
- package/core/output-budget.js +2 -2
- package/core/parallel-build.js +52 -1
- package/core/project.js +217 -112
- package/core/provenance-constructor-base.js +161 -0
- package/core/provenance-facts.js +257 -27
- package/core/provenance-overload.js +458 -54
- package/core/provenance-type.js +487 -0
- package/core/provenance.js +234 -15
- package/core/public-command.js +17 -2
- package/core/query-bodies.js +159 -0
- package/core/reflection.js +255 -2
- package/core/rename-token-syntax.js +111 -0
- package/core/reporting.js +13 -7
- package/core/rust-macro-expansion.js +151 -19
- package/core/rust-modules.js +1 -1
- package/core/rust-names.js +607 -0
- package/core/search.js +70 -17
- package/core/shared.js +98 -1
- package/core/source-memo.js +201 -0
- package/core/text-usage-syntax.js +38 -0
- package/core/tracing.js +7 -1
- package/core/type-denotation.js +124 -37
- package/core/type-references.js +615 -246
- package/core/value-references.js +2777 -0
- package/core/verify.js +1097 -281
- package/languages/adapter.js +28 -4
- package/languages/c-family.js +1118 -164
- package/languages/constructor-bases.js +105 -0
- package/languages/cpp-platform.js +26 -0
- package/languages/cpp-site-syntax.js +83 -0
- package/languages/csharp-scope.js +439 -0
- package/languages/csharp.js +428 -68
- package/languages/go.js +385 -207
- package/languages/html.js +4 -2
- package/languages/index.js +397 -2
- package/languages/java-platform.js +226 -0
- package/languages/java-unicode.js +269 -0
- package/languages/java.js +461 -117
- package/languages/javascript.js +418 -66
- package/languages/lexical-scope.js +190 -23
- package/languages/python.js +404 -54
- package/languages/rust.js +510 -82
- package/languages/utils.js +67 -8
- package/mcp/server.js +8 -2
- package/package.json +2 -2
|
@@ -0,0 +1,2777 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { langTraits, LANGUAGES, getParser, safeParse } = require('../languages');
|
|
4
|
+
const { sameNode, visitNameNodes } = require('../languages/utils');
|
|
5
|
+
const { referenceScope, pythonClassBodyOrder } = require('../languages/lexical-scope');
|
|
6
|
+
const { TypeReferenceResolver, referenceRole, referenceBindingScope, referenceTypeParameterScope,
|
|
7
|
+
typeReferenceSites, referenceImportVerdict } = require('./type-references');
|
|
8
|
+
const { ownerRefOf, classDefsNamed, findDeclaringClass, relationToTargets, descendsFromTargets, shareDescendant,
|
|
9
|
+
parentRefsOf, supertypeRefsOf, lexicalClassRefOf, completeForwardDeclaration } = require('./class-identity');
|
|
10
|
+
const { isValueDefinition } = require('./shared');
|
|
11
|
+
const TYPE_BODIES = new Set(['class_declaration', 'class_definition', 'class',
|
|
12
|
+
'abstract_class_declaration', 'struct_specifier', 'class_specifier', 'union_specifier',
|
|
13
|
+
'struct_item', 'enum_item', 'enum_declaration', 'enum_specifier',
|
|
14
|
+
'struct_declaration', 'record_declaration', 'interface_declaration', 'union_item', 'type_spec',
|
|
15
|
+
'annotation_type_declaration', 'record_struct_declaration']);
|
|
16
|
+
const MEMBER_NODES = new Set(['attribute', 'member_expression', 'member_access_expression',
|
|
17
|
+
'member_binding_expression', 'field_expression', 'field_access', 'selector_expression']);
|
|
18
|
+
const ROOTS = new Set(['program', 'module', 'source_file', 'translation_unit', 'compilation_unit']);
|
|
19
|
+
const TYPE_NAMES = new Set(['identifier', 'type_identifier', 'namespace_identifier']);
|
|
20
|
+
const TYPE_WRAPPERS = new Set(['type', 'type_annotation', 'reference_type', 'pointer_type',
|
|
21
|
+
'nullable_type', 'parenthesized_type', 'type_descriptor']);
|
|
22
|
+
const DECLARATIONS = new Set(['parameter', 'formal_parameter', 'spread_parameter',
|
|
23
|
+
'required_parameter', 'optional_parameter', 'typed_parameter', 'parameter_declaration',
|
|
24
|
+
'variable_declarator', 'let_declaration', 'assignment', 'var_spec', 'const_spec',
|
|
25
|
+
'declaration', 'local_declaration_statement', 'field_declaration',
|
|
26
|
+
'field_definition', 'public_field_definition', 'short_var_declaration']);
|
|
27
|
+
const GRAMMAR_LANGUAGES = new Map(Object.values(LANGUAGES).map(config =>
|
|
28
|
+
[config.treeSitterLang, config.name]));
|
|
29
|
+
const TYPE_KINDS = new Set(['class', 'struct', 'interface', 'trait', 'enum', 'record',
|
|
30
|
+
'union', 'type', 'namespace', 'impl']);
|
|
31
|
+
const FUNCTION_NODES = new Set(['function', 'function_expression', 'function_declaration',
|
|
32
|
+
'generator_function', 'generator_function_declaration']);
|
|
33
|
+
const CALLABLE_SCOPES = new Set([...FUNCTION_NODES, 'function_definition', 'method_definition',
|
|
34
|
+
'method_declaration', 'constructor_declaration', 'lambda', 'lambda_expression',
|
|
35
|
+
'arrow_function', 'closure_expression', 'function_item']);
|
|
36
|
+
const WRITE_EXPRESSIONS = new Set(['assignment', 'assignment_expression', 'assignment_statement',
|
|
37
|
+
'augmented_assignment', 'augmented_assignment_expression', 'compound_assignment_expr',
|
|
38
|
+
'update_expression', 'inc_statement', 'dec_statement']);
|
|
39
|
+
|
|
40
|
+
/** One answer for several bindings of a name (jsBindingHolds). */
|
|
41
|
+
function mergeHolds(holds) {
|
|
42
|
+
if (holds.length > 0 && holds.every(hold => hold === 'value')) return 'value';
|
|
43
|
+
if (holds.length > 0 && holds.every(hold => hold?.modules)) {
|
|
44
|
+
return { modules: [...new Set(holds.flatMap(hold => hold.modules))] };
|
|
45
|
+
}
|
|
46
|
+
return 'unknown';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Reads of an instance member while evaluating the RHS of its first
|
|
50
|
+
* recorded write. The write has not happened yet, so the old value may be
|
|
51
|
+
* inherited or supplied by an earlier runtime initialization. */
|
|
52
|
+
function instanceAttributeInitializerReads(index, definition) {
|
|
53
|
+
if (!definition?.instanceAttribute) return [];
|
|
54
|
+
const memo = index._opMemo?.('instanceAttributeInitializerReads', () => new WeakMap());
|
|
55
|
+
if (memo?.has(definition)) return memo.get(definition);
|
|
56
|
+
const file = definition.file;
|
|
57
|
+
let language = index.files.get(file)?.language;
|
|
58
|
+
let source;
|
|
59
|
+
try {
|
|
60
|
+
source = index._readFile(file);
|
|
61
|
+
} catch {
|
|
62
|
+
// A file removed since indexing is no longer in the observed
|
|
63
|
+
// source universe, as in computeGroundSet.
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
const embedded = langTraits(language)?.embeddedSyntax;
|
|
67
|
+
if (embedded) {
|
|
68
|
+
source = embedded.source(source, getParser(language));
|
|
69
|
+
language = embedded.language;
|
|
70
|
+
}
|
|
71
|
+
const tree = embedded ? require('./source-memo').parsedSource(language, source)
|
|
72
|
+
: index._getParsedTree(file, source, language) || safeParse(getParser(language), source);
|
|
73
|
+
const reads = [];
|
|
74
|
+
if (tree) visitNameNodes(tree, source, definition.name, node => {
|
|
75
|
+
if (node.startPosition.row + 1 !== (definition.nameLine || definition.startLine)) return true;
|
|
76
|
+
const member = node.parent;
|
|
77
|
+
if (!['attribute', 'member_expression'].includes(member?.type)) return true;
|
|
78
|
+
const name = member.childForFieldName('attribute') || member.childForFieldName('property');
|
|
79
|
+
const receiver = member.childForFieldName('object');
|
|
80
|
+
if (!name || !sameNode(name, node) || !receiver) return true;
|
|
81
|
+
const assignment = member.parent;
|
|
82
|
+
if (!['assignment', 'assignment_expression'].includes(assignment?.type) ||
|
|
83
|
+
!sameNode(assignment.childForFieldName('left'), member)) return true;
|
|
84
|
+
const right = assignment.childForFieldName('right');
|
|
85
|
+
const stack = right ? [right] : [];
|
|
86
|
+
while (stack.length) {
|
|
87
|
+
const current = stack.pop();
|
|
88
|
+
if (CALLABLE_SCOPES.has(current.type) || TYPE_BODIES.has(current.type) ||
|
|
89
|
+
current.type === 'generator_expression') continue;
|
|
90
|
+
if (['attribute', 'member_expression'].includes(current.type)) {
|
|
91
|
+
const property = current.childForFieldName('attribute') || current.childForFieldName('property');
|
|
92
|
+
const object = current.childForFieldName('object');
|
|
93
|
+
if (property?.text === definition.name && object?.type === receiver.type &&
|
|
94
|
+
object.text === receiver.text) reads.push({ line: property.startPosition.row + 1,
|
|
95
|
+
column: property.startPosition.column, start: property.startIndex, end: property.endIndex });
|
|
96
|
+
}
|
|
97
|
+
stack.push(...current.namedChildren);
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
});
|
|
101
|
+
reads.sort((a, b) => a.start - b.start);
|
|
102
|
+
memo?.set(definition, reads);
|
|
103
|
+
return reads;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The template-id a name token heads (`v` in `v<T>`), else the token. */
|
|
107
|
+
function templateIdOf(node) {
|
|
108
|
+
const parent = node?.parent;
|
|
109
|
+
return parent && ['template_function', 'template_method'].includes(parent.type) &&
|
|
110
|
+
sameNode(parent.childForFieldName('name'), node) ? parent : node;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The Python class whose body a node is directly in (not in a nested
|
|
114
|
+
* function, lambda or comprehension body), or null. */
|
|
115
|
+
function pythonClassBodyOf(node) {
|
|
116
|
+
let child = node;
|
|
117
|
+
for (let scope = node.parent; scope; child = scope, scope = scope.parent) {
|
|
118
|
+
if (scope.type === 'function_definition' || scope.type === 'lambda') {
|
|
119
|
+
if (sameNode(scope.childForFieldName('body'), child)) return null;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (['list_comprehension', 'set_comprehension', 'dictionary_comprehension', 'generator_expression']
|
|
123
|
+
.includes(scope.type)) {
|
|
124
|
+
const first = scope.namedChildren.find(part => part.type === 'for_in_clause')?.childForFieldName('right');
|
|
125
|
+
if (!first || !contains(first, node)) return null;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (scope.type === 'class_definition') {
|
|
129
|
+
return sameNode(scope.childForFieldName('body'), child) ? scope : null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function declarationTrees(index, file, content, language) {
|
|
136
|
+
const memo = index.isProvenanceFactIndex ? null
|
|
137
|
+
: index._opMemo?.('valueDeclarationTrees', () => new Map());
|
|
138
|
+
if (memo?.has(file)) return memo.get(file);
|
|
139
|
+
const parser = getParser(language);
|
|
140
|
+
const literal = index._getParsedTree(file, content, language) || safeParse(parser, content);
|
|
141
|
+
const entry = index.files.get(file);
|
|
142
|
+
let recovered = null;
|
|
143
|
+
if (langTraits(language)?.textualIncludes &&
|
|
144
|
+
(literal?.rootNode.hasError || entry?.recoveryBlanks?.length)) {
|
|
145
|
+
const blanks = Array.isArray(entry?.recoveryBlanks) && entry.hash &&
|
|
146
|
+
require('crypto').createHash('md5').update(content).digest('hex') === entry.hash
|
|
147
|
+
? entry.recoveryBlanks : null;
|
|
148
|
+
const module = LANGUAGES[language]?.module();
|
|
149
|
+
if (module?.recoveredTree) recovered = module.recoveredTree(content, parser, blanks);
|
|
150
|
+
}
|
|
151
|
+
const trees = [recovered, literal].filter(Boolean);
|
|
152
|
+
memo?.set(file, trees);
|
|
153
|
+
return trees;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function isWrittenBinding(node) {
|
|
157
|
+
for (let current = node; current?.parent; current = current.parent) {
|
|
158
|
+
const parent = current.parent;
|
|
159
|
+
if (MEMBER_NODES.has(parent.type) || parent.type === 'scoped_identifier') return false;
|
|
160
|
+
if (WRITE_EXPRESSIONS.has(parent.type)) {
|
|
161
|
+
const left = parent.childForFieldName('left') || parent.childForFieldName('argument');
|
|
162
|
+
return left ? contains(left, node) : parent.namedChildCount === 1;
|
|
163
|
+
}
|
|
164
|
+
if (!['expression_list', 'object_pattern', 'array_pattern', 'pair_pattern',
|
|
165
|
+
'parenthesized_expression', 'tuple_pattern'].includes(parent.type)) return false;
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Whether a member name token is the name of an assignment target
|
|
171
|
+
* (`self.x = v`, `self.x += v`, `self.a, self.b = ..`). */
|
|
172
|
+
function isWrittenMember(node) {
|
|
173
|
+
const member = node.parent;
|
|
174
|
+
if (!member || !MEMBER_NODES.has(member.type)) return false;
|
|
175
|
+
for (let current = member; current?.parent; current = current.parent) {
|
|
176
|
+
const parent = current.parent;
|
|
177
|
+
if (WRITE_EXPRESSIONS.has(parent.type)) {
|
|
178
|
+
const left = parent.childForFieldName('left');
|
|
179
|
+
return !!left && contains(left, member);
|
|
180
|
+
}
|
|
181
|
+
if (!['expression_list', 'pattern_list', 'tuple_pattern', 'list_pattern', 'tuple', 'list',
|
|
182
|
+
'parenthesized_expression'].includes(parent.type)) return false;
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function contains(scope, node) {
|
|
188
|
+
return !scope || scope.startIndex <= node.startIndex && scope.endIndex >= node.endIndex;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Whether a Go identifier is the key of a keyed composite-literal element
|
|
192
|
+
* (`limit` in `Box{limit: 5}`), fix #434. */
|
|
193
|
+
function goKeyedFieldKey(node) {
|
|
194
|
+
const key = node?.parent;
|
|
195
|
+
const keyed = key?.parent;
|
|
196
|
+
return node?.type === 'identifier' && key?.type === 'literal_element' && keyed?.type === 'keyed_element' &&
|
|
197
|
+
key.namedChildCount === 1 && sameNode(keyed.namedChild(0), key);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The struct type a Go keyed element's literal builds (fix #434): its
|
|
202
|
+
* composite literal's written type, through pointers, or for an elided
|
|
203
|
+
* inner literal (`[]Box{{limit: 3}}`, `map[K]Box{k: {..}}`) the element or
|
|
204
|
+
* value type of the enclosing literal's type. Null when not written.
|
|
205
|
+
*/
|
|
206
|
+
function goCompositeLiteralType(keyNode) {
|
|
207
|
+
const literalValue = keyNode.parent.parent.parent;
|
|
208
|
+
if (literalValue?.type !== 'literal_value') return null;
|
|
209
|
+
const holder = literalValue.parent;
|
|
210
|
+
if (holder?.type === 'composite_literal') return unwrapGoType(holder.childForFieldName('type'));
|
|
211
|
+
if (holder?.type !== 'literal_element') return null;
|
|
212
|
+
const element = holder.parent;
|
|
213
|
+
const outerValue = element?.type === 'keyed_element' ? element.parent : element;
|
|
214
|
+
const outerType = goCompositeLiteralTypeOfValue(outerValue);
|
|
215
|
+
if (!outerType) return null;
|
|
216
|
+
if (outerType.type === 'slice_type' || outerType.type === 'array_type' || outerType.type === 'implicit_length_array_type') {
|
|
217
|
+
return unwrapGoType(outerType.childForFieldName('element'));
|
|
218
|
+
}
|
|
219
|
+
if (outerType.type === 'map_type') {
|
|
220
|
+
const isKey = element?.type === 'keyed_element' && sameNode(element.namedChild(0), holder);
|
|
221
|
+
return unwrapGoType(outerType.childForFieldName(isKey ? 'key' : 'value'));
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function goCompositeLiteralTypeOfValue(literalValue) {
|
|
227
|
+
if (literalValue?.type !== 'literal_value') return null;
|
|
228
|
+
const holder = literalValue.parent;
|
|
229
|
+
if (holder?.type === 'composite_literal') return holder.childForFieldName('type');
|
|
230
|
+
if (holder?.type !== 'literal_element') return null;
|
|
231
|
+
const element = holder.parent;
|
|
232
|
+
const outerValue = element?.type === 'keyed_element' ? element.parent : element;
|
|
233
|
+
const outerType = goCompositeLiteralTypeOfValue(outerValue);
|
|
234
|
+
if (!outerType) return null;
|
|
235
|
+
if (['slice_type', 'array_type', 'implicit_length_array_type'].includes(outerType.type)) {
|
|
236
|
+
return unwrapGoType(outerType.childForFieldName('element'));
|
|
237
|
+
}
|
|
238
|
+
if (outerType.type === 'map_type') {
|
|
239
|
+
const isKey = element?.type === 'keyed_element' && sameNode(element.namedChild(0), holder);
|
|
240
|
+
return unwrapGoType(outerType.childForFieldName(isKey ? 'key' : 'value'));
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function unwrapGoType(node) {
|
|
246
|
+
let current = node;
|
|
247
|
+
while (current?.type === 'pointer_type' || current?.type === 'parenthesized_type') {
|
|
248
|
+
current = current.type === 'pointer_type' ? current.namedChild(0) : current.namedChild(0);
|
|
249
|
+
}
|
|
250
|
+
return current?.type === 'type_identifier' || current?.type === 'qualified_type' ||
|
|
251
|
+
current?.type === 'generic_type' ? current : current?.type === 'slice_type' || current?.type === 'map_type' ||
|
|
252
|
+
current?.type === 'array_type' ? current : null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function typeSyntax(node) {
|
|
256
|
+
if (!node) return null;
|
|
257
|
+
if (TYPE_NAMES.has(node.type)) return { name: node.text, node };
|
|
258
|
+
if (node.type === 'qualified_identifier') {
|
|
259
|
+
const tail = typeSyntax(node.childForFieldName('name'));
|
|
260
|
+
const scope = node.childForFieldName('scope');
|
|
261
|
+
if (!tail) return null;
|
|
262
|
+
if (!scope) return node.children.some(child => child.type === '::')
|
|
263
|
+
? { ...tail, qualifierText: `::${tail.qualifierText || ''}` } : tail;
|
|
264
|
+
return { ...tail, qualifier: tail.qualifier || scope,
|
|
265
|
+
qualifierText: [scope.text, tail.qualifierText].filter(Boolean).join('::') };
|
|
266
|
+
}
|
|
267
|
+
if (TYPE_WRAPPERS.has(node.type)) {
|
|
268
|
+
return typeSyntax(node.childForFieldName('type') || node.namedChild(0));
|
|
269
|
+
}
|
|
270
|
+
if (node.type === 'generic_type' || node.type === 'generic_name') {
|
|
271
|
+
return typeSyntax(node.childForFieldName('type') || node.childForFieldName('name') || node.namedChild(0));
|
|
272
|
+
}
|
|
273
|
+
const name = node.childForFieldName('name') || (node.type === 'scoped_type_identifier'
|
|
274
|
+
? node.namedChild(node.namedChildCount - 1) : null);
|
|
275
|
+
const qualifier = node.childForFieldName('scope') || node.childForFieldName('qualifier') ||
|
|
276
|
+
node.childForFieldName('package') || node.childForFieldName('module') ||
|
|
277
|
+
node.childForFieldName('path') || (node.type === 'scoped_type_identifier' ? node.namedChild(0) : null);
|
|
278
|
+
if (name && TYPE_NAMES.has(name.type)) return {
|
|
279
|
+
name: name.text, node: name,
|
|
280
|
+
...(qualifier && { qualifier, qualifierText: qualifier.text }),
|
|
281
|
+
};
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function declarationType(binding) {
|
|
286
|
+
for (let node = binding; node; node = node.parent) {
|
|
287
|
+
if (!DECLARATIONS.has(node.type)) continue;
|
|
288
|
+
const type = node.childForFieldName('type');
|
|
289
|
+
if (type) return { syntax: typeSyntax(type), origin: 'annotation' };
|
|
290
|
+
let value = node.childForFieldName('value') || node.childForFieldName('right');
|
|
291
|
+
// Go `b, p := Box{..}, &Box{..}` (fix #434): the value at the
|
|
292
|
+
// binding's position, through the address operator.
|
|
293
|
+
if (value?.type === 'expression_list') {
|
|
294
|
+
const left = node.childForFieldName('left');
|
|
295
|
+
const names = left?.type === 'expression_list' ? left.namedChildren : left ? [left] : [];
|
|
296
|
+
const at = names.findIndex(name => sameNode(name, binding));
|
|
297
|
+
value = at >= 0 && value.namedChildCount === names.length ? value.namedChild(at) : null;
|
|
298
|
+
}
|
|
299
|
+
if (value?.type === 'unary_expression' && value.childForFieldName('operator')?.type === '&') {
|
|
300
|
+
value = value.childForFieldName('operand');
|
|
301
|
+
}
|
|
302
|
+
if (value?.type === 'new_expression' || value?.type === 'object_creation_expression') {
|
|
303
|
+
return { syntax: typeSyntax(value.childForFieldName('constructor') ||
|
|
304
|
+
value.childForFieldName('type')), origin: 'constructor' };
|
|
305
|
+
}
|
|
306
|
+
if (value?.type === 'struct_expression' || value?.type === 'composite_literal') {
|
|
307
|
+
return { syntax: typeSyntax(value.childForFieldName('name') ||
|
|
308
|
+
value.childForFieldName('type')), origin: 'constructor' };
|
|
309
|
+
}
|
|
310
|
+
if (node.type === 'variable_declarator' && node.parent?.childForFieldName('type')) {
|
|
311
|
+
return { syntax: typeSyntax(node.parent.childForFieldName('type')), origin: 'annotation' };
|
|
312
|
+
}
|
|
313
|
+
if (node.type !== 'variable_declarator') break;
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function memberReceiver(node, role) {
|
|
319
|
+
if (role.role === 'member' && role.receiver) return role.receiver;
|
|
320
|
+
const parent = node.parent;
|
|
321
|
+
if (!MEMBER_NODES.has(parent?.type)) return null;
|
|
322
|
+
const name = parent.childForFieldName('field') || parent.childForFieldName('property') ||
|
|
323
|
+
parent.childForFieldName('name') || parent.childForFieldName('attribute');
|
|
324
|
+
if (!sameNode(name, node)) return null;
|
|
325
|
+
return parent.childForFieldName('object') || parent.childForFieldName('value') ||
|
|
326
|
+
parent.childForFieldName('argument') || parent.childForFieldName('operand') ||
|
|
327
|
+
parent.childForFieldName('expression');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function enclosingEnum(node) {
|
|
331
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
332
|
+
if (parent.type === 'enum_specifier') return parent;
|
|
333
|
+
}
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function enumValueBindingInfo(index, definition) {
|
|
338
|
+
const memo = index._opMemo?.('enumValueBindingInfo', () => new WeakMap());
|
|
339
|
+
if (memo?.has(definition)) return memo.get(definition);
|
|
340
|
+
if (definition.className && ownerRefOf(index, definition)?.def?.type !== 'enum') return null;
|
|
341
|
+
const file = definition.file, content = index._readFile(file);
|
|
342
|
+
const language = index.files.get(file)?.language;
|
|
343
|
+
const tree = index._getParsedTree(file, content, language) || safeParse(getParser(language), content);
|
|
344
|
+
const matches = [];
|
|
345
|
+
if (tree) visitNameNodes(tree, content, definition.name, node => {
|
|
346
|
+
if (node.text !== definition.name || node.startPosition.row + 1 !==
|
|
347
|
+
(definition.nameLine || definition.startLine) || node.parent?.type !== 'enumerator' ||
|
|
348
|
+
!sameNode(node.parent.childForFieldName('name'), node)) return true;
|
|
349
|
+
let owner = node.parent;
|
|
350
|
+
while (owner && owner.type !== 'enum_specifier') owner = owner.parent;
|
|
351
|
+
if (owner) {
|
|
352
|
+
matches.push({ node, enumNode: owner,
|
|
353
|
+
scope: referenceBindingScope(node, language, referenceRole(node, language)),
|
|
354
|
+
unscoped: !owner.children.some(child => child.type === 'class' || child.type === 'struct') });
|
|
355
|
+
}
|
|
356
|
+
return true;
|
|
357
|
+
});
|
|
358
|
+
const result = matches.length === 1 ? matches[0] : null;
|
|
359
|
+
memo?.set(definition, result);
|
|
360
|
+
return result;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Tree -> Map(`${language}\0${name}` -> the name's tokens with their roles
|
|
364
|
+
// and binding scopes); entries die with their trees.
|
|
365
|
+
const SYNTAX_NODES_BY_TREE = new WeakMap();
|
|
366
|
+
|
|
367
|
+
class ValueReferenceResolver extends TypeReferenceResolver {
|
|
368
|
+
constructor(index, def, name) {
|
|
369
|
+
super(index, def, name);
|
|
370
|
+
this.family = langTraits(this.language)?.nameReferenceFamily || this.family;
|
|
371
|
+
this.declarationKinds = new Set(['state', 'constant']);
|
|
372
|
+
this.all = (index.symbols.get(name) || []).filter(d => isValueDefinition(d) &&
|
|
373
|
+
(def.className || !d.className));
|
|
374
|
+
this.group = new Set([def]);
|
|
375
|
+
this.valueReferences = true;
|
|
376
|
+
this.owner = def.className ? ownerRefOf(index, def) : null;
|
|
377
|
+
this.references = [];
|
|
378
|
+
this.syntaxMemo = new Map();
|
|
379
|
+
this.lexicalScopeMemos = new WeakMap();
|
|
380
|
+
this.typeMemo = new Map();
|
|
381
|
+
this.memberMemo = new Map();
|
|
382
|
+
this.enumMemo = new Map();
|
|
383
|
+
this.scopeMemo = new Map();
|
|
384
|
+
this.localFileModes = new Map();
|
|
385
|
+
this.enumReferenceSyntax = this.owner?.def?.type === 'enum' &&
|
|
386
|
+
langTraits(this.language)?.staticEnumReferenceSyntax
|
|
387
|
+
? new (require('./enum-reference-syntax').EnumReferenceSyntax)(this) : null;
|
|
388
|
+
this.patternConstant = false;
|
|
389
|
+
this.selectedDeclarations = 0;
|
|
390
|
+
this.declarationStorages = new Set();
|
|
391
|
+
this.declarationRegions = new Set();
|
|
392
|
+
if (langTraits(this.language)?.valuePatternConstants) {
|
|
393
|
+
const content = index._readFile(def.file);
|
|
394
|
+
const tree = index._getParsedTree(def.file, content, this.language) ||
|
|
395
|
+
safeParse(getParser(this.language), content);
|
|
396
|
+
if (tree) visitNameNodes(tree, content, name, node => {
|
|
397
|
+
if (node.startPosition.row + 1 === (def.nameLine || def.startLine) &&
|
|
398
|
+
['const_item', 'enum_variant'].includes(node.parent?.type)) this.patternConstant = true;
|
|
399
|
+
return true;
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
// A Python module variable is one variable however many statements
|
|
403
|
+
// bind it (fix #420): every module-scope binding of the name in the
|
|
404
|
+
// pin's file is renamed with it, as the run-time alternatives of a
|
|
405
|
+
// callable are (fix #376), so `x = x` and `import json; json = json`
|
|
406
|
+
// keep reading the variable they read before.
|
|
407
|
+
this.moduleVariable = !!langTraits(this.language)?.assignmentDeclaresLocal &&
|
|
408
|
+
!def.className && def.lexicalScopeStartLine == null;
|
|
409
|
+
if (this.moduleVariable) {
|
|
410
|
+
for (const other of this.all) {
|
|
411
|
+
if (other.file === def.file && !other.className && other.lexicalScopeStartLine == null) this.group.add(other);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
this.linkAlternatives = null;
|
|
415
|
+
if (langTraits(this.language)?.textualIncludes) this.groupLinkage();
|
|
416
|
+
this.members = (index.symbols.get(name) || []).filter(d => d.className &&
|
|
417
|
+
!TYPE_KINDS.has(d.type) &&
|
|
418
|
+
(!langTraits(this.language)?.fieldsAndMethodsSeparate || isValueDefinition(d)));
|
|
419
|
+
// A member value's slot (fix #420): where member lookup is by name
|
|
420
|
+
// through the class chain (structural languages), the same-name
|
|
421
|
+
// fields of the declaring ancestors, of the interfaces a class body
|
|
422
|
+
// implements and of overriding descendants are one member: TS checks
|
|
423
|
+
// `implements`, and a subclass field shadows the renamed one at run
|
|
424
|
+
// time. Accessors filling the slot are reviewed, not token-renamed.
|
|
425
|
+
this.targetStorage = this.valueStorage(def);
|
|
426
|
+
this.slotAccessors = [];
|
|
427
|
+
const traits = langTraits(this.language);
|
|
428
|
+
// A private member (`#x`, a mangled `__x`) is lexical to its class,
|
|
429
|
+
// and static and instance storage are separate slots.
|
|
430
|
+
const privateMember = member => member.type === 'private field' ||
|
|
431
|
+
!!traits?.privateMemberName?.(member.name);
|
|
432
|
+
if (this.owner && traits?.typeSystem === 'structural' && !privateMember(def)) {
|
|
433
|
+
const closure = require('./contract-membership').hierarchySlotClosure(index, def, null, { values: true });
|
|
434
|
+
for (const entry of closure ? [...closure.ancestors, ...closure.descendants] : []) {
|
|
435
|
+
for (const member of entry.members) {
|
|
436
|
+
if (member === def || privateMember(member) ||
|
|
437
|
+
traits.separateStaticStorage && this.valueStorage(member) !== this.targetStorage) continue;
|
|
438
|
+
if (isValueDefinition(member)) this.group.add(member);
|
|
439
|
+
else if (!this.slotAccessors.includes(member)) this.slotAccessors.push(member);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
this.memberCandidates = langTraits(this.language)?.separateStaticStorage
|
|
444
|
+
? this.members.filter(member => this.valueStorage(member) === this.targetStorage) : this.members;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* C/C++ external linkage (fix #420): every file-scope declaration of the
|
|
449
|
+
* name without `static` in one namespace denotes one object of the
|
|
450
|
+
* program, so `extern int g;` in a header and `int g = 0;` in a source
|
|
451
|
+
* file are renamed together, as functions are by their linkage (#361).
|
|
452
|
+
* Several external definitions belong to different programs or
|
|
453
|
+
* configurations: their declarations and the uses that see them are
|
|
454
|
+
* review items (`link-alternatives`).
|
|
455
|
+
*/
|
|
456
|
+
groupLinkage() {
|
|
457
|
+
const def = this.def;
|
|
458
|
+
const { effectiveNamespace } = require('./cpp-scope');
|
|
459
|
+
const namespace = d => effectiveNamespace(this.index, d) || '';
|
|
460
|
+
const linked = d => !d.className && !d.enclosingType && d.lexicalScopeStartLine == null &&
|
|
461
|
+
!(d.modifiers || []).includes('static') &&
|
|
462
|
+
langTraits(this.index.files.get(d.file)?.language)?.textualIncludes;
|
|
463
|
+
if (!linked(def)) return;
|
|
464
|
+
const ownNamespace = namespace(def);
|
|
465
|
+
const family = this.all.filter(d => linked(d) && namespace(d) === ownNamespace);
|
|
466
|
+
const definitions = family.filter(d => !(d.modifiers || []).includes('extern'));
|
|
467
|
+
if (definitions.length <= 1) {
|
|
468
|
+
for (const member of family) this.group.add(member);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
this.linkAlternatives = new Set(family.filter(member => member !== def));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
judge(defs, reason) {
|
|
475
|
+
if (this.linkAlternatives && defs.some(d => this.linkAlternatives.has(d))) {
|
|
476
|
+
return { verdict: 'unknown', reason: 'link-alternatives' };
|
|
477
|
+
}
|
|
478
|
+
return super.judge(defs, reason);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Another declaration of the renamed entity declared by this token: a
|
|
483
|
+
* linkage declaration (C/C++), a module-variable binding statement
|
|
484
|
+
* (Python) or a member of the slot closure (fix #420). 'group' renames
|
|
485
|
+
* it, 'alternative' leaves it for review.
|
|
486
|
+
*/
|
|
487
|
+
groupMemberAt(file, node, otherOwner = null) {
|
|
488
|
+
const line = node.startPosition.row + 1;
|
|
489
|
+
const at = d => d !== this.def && d.file === file && (d.nameLine || d.startLine) === line &&
|
|
490
|
+
(!otherOwner || ownerRefOf(this.index, d)?.key === otherOwner.key);
|
|
491
|
+
if ([...this.group].some(at)) return 'group';
|
|
492
|
+
if (this.linkAlternatives && [...this.linkAlternatives].some(at)) return 'alternative';
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
sourceLocalBinding(file, binding, reference = binding.node) {
|
|
497
|
+
if (!binding.scope || ROOTS.has(binding.scope.type) || TYPE_BODIES.has(binding.scope.type) ||
|
|
498
|
+
['class_body', 'declaration_list', 'field_declaration_list'].includes(binding.scope.type) ||
|
|
499
|
+
binding.role?.role === 'import') return false;
|
|
500
|
+
const traits = langTraits(this.index.files.get(file)?.language);
|
|
501
|
+
if (traits.assignmentDeclaresLocal && !referenceScope(reference,
|
|
502
|
+
this.index.files.get(file)?.language, new Map(), this.name)?.local) return false;
|
|
503
|
+
if (traits.textualIncludes && binding.role?.why === 'function') return false;
|
|
504
|
+
for (let parent = binding.node.parent; parent; parent = parent.parent) {
|
|
505
|
+
if (parent.type === 'ERROR' || TYPE_BODIES.has(parent.type) || ROOTS.has(parent.type) ||
|
|
506
|
+
['using_declaration', 'using_directive', 'namespace_alias_declaration'].includes(parent.type)) return false;
|
|
507
|
+
if (parent.type === 'declaration' && parent.namedChildren.some(child =>
|
|
508
|
+
child.type === 'storage_class_specifier' && child.text === 'extern')) return false;
|
|
509
|
+
if (CALLABLE_SCOPES.has(parent.type) ||
|
|
510
|
+
['accessor_declaration', 'local_function_statement', 'func_literal'].includes(parent.type)) return true;
|
|
511
|
+
}
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
readLocalFile(file, entry, language, rows, content) {
|
|
516
|
+
const enumSyntax = this.enumReferenceSyntax?.read(file, entry, language, rows, content);
|
|
517
|
+
if (enumSyntax) return enumSyntax;
|
|
518
|
+
if (file === this.def.file || this.patternConstant || !entry.hash ||
|
|
519
|
+
!this.index._usageResultCache || this.index.isProvenanceFactIndex) return null;
|
|
520
|
+
const crypto = require('crypto');
|
|
521
|
+
const { indexedContent, syntaxMode } = require('./source-memo');
|
|
522
|
+
if (!indexedContent(this.index, file, entry, content)) return null;
|
|
523
|
+
// Source generations also cover macro definitions in other files.
|
|
524
|
+
// These facts never retain a project-dependent ownership verdict.
|
|
525
|
+
const generation = this.index._opMemo('valueLocalSourceGeneration', () => {
|
|
526
|
+
const hash = crypto.createHash('sha256');
|
|
527
|
+
for (const [path, source] of [...this.index.files].sort((a, b) =>
|
|
528
|
+
require('./shared').codeUnitCompare(a[0], b[0]))) hash.update(path + '\0' + source.hash + '\0' + source.language + '\0');
|
|
529
|
+
return hash.update(JSON.stringify(this.index.config)).digest('hex');
|
|
530
|
+
});
|
|
531
|
+
const mode = syntaxMode('valueLocal', [generation, language,
|
|
532
|
+
entry.recoveryBlanks, entry.conditionalViews, entry.externalMacroKey, rows]);
|
|
533
|
+
this.localFileModes.set(file, { mode, referenceStart: this.references.length });
|
|
534
|
+
const value = require('./cache').getUsageResult(this.index, file, this.name, mode);
|
|
535
|
+
if (!Array.isArray(value)) return null;
|
|
536
|
+
const lines = new Map(), references = [], wanted = new Set(rows);
|
|
537
|
+
for (const record of value) {
|
|
538
|
+
if (!Array.isArray(record) || !wanted.has(record[1])) return null;
|
|
539
|
+
if (record[0] === 'line') {
|
|
540
|
+
const [, line, skips, columns, text, tokens] = record;
|
|
541
|
+
if (lines.has(line) || !Number.isInteger(skips) || !Number.isInteger(tokens) ||
|
|
542
|
+
!Array.isArray(columns) || columns.some(c => !Number.isInteger(c) || c < 0) ||
|
|
543
|
+
typeof text !== 'boolean') return null;
|
|
544
|
+
lines.set(line, { edits: [], reviews: [], skips, skipColumns: columns.slice(), text, tokens, macroBody: false });
|
|
545
|
+
} else if (record[0] === 'reference') {
|
|
546
|
+
const [, line, column, accessKind, reason, receiver] = record;
|
|
547
|
+
if (!Number.isInteger(column) || column < 0 || typeof accessKind !== 'string' ||
|
|
548
|
+
typeof reason !== 'string' || receiver != null && typeof receiver !== 'string') return null;
|
|
549
|
+
references.push({ file: entry.relativePath, absoluteFile: file, line, column,
|
|
550
|
+
expression: this.index.getLineContent(file, line).trim(), accessKind,
|
|
551
|
+
verdict: 'no', reason, ...(receiver != null && { receiver }) });
|
|
552
|
+
} else return null;
|
|
553
|
+
}
|
|
554
|
+
if (lines.size !== rows.length) return null;
|
|
555
|
+
this.references.push(...references);
|
|
556
|
+
return lines;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
rememberLocalFile(file, lines, localOnly) {
|
|
560
|
+
this.enumReferenceSyntax?.remember(file, lines);
|
|
561
|
+
const state = this.localFileModes.get(file);
|
|
562
|
+
if (!state || !localOnly || [...lines.values()].some(slot =>
|
|
563
|
+
slot.edits.length || slot.reviews.length || slot.macroBody || slot.stringAnnotation)) return;
|
|
564
|
+
const references = this.references.slice(state.referenceStart);
|
|
565
|
+
if (references.some(ref => ref.absoluteFile !== file || ref.verdict !== 'no')) return;
|
|
566
|
+
const value = [...lines].map(([line, slot]) =>
|
|
567
|
+
['line', line, slot.skips, slot.skipColumns.slice(), slot.text, slot.tokens]);
|
|
568
|
+
for (const ref of references) value.push(['reference', ref.line, ref.column,
|
|
569
|
+
ref.accessKind, ref.reason, ref.receiver ?? null]);
|
|
570
|
+
require('./cache').storeUsageResult(this.index, file, this.name, state.mode, value);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
siteArity() { return null; }
|
|
574
|
+
|
|
575
|
+
valueScope(file, line, column = null) {
|
|
576
|
+
const language = this.index.files.get(file)?.language;
|
|
577
|
+
const reader = langTraits(language)?.valueScopes;
|
|
578
|
+
if (!reader) return null;
|
|
579
|
+
if (!this.scopeMemo.has(file)) this.scopeMemo.set(file,
|
|
580
|
+
reader(this.index._readFile(file), getParser(language)));
|
|
581
|
+
const scopes = this.scopeMemo.get(file).filter(scope => line - 1 >= scope.startRow &&
|
|
582
|
+
line - 1 <= scope.endRow && (column == null ||
|
|
583
|
+
(line - 1 !== scope.startRow || column >= scope.startCol) &&
|
|
584
|
+
(line - 1 !== scope.endRow || column < scope.endCol)));
|
|
585
|
+
const identities = new Set(scopes.map(scope => scope.module
|
|
586
|
+
? `module:${scope.startRow}:${scope.startCol}` : 'classic'));
|
|
587
|
+
return identities.size === 1 ? [...identities][0] : 'unknown';
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
scopeRelation(file, definitionLine, node, definitionColumn = null) {
|
|
591
|
+
const declared = this.valueScope(file, definitionLine, definitionColumn);
|
|
592
|
+
if (declared == null) return 'yes';
|
|
593
|
+
const referenced = this.valueScope(file, node.startPosition.row + 1, node.startPosition.column);
|
|
594
|
+
if (declared === 'unknown' || referenced === 'unknown') return 'unknown';
|
|
595
|
+
if (declared === referenced) return 'yes';
|
|
596
|
+
return declared === 'classic' ? 'unknown' : 'no';
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
valueStorage(definition) {
|
|
600
|
+
return definition.modifiers?.includes('static') ||
|
|
601
|
+
ownerRefOf(this.index, definition)?.def?.type === 'enum' ? 'class' : 'instance';
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
thisStorage(node) {
|
|
605
|
+
for (let scope = node.parent; scope; scope = scope.parent) {
|
|
606
|
+
if (scope.type === 'class_static_block') return 'class';
|
|
607
|
+
if (['method_definition', 'field_definition', 'public_field_definition'].includes(scope.type) &&
|
|
608
|
+
scope.parent?.type === 'class_body') {
|
|
609
|
+
return scope.children.some(child => child.type === 'static') ? 'class' : 'instance';
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
patternReference(file, node, role) {
|
|
616
|
+
if (!this.patternConstant || !role.binding) return null;
|
|
617
|
+
for (let current = node; current?.parent; current = current.parent) {
|
|
618
|
+
const parent = current.parent;
|
|
619
|
+
const pattern = parent.childForFieldName('pattern');
|
|
620
|
+
if (pattern && contains(pattern, node)) {
|
|
621
|
+
return this.resolveName(file, node.startPosition.row + 1);
|
|
622
|
+
}
|
|
623
|
+
if (CALLABLE_SCOPES.has(parent.type) || ROOTS.has(parent.type)) break;
|
|
624
|
+
}
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
enumInfo(definition) {
|
|
629
|
+
if (this.enumMemo.has(definition)) return this.enumMemo.get(definition);
|
|
630
|
+
const result = enumValueBindingInfo(this.index, definition);
|
|
631
|
+
this.enumMemo.set(definition, result);
|
|
632
|
+
return result;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
fileTypes(file, name = this.name) {
|
|
636
|
+
return (this.index.symbols.get(name) || []).filter(d => d.file === file &&
|
|
637
|
+
isValueDefinition(d) && !d.className && (!this.siteNode ||
|
|
638
|
+
this.scopeRelation(file, d.nameLine || d.startLine, this.siteNode) !== 'no'));
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
syntaxNodes(file, tree, name) {
|
|
642
|
+
const key = `${file}\0${tree.rootNode.id}\0${name}`;
|
|
643
|
+
if (this.syntaxMemo.has(key)) return this.syntaxMemo.get(key);
|
|
644
|
+
const language = GRAMMAR_LANGUAGES.get(tree.language?.name) || this.index.files.get(file)?.language;
|
|
645
|
+
// The tokens of a name in a tree are syntax alone: every resolver of
|
|
646
|
+
// the process shares them while the tree lives (fix #436: each
|
|
647
|
+
// query re-classified every occurrence of each receiver's name).
|
|
648
|
+
// A witness view records its reads, so it derives its own.
|
|
649
|
+
let shared = null;
|
|
650
|
+
if (!this.index.isProvenanceFactIndex) {
|
|
651
|
+
shared = SYNTAX_NODES_BY_TREE.get(tree);
|
|
652
|
+
if (!shared) SYNTAX_NODES_BY_TREE.set(tree, shared = new Map());
|
|
653
|
+
const nodes = shared.get(`${language}\0${name}`);
|
|
654
|
+
if (nodes) {
|
|
655
|
+
this.syntaxMemo.set(key, nodes);
|
|
656
|
+
return nodes;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
const source = typeof tree.input === 'string' ? tree.input : this.index._readFile(file);
|
|
660
|
+
const nodes = [];
|
|
661
|
+
visitNameNodes(tree, source, name, node => {
|
|
662
|
+
if (node.text !== name) return true;
|
|
663
|
+
const role = referenceRole(node, language);
|
|
664
|
+
const declarationName = role.binding && role.why === 'function' &&
|
|
665
|
+
CALLABLE_SCOPES.has(node.parent?.type);
|
|
666
|
+
// Ordinary references need their token and role; only a binding
|
|
667
|
+
// needs the scope in which it declares the name.
|
|
668
|
+
let scope = role.binding
|
|
669
|
+
? referenceBindingScope(declarationName ? node.parent : node, language, role) : null;
|
|
670
|
+
if (role.genericParameter) scope = referenceTypeParameterScope(node);
|
|
671
|
+
if (role.binding && node.parent?.parent?.type === 'template_parameter_list') {
|
|
672
|
+
scope = node.parent.parent.parent;
|
|
673
|
+
}
|
|
674
|
+
nodes.push({ node, role, scope });
|
|
675
|
+
return true;
|
|
676
|
+
});
|
|
677
|
+
this.syntaxMemo.set(key, nodes);
|
|
678
|
+
if (shared) {
|
|
679
|
+
if (shared.size >= 64) shared.delete(shared.keys().next().value);
|
|
680
|
+
shared.set(`${language}\0${name}`, nodes);
|
|
681
|
+
}
|
|
682
|
+
return nodes;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
enumSyntaxFact(file, entry, token, receiver, qualifier, position, shape) {
|
|
686
|
+
const { node, role } = token;
|
|
687
|
+
if (['macro', 'template'].includes(role.role)) return {
|
|
688
|
+
line: node.startPosition.row + 1, column: node.startPosition.column, at: node.startIndex,
|
|
689
|
+
kind: 'opaque', sourceRole: role.role, accessKind: this.references.at(-1).accessKind,
|
|
690
|
+
};
|
|
691
|
+
const syntax = receiver || qualifier;
|
|
692
|
+
if (position || shape?.uncertainRole || shape?.kind === 'initializer' ||
|
|
693
|
+
!syntax || !TYPE_NAMES.has(syntax.type) ||
|
|
694
|
+
!(receiver && role.role === 'member' || qualifier && role.role === 'value')) return null;
|
|
695
|
+
for (let scope = node; scope; scope = scope.parent) {
|
|
696
|
+
if (scope.type === 'ERROR' || CALLABLE_SCOPES.has(scope.type) && scope.hasError) return null;
|
|
697
|
+
}
|
|
698
|
+
if (receiver && this.localBinding(file, syntax, syntax.text, true)) return null;
|
|
699
|
+
if (this.syntaxNodes(file, node.tree, syntax.text).some(binding => binding.role.typeParam &&
|
|
700
|
+
contains(referenceTypeParameterScope(binding.node), syntax))) return null;
|
|
701
|
+
let owner = null;
|
|
702
|
+
for (let parent = syntax.parent; parent; parent = parent.parent) {
|
|
703
|
+
if (!TYPE_BODIES.has(parent.type)) continue;
|
|
704
|
+
const name = parent.childForFieldName('name');
|
|
705
|
+
if (!name || !TYPE_NAMES.has(name.type)) return null;
|
|
706
|
+
owner = { name: name.text, startLine: parent.startPosition.row + 1,
|
|
707
|
+
endLine: parent.endPosition.row + 1 };
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
const context = langTraits(entry.language).enumReferenceContext?.(syntax) || null;
|
|
711
|
+
return { line: node.startPosition.row + 1, column: node.startPosition.column,
|
|
712
|
+
at: node.startIndex, receiver: syntax.text, kind: receiver ? 'member' : 'type', owner, context,
|
|
713
|
+
accessKind: this.references.at(-1).accessKind };
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
selectedBinding(file, node) {
|
|
717
|
+
if (this.owner || file !== this.def.file || node.startPosition.row + 1 !==
|
|
718
|
+
(this.def.nameLine || this.def.startLine)) return false;
|
|
719
|
+
const bindings = this.syntaxNodes(file, node.tree, this.name).filter(token =>
|
|
720
|
+
token.node.startPosition.row === node.startPosition.row && token.role.binding &&
|
|
721
|
+
token.role.why !== 'parameter' && !TYPE_BODIES.has(token.scope?.type) &&
|
|
722
|
+
!['class_body', 'declaration_list', 'field_declaration_list'].includes(token.scope?.type));
|
|
723
|
+
return bindings.length === 1 && sameNode(bindings[0].node, node);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
lexicalMemo(node) {
|
|
727
|
+
let memo = this.lexicalScopeMemos.get(node.tree);
|
|
728
|
+
if (!memo) { memo = new Map(); this.lexicalScopeMemos.set(node.tree, memo); }
|
|
729
|
+
return memo;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/** Python scope lookup (LEGB, `global`/`nonlocal`, defaults and
|
|
733
|
+
* decorators in the enclosing scope) of a name at `node`. A read
|
|
734
|
+
* directly in a class body that binds the name too follows the body's
|
|
735
|
+
* statement order: before every binding it reads the scope around the
|
|
736
|
+
* class (`limit = limit`), after an unconditional one the class
|
|
737
|
+
* namespace ('class'). */
|
|
738
|
+
pythonScope(node, name) {
|
|
739
|
+
const lexical = referenceScope(node, 'python', this.lexicalMemo(node), name, { globalBindsModule: true });
|
|
740
|
+
if (lexical !== 'unknown') return lexical;
|
|
741
|
+
const classNode = pythonClassBodyOf(node);
|
|
742
|
+
const order = classNode && pythonClassBodyOrder(classNode, node, name);
|
|
743
|
+
if (order === 'outer') return this.pythonScope(classNode, name);
|
|
744
|
+
if (order === 'class') return { local: true, defRows: null, classBody: classNode };
|
|
745
|
+
return lexical;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* The binding a Python name reference resolves to (fix #420). Python
|
|
750
|
+
* binds per scope, not per statement: the lookup's own scope verdict
|
|
751
|
+
* decides, and a binding of the name in an enclosing function never
|
|
752
|
+
* shadows a reference its rules send elsewhere (a `global` declaration,
|
|
753
|
+
* a default value evaluated in the enclosing scope).
|
|
754
|
+
*/
|
|
755
|
+
pythonLocalBinding(file, node, name, lexical, includeModule) {
|
|
756
|
+
if (lexical === 'none') return { verdict: 'no', reason: 'non-value-position' };
|
|
757
|
+
if (!lexical || lexical === 'unknown') return { verdict: 'unknown', reason: 'lexical-binding-unresolved' };
|
|
758
|
+
if (lexical === 'module' && !includeModule) return null;
|
|
759
|
+
if (lexical.classBody) return this.owner ? null : { verdict: 'no', reason: 'class-binding' };
|
|
760
|
+
const scope = lexical === 'module' ? node.tree.rootNode : lexical.scopeNode;
|
|
761
|
+
const tokens = this.syntaxNodes(file, node.tree, name);
|
|
762
|
+
if (lexical === 'module' && tokens.some(token => token.role.scopeDeclaration === 'global_statement')) {
|
|
763
|
+
return { verdict: 'unknown', reason: 'module-rebinding' };
|
|
764
|
+
}
|
|
765
|
+
const bindings = scope ? tokens.filter(token => token.role.binding && token.scope &&
|
|
766
|
+
sameNode(token.scope, scope)) : [];
|
|
767
|
+
if (bindings.length === 1 && bindings[0].node.startIndex < node.startIndex) {
|
|
768
|
+
return { binding: { ...bindings[0], scope } };
|
|
769
|
+
}
|
|
770
|
+
if (lexical === 'module') {
|
|
771
|
+
return bindings.length ? { verdict: 'unknown', reason: 'module-rebinding' } : null;
|
|
772
|
+
}
|
|
773
|
+
return { verdict: 'no', reason: 'local-binding' };
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
localBinding(file, node, name, includeModule = false, contextualValue = false) {
|
|
777
|
+
const language = GRAMMAR_LANGUAGES.get(node.tree.language?.name) || this.index.files.get(file)?.language;
|
|
778
|
+
const memo = this.lexicalMemo(node);
|
|
779
|
+
if (langTraits(language)?.assignmentDeclaresLocal) {
|
|
780
|
+
return this.pythonLocalBinding(file, node, name, this.pythonScope(node, name), includeModule);
|
|
781
|
+
}
|
|
782
|
+
const lexical = referenceScope(node, language, memo, name);
|
|
783
|
+
if (lexical === 'none' && !contextualValue) return { verdict: 'no', reason: 'non-value-position' };
|
|
784
|
+
let bindings = this.syntaxNodes(file, node.tree, name).filter(binding =>
|
|
785
|
+
binding.role.binding &&
|
|
786
|
+
this.scopeRelation(file, binding.node.startPosition.row + 1, node,
|
|
787
|
+
binding.node.startPosition.column) !== 'no' &&
|
|
788
|
+
!(name === this.name && this.patternReference(file, binding.node, binding.role)?.verdict === 'yes') &&
|
|
789
|
+
(includeModule || binding.scope && !ROOTS.has(binding.scope.type)) &&
|
|
790
|
+
(binding.role.genericParameter || !TYPE_BODIES.has(binding.scope?.type)) &&
|
|
791
|
+
!['class_body', 'declaration_list', 'field_declaration_list'].includes(binding.scope?.type) &&
|
|
792
|
+
contains(binding.scope, node)).map(binding => ({ ...binding,
|
|
793
|
+
scope: binding.scope || node.tree.rootNode }));
|
|
794
|
+
// A pattern variable is in scope where its pattern's flow puts it
|
|
795
|
+
// (fix #420: Java `instanceof`/switch patterns), not in its block.
|
|
796
|
+
const patternReach = langTraits(language)?.patternBindingReach;
|
|
797
|
+
if (patternReach && bindings.some(binding => binding.role.why === 'pattern')) {
|
|
798
|
+
const reached = [];
|
|
799
|
+
for (const binding of bindings) {
|
|
800
|
+
const verdict = binding.role.why === 'pattern' ? patternReach(binding.node, node) : 'in';
|
|
801
|
+
if (verdict === 'unknown') return { verdict: 'unknown', reason: 'pattern-variable-scope' };
|
|
802
|
+
if (verdict === 'in') reached.push(binding);
|
|
803
|
+
}
|
|
804
|
+
bindings = reached;
|
|
805
|
+
}
|
|
806
|
+
if (!bindings.length) {
|
|
807
|
+
if (lexical?.local && name === this.name && langTraits(language)?.moduleValueLookup &&
|
|
808
|
+
this.resolveName(file, node.startPosition.row + 1).verdict === 'yes') return null;
|
|
809
|
+
return lexical === 'unknown' || lexical?.local
|
|
810
|
+
? { verdict: 'unknown', reason: 'lexical-binding-unresolved' } : null;
|
|
811
|
+
}
|
|
812
|
+
bindings.sort((a, b) => (a.scope.endIndex - a.scope.startIndex) -
|
|
813
|
+
(b.scope.endIndex - b.scope.startIndex) || b.node.startIndex - a.node.startIndex);
|
|
814
|
+
const nearest = bindings[0];
|
|
815
|
+
if (this.scopeRelation(file, nearest.node.startPosition.row + 1, node,
|
|
816
|
+
nearest.node.startPosition.column) === 'unknown') {
|
|
817
|
+
return { verdict: 'unknown', reason: 'script-binding-scope' };
|
|
818
|
+
}
|
|
819
|
+
if (name === this.name && this.patternReference(file, nearest.node, nearest.role)?.verdict === 'unknown') {
|
|
820
|
+
return { verdict: 'unknown', reason: 'constant-pattern-binding' };
|
|
821
|
+
}
|
|
822
|
+
const sameScope = bindings.filter(binding => sameNode(binding.scope, nearest.scope));
|
|
823
|
+
if (sameScope.length !== 1 || nearest.node.startIndex >= node.startIndex) {
|
|
824
|
+
return { verdict: 'unknown', reason: 'lexical-binding-unresolved' };
|
|
825
|
+
}
|
|
826
|
+
return { binding: nearest,
|
|
827
|
+
...(name === this.name && this.selectedBinding(file, nearest.node) && { pin: true }) };
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* A Python token that binds or declares a name (an assignment or loop
|
|
832
|
+
* target, a def or class name, an import, a `global` declaration):
|
|
833
|
+
* whether it binds the pinned module variable (fix #420). Null for
|
|
834
|
+
* tokens that bind nothing. An unaliased import of the name binds the
|
|
835
|
+
* variable too; renaming it gives the import an alias (`importAlias`).
|
|
836
|
+
*/
|
|
837
|
+
/**
|
|
838
|
+
* A Python read run at module load before the pin's variable is first
|
|
839
|
+
* bound reads what a preceding star import binds (fix #434): `from m
|
|
840
|
+
* import *` then `X = LIMIT + 1` above `LIMIT = 5` reads m's LIMIT. A
|
|
841
|
+
* read in a function body runs later and reads the variable.
|
|
842
|
+
*/
|
|
843
|
+
pythonStarBindingAt(file, node) {
|
|
844
|
+
const entry = this.index.files.get(file);
|
|
845
|
+
const line = node.startPosition.row + 1;
|
|
846
|
+
const stars = (entry?.importDetails || []).filter(detail => detail.names?.includes('*') && detail.line <= line);
|
|
847
|
+
if (stars.length === 0) return null;
|
|
848
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
849
|
+
if (parent.type === 'function_definition' || parent.type === 'lambda') return null;
|
|
850
|
+
}
|
|
851
|
+
const lines = [...this.group].filter(def => def.file === file).map(def => def.nameLine || def.startLine);
|
|
852
|
+
if (lines.length === 0 || line > Math.min(...lines)) return null;
|
|
853
|
+
const binds = stars.map(star => {
|
|
854
|
+
const target = this.moduleFile(entry, star.module);
|
|
855
|
+
const other = target && this.index.files.get(target);
|
|
856
|
+
if (!other) return 'unknown';
|
|
857
|
+
const exported = (other.exportDetails || []).some(detail => (detail.alias || detail.name) === this.name);
|
|
858
|
+
if (this.name.startsWith('_') && !exported) return 'no';
|
|
859
|
+
if (exported || (other.symbols || []).some(def => def.name === this.name && !def.className &&
|
|
860
|
+
def.lexicalScopeStartLine == null) ||
|
|
861
|
+
(other.importBindings || []).some(binding => (binding.alias || binding.name) === this.name) ||
|
|
862
|
+
(other.moduleAssignedNames || []).includes(this.name)) return 'yes';
|
|
863
|
+
return (other.importNames || []).includes('*') ? 'unknown' : 'no';
|
|
864
|
+
});
|
|
865
|
+
if (binds.includes('yes')) return { verdict: 'no', reason: 'star-import-binding' };
|
|
866
|
+
return binds.includes('unknown') ? { verdict: 'unknown', reason: 'star-import-binding' } : null;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
pythonBindingAt(file, token) {
|
|
870
|
+
const { node, role } = token;
|
|
871
|
+
const parent = node.parent;
|
|
872
|
+
const inPinFile = file === this.def.file;
|
|
873
|
+
const moduleVerdict = () => {
|
|
874
|
+
if (inPinFile) return { verdict: 'yes' };
|
|
875
|
+
// Another module's variable that its import binds to the pin is
|
|
876
|
+
// renamed as a whole there; one some other import also binds
|
|
877
|
+
// stays for review.
|
|
878
|
+
const importer = this.pythonImporterVariable(file);
|
|
879
|
+
return importer === true ? { verdict: 'yes' } : importer === 'mixed'
|
|
880
|
+
? { verdict: 'unknown', reason: 'module-rebinding' } : { verdict: 'no', reason: 'other-module' };
|
|
881
|
+
};
|
|
882
|
+
if (role.scopeDeclaration) {
|
|
883
|
+
return role.scopeDeclaration === 'global_statement' ? moduleVerdict()
|
|
884
|
+
: { verdict: 'no', reason: 'nonlocal-binding' };
|
|
885
|
+
}
|
|
886
|
+
// Another module's imports of the pin are import references.
|
|
887
|
+
const importStatement = inPinFile &&
|
|
888
|
+
(role.role === 'import' || ['module-path', 'import-alias'].includes(role.why));
|
|
889
|
+
const classDecl = role.role === 'decl' && parent?.type === 'class_definition';
|
|
890
|
+
if (!role.binding && !importStatement && !classDecl) return null;
|
|
891
|
+
if (role.why === 'parameter') return { verdict: 'no', reason: 'local-binding' };
|
|
892
|
+
let edit = { verdict: 'yes' };
|
|
893
|
+
if (importStatement) {
|
|
894
|
+
if (role.why === 'module-path') {
|
|
895
|
+
// `import json` binds its first segment; `from pkg import x`
|
|
896
|
+
// names a module path and binds nothing of that spelling.
|
|
897
|
+
const dotted = parent?.type === 'dotted_name' ? parent : null;
|
|
898
|
+
if (!dotted || dotted.parent?.type !== 'import_statement' ||
|
|
899
|
+
!sameNode(dotted.namedChild(0), node)) return { verdict: 'no', reason: 'module-path' };
|
|
900
|
+
if (dotted.namedChildCount > 1) edit = { verdict: 'unknown', reason: 'dotted-import-binding' };
|
|
901
|
+
else edit = { verdict: 'yes', importAlias: true };
|
|
902
|
+
} else if (role.role === 'import') {
|
|
903
|
+
if (role.aliased) return { verdict: 'no', reason: 'import-alias' };
|
|
904
|
+
// The alias token of `from m import x as x` binds; a plain
|
|
905
|
+
// `from m import x` binds the name it imports.
|
|
906
|
+
if (parent?.type !== 'aliased_import') edit = { verdict: 'yes', importAlias: true };
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
const scope = referenceBindingScope(role.why === 'function' || classDecl ? parent : node, 'python', role);
|
|
910
|
+
if (scope?.type === 'class_definition') return { verdict: 'no', reason: 'class-binding' };
|
|
911
|
+
const lexical = this.pythonScope(node, this.name);
|
|
912
|
+
if (lexical === 'module') {
|
|
913
|
+
const verdict = moduleVerdict();
|
|
914
|
+
return verdict.verdict === 'yes' ? edit : verdict;
|
|
915
|
+
}
|
|
916
|
+
if (lexical && typeof lexical === 'object') return { verdict: 'no', reason: 'local-binding' };
|
|
917
|
+
// An import binds in the function holding it, a lookup the scope
|
|
918
|
+
// rules leave open only for imported names.
|
|
919
|
+
if (importStatement) {
|
|
920
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
921
|
+
if (current.type === 'function_definition' || current.type === 'lambda') {
|
|
922
|
+
return { verdict: 'no', reason: 'local-binding' };
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
return { verdict: 'unknown', reason: 'lexical-binding-unresolved' };
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* A Python module whose module variable of the pin's name its own
|
|
931
|
+
* imports bind to the pin (fix #420): that variable is the pin there
|
|
932
|
+
* (true), so its rebindings, `global` writes and reads are renamed with
|
|
933
|
+
* the import. 'mixed' when another import binds it to something else,
|
|
934
|
+
* false when no import of the module binds it to the pin.
|
|
935
|
+
*/
|
|
936
|
+
pythonImporterVariable(file) {
|
|
937
|
+
if (!this._importerVariables) this._importerVariables = new Map();
|
|
938
|
+
if (this._importerVariables.has(file)) return this._importerVariables.get(file);
|
|
939
|
+
const entry = this.index.files.get(file);
|
|
940
|
+
let result = false;
|
|
941
|
+
if (file !== this.def.file && !this.owner && entry && langTraits(entry.language)?.assignmentDeclaresLocal) {
|
|
942
|
+
// An import inside a function binds a local of it.
|
|
943
|
+
const bindings = (entry.importBindings || []).filter(binding => binding.module != null &&
|
|
944
|
+
binding.name !== '*' && (binding.alias || binding.name) === this.name &&
|
|
945
|
+
!(Number.isInteger(binding.line) && this.index.findEnclosingFunction(file, binding.line, true)));
|
|
946
|
+
const reaches = bindings.map(binding => binding.kind !== 'import' && binding.name === this.name &&
|
|
947
|
+
this.importBindingVerdict(file, entry, { ...binding, alias: undefined }, this.name).verdict === 'yes');
|
|
948
|
+
if (reaches.length > 0 && reaches.every(Boolean)) result = true;
|
|
949
|
+
else if (reaches.some(Boolean)) result = 'mixed';
|
|
950
|
+
}
|
|
951
|
+
this._importerVariables.set(file, result);
|
|
952
|
+
return result;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* What a JS/TS import binding holds (fix #420): 'namespace' (the
|
|
957
|
+
* imported module's own namespace or exports object), `{ modules }`
|
|
958
|
+
* (the namespace of other modules it re-exports: `export * as ns`,
|
|
959
|
+
* `import * as ns ..; export { ns }`), 'value' (an exported declaration:
|
|
960
|
+
* its members are properties of that value) or 'unknown' (a re-export
|
|
961
|
+
* chain the index cannot follow).
|
|
962
|
+
*/
|
|
963
|
+
jsBindingHolds(file, binding, depth = 0) {
|
|
964
|
+
const entry = this.index.files.get(file);
|
|
965
|
+
if (binding.kind === 'namespace' || binding.kind === 'require') {
|
|
966
|
+
if (depth === 0) return 'namespace';
|
|
967
|
+
const module = entry && this.moduleFile(entry, binding.module);
|
|
968
|
+
return module ? { modules: [module] } : 'unknown';
|
|
969
|
+
}
|
|
970
|
+
if (depth > 4) return 'unknown';
|
|
971
|
+
const target = entry && this.moduleFile(entry, binding.module);
|
|
972
|
+
if (!target) {
|
|
973
|
+
const { bindingIsExternal } = require('./type-denotation');
|
|
974
|
+
return bindingIsExternal(this.index, entry, file, binding) === true ? 'value' : 'unknown';
|
|
975
|
+
}
|
|
976
|
+
return this.jsExportHolds(target, binding.kind === 'default' ? null : binding.name, depth) || 'unknown';
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* What module `target` exports under `name` (null: its default export),
|
|
981
|
+
* as jsBindingHolds answers; null when it exports no such name. Names a
|
|
982
|
+
* module re-exports with `export * from` are its exports too (fix #431).
|
|
983
|
+
*/
|
|
984
|
+
jsExportHolds(target, name, depth, seen = new Set()) {
|
|
985
|
+
if (depth > 4 || seen.has(target)) return 'unknown';
|
|
986
|
+
seen.add(target);
|
|
987
|
+
const targetEntry = this.index.files.get(target);
|
|
988
|
+
const exports = targetEntry?.exportDetails || [];
|
|
989
|
+
const details = exports.filter(detail => name == null ? detail.type === 'default'
|
|
990
|
+
: detail.type !== 'default' && detail.type !== 're-export-all' && (detail.alias || detail.name) === name ||
|
|
991
|
+
detail.type === 're-export-all' && detail.alias === name);
|
|
992
|
+
if (details.length === 0) {
|
|
993
|
+
if (name == null) return null;
|
|
994
|
+
const holds = [];
|
|
995
|
+
for (const star of exports) {
|
|
996
|
+
if (star.type !== 're-export-all' || star.alias) continue;
|
|
997
|
+
const module = star.source && this.moduleFile(targetEntry, star.source);
|
|
998
|
+
const hold = module ? this.jsExportHolds(module, name, depth + 1, seen) : 'unknown';
|
|
999
|
+
if (hold) holds.push(hold);
|
|
1000
|
+
}
|
|
1001
|
+
return holds.length > 0 ? mergeHolds(holds) : null;
|
|
1002
|
+
}
|
|
1003
|
+
const holds = details.map(detail => {
|
|
1004
|
+
if (detail.type === 're-export-all') {
|
|
1005
|
+
const module = detail.source && this.moduleFile(targetEntry, detail.source);
|
|
1006
|
+
return module ? { modules: [module] } : 'unknown';
|
|
1007
|
+
}
|
|
1008
|
+
if (detail.type === 're-export') {
|
|
1009
|
+
return detail.source ? this.jsBindingHolds(target, { name: detail.name, module: detail.source,
|
|
1010
|
+
kind: detail.name === 'default' ? 'default' : 'named' }, depth + 1) : 'unknown';
|
|
1011
|
+
}
|
|
1012
|
+
if (detail.type === 'exports' || detail.type === 'module.exports') return 'value';
|
|
1013
|
+
const imported = (targetEntry?.importBindings || []).filter(other =>
|
|
1014
|
+
(other.alias || other.name) === detail.name && other.module != null);
|
|
1015
|
+
if (imported.length === 0) return 'value';
|
|
1016
|
+
return mergeHolds(imported.map(other => this.jsBindingHolds(target, other, depth + 1)));
|
|
1017
|
+
});
|
|
1018
|
+
return mergeHolds(holds);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* The top-level declaration module `target` exports under `name` (null:
|
|
1023
|
+
* its default export), followed through re-exports, imports and `export
|
|
1024
|
+
* *` (fix #434): `{ file, name }`, 'unknown' for a link the index cannot
|
|
1025
|
+
* follow or several answers, null when the module exports no such name.
|
|
1026
|
+
*/
|
|
1027
|
+
jsExportedDeclaration(target, name, depth = 0, seen = new Set()) {
|
|
1028
|
+
const key = `${target}\0${name ?? ''}`;
|
|
1029
|
+
if (depth > 4 || seen.has(key)) return 'unknown';
|
|
1030
|
+
seen.add(key);
|
|
1031
|
+
const entry = this.index.files.get(target);
|
|
1032
|
+
const exports = entry?.exportDetails || [];
|
|
1033
|
+
const single = answers => {
|
|
1034
|
+
if (answers.some(answer => answer === 'unknown')) return 'unknown';
|
|
1035
|
+
const found = answers.filter(Boolean);
|
|
1036
|
+
if (found.length === 0) return null;
|
|
1037
|
+
return found.every(answer => answer.file === found[0].file && answer.name === found[0].name)
|
|
1038
|
+
? found[0] : 'unknown';
|
|
1039
|
+
};
|
|
1040
|
+
const details = exports.filter(detail => name == null ? detail.type === 'default'
|
|
1041
|
+
: detail.type !== 'default' && detail.type !== 're-export-all' && (detail.alias || detail.name) === name);
|
|
1042
|
+
if (details.length === 0) {
|
|
1043
|
+
if (name == null) return null;
|
|
1044
|
+
return single(exports.filter(star => star.type === 're-export-all' && !star.alias).map(star => {
|
|
1045
|
+
const module = star.source && this.moduleFile(entry, star.source);
|
|
1046
|
+
return module ? this.jsExportedDeclaration(module, name, depth + 1, seen) : 'unknown';
|
|
1047
|
+
}));
|
|
1048
|
+
}
|
|
1049
|
+
return single(details.map(detail => {
|
|
1050
|
+
if (detail.type === 're-export') {
|
|
1051
|
+
const module = detail.source && this.moduleFile(entry, detail.source);
|
|
1052
|
+
return module ? this.jsExportedDeclaration(module, detail.name === 'default' ? null : detail.name,
|
|
1053
|
+
depth + 1, seen) : 'unknown';
|
|
1054
|
+
}
|
|
1055
|
+
if (detail.type === 'exports' || detail.type === 'module.exports') return 'unknown';
|
|
1056
|
+
const local = detail.localName || detail.name;
|
|
1057
|
+
if (!local || local === 'default') return 'unknown';
|
|
1058
|
+
const imported = (entry.importBindings || []).filter(binding =>
|
|
1059
|
+
(binding.alias || binding.name) === local && binding.module != null);
|
|
1060
|
+
if (imported.length === 0) return { file: target, name: local };
|
|
1061
|
+
return single(imported.map(binding => {
|
|
1062
|
+
if (binding.kind !== 'named' && binding.kind !== 'default') return 'unknown';
|
|
1063
|
+
const module = this.moduleFile(entry, binding.module);
|
|
1064
|
+
return module ? this.jsExportedDeclaration(module, binding.kind === 'default' ? null : binding.name,
|
|
1065
|
+
depth + 1, seen) : 'unknown';
|
|
1066
|
+
}));
|
|
1067
|
+
}));
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* The class, enum or interface a JS/TS receiver names through an import
|
|
1072
|
+
* the receiver's name alone does not spell (fix #434): an aliased or
|
|
1073
|
+
* default import (`import { Color as C }` then `C.Red`), a module
|
|
1074
|
+
* namespace chain (`ns.Color.Red`), or a type its namespace declares
|
|
1075
|
+
* (`E.Shade.Red`). Null when the link is no such import or undecided.
|
|
1076
|
+
*/
|
|
1077
|
+
jsImportedOwner(file, receiver) {
|
|
1078
|
+
const entry = this.index.files.get(file);
|
|
1079
|
+
const language = entry?.language;
|
|
1080
|
+
const syntax = language === 'html' ? 'javascript' : language;
|
|
1081
|
+
let declaration = null;
|
|
1082
|
+
if (receiver.type === 'identifier') {
|
|
1083
|
+
const bindings = (entry.importBindings || []).filter(binding =>
|
|
1084
|
+
(binding.alias || binding.name) === receiver.text && binding.module != null);
|
|
1085
|
+
if (bindings.length !== 1) return null;
|
|
1086
|
+
const [binding] = bindings;
|
|
1087
|
+
if (binding.kind !== 'default' && !(binding.kind === 'named' && binding.alias && binding.alias !== binding.name)) return null;
|
|
1088
|
+
if (referenceScope(receiver, syntax, this.lexicalMemo(receiver), receiver.text) !== 'module') return null;
|
|
1089
|
+
const target = this.moduleFile(entry, binding.module);
|
|
1090
|
+
declaration = target ? this.jsExportedDeclaration(target, binding.kind === 'default' ? null : binding.name) : null;
|
|
1091
|
+
} else if (receiver.type === 'member_expression') {
|
|
1092
|
+
const property = receiver.childForFieldName('property');
|
|
1093
|
+
const object = receiver.childForFieldName('object');
|
|
1094
|
+
if (property?.type !== 'property_identifier' || !object) return null;
|
|
1095
|
+
const holds = this.jsNamespaceHolds(file, object);
|
|
1096
|
+
if (holds?.modules) {
|
|
1097
|
+
const answers = holds.modules.map(module => this.jsExportedDeclaration(module, property.text));
|
|
1098
|
+
declaration = answers.length === 1 ? answers[0] : 'unknown';
|
|
1099
|
+
} else if (!holds && object.type === 'identifier') {
|
|
1100
|
+
// A type a TS namespace declares (`E.Shade`), the namespace
|
|
1101
|
+
// named in this module.
|
|
1102
|
+
const space = this.jsImportedOwner(file, object) || this.classForType(file, { name: object.text, node: object }, true);
|
|
1103
|
+
const named = space?.def?.type === 'namespace' ? space.def
|
|
1104
|
+
: (this.index.symbols.get(object.text) || []).filter(def => def.type === 'namespace' && def.file === file &&
|
|
1105
|
+
!def.className && referenceScope(object, syntax, this.lexicalMemo(object), object.text) === 'module');
|
|
1106
|
+
const spaceDef = Array.isArray(named) ? (named.length === 1 ? named[0] : null) : named;
|
|
1107
|
+
if (!spaceDef) return null;
|
|
1108
|
+
const inner = classDefsNamed(this.index, property.text).entries.filter(candidate =>
|
|
1109
|
+
candidate.def.file === spaceDef.file && candidate.def.startLine > spaceDef.startLine &&
|
|
1110
|
+
candidate.def.endLine <= spaceDef.endLine &&
|
|
1111
|
+
!(this.index.files.get(spaceDef.file)?.symbols || []).some(other => other !== spaceDef &&
|
|
1112
|
+
other.type === 'namespace' && other.startLine > spaceDef.startLine && other.endLine <= spaceDef.endLine &&
|
|
1113
|
+
other.startLine < candidate.def.startLine && other.endLine >= candidate.def.endLine));
|
|
1114
|
+
return inner.length === 1 ? { name: property.text, key: inner[0].key, def: inner[0].def } : null;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
if (!declaration || declaration === 'unknown') return null;
|
|
1118
|
+
const entries = classDefsNamed(this.index, declaration.name).entries.filter(candidate =>
|
|
1119
|
+
candidate.def.file === declaration.file && !candidate.def.className && candidate.def.lexicalScopeStartLine == null);
|
|
1120
|
+
return entries.length === 1 ? { name: declaration.name, key: entries[0].key, def: entries[0].def } : null;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* The modules a namespace receiver chain denotes (fix #431): an import
|
|
1125
|
+
* binding of a module namespace, then each `.name` the modules export as
|
|
1126
|
+
* a namespace (`z.core` through `export * as core from`). 'value' when a
|
|
1127
|
+
* link holds an ordinary value, 'unknown' when a link cannot be
|
|
1128
|
+
* followed, null when the head is no module-scope import binding.
|
|
1129
|
+
*/
|
|
1130
|
+
jsNamespaceHolds(file, expression, depth = 0) {
|
|
1131
|
+
if (depth > 4) return 'unknown';
|
|
1132
|
+
if (expression?.type === 'identifier') {
|
|
1133
|
+
const language = this.index.files.get(file)?.language;
|
|
1134
|
+
const lexical = referenceScope(expression, language === 'html' ? 'javascript' : language,
|
|
1135
|
+
this.lexicalMemo(expression), expression.text);
|
|
1136
|
+
if (lexical !== 'module') return lexical === 'unknown' ? 'unknown' : null;
|
|
1137
|
+
const entry = this.index.files.get(file);
|
|
1138
|
+
const bindings = (entry?.importBindings || []).filter(binding =>
|
|
1139
|
+
(binding.alias || binding.name) === expression.text && binding.module != null);
|
|
1140
|
+
if (bindings.length === 0) return null;
|
|
1141
|
+
return mergeHolds(bindings.map(binding => {
|
|
1142
|
+
if (binding.kind !== 'namespace' && binding.kind !== 'require') return this.jsBindingHolds(file, binding);
|
|
1143
|
+
const module = entry && this.moduleFile(entry, binding.module);
|
|
1144
|
+
return module ? { modules: [module] } : 'unknown';
|
|
1145
|
+
}));
|
|
1146
|
+
}
|
|
1147
|
+
if (expression?.type !== 'member_expression') return null;
|
|
1148
|
+
const property = expression.childForFieldName('property');
|
|
1149
|
+
if (property?.type !== 'property_identifier') return null;
|
|
1150
|
+
const inner = this.jsNamespaceHolds(file, expression.childForFieldName('object'), depth + 1);
|
|
1151
|
+
if (!inner?.modules) return inner;
|
|
1152
|
+
return mergeHolds(inner.modules.map(module => this.jsExportHolds(module, property.text, 1) || 'unknown'));
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* An import binding of a module's whole value at this token (fix #431):
|
|
1157
|
+
* `const req = require('./request')` of a module whose `module.exports`
|
|
1158
|
+
* is the pin, `import req from './request'` of one whose default export
|
|
1159
|
+
* is the pin. The access verdict, or null.
|
|
1160
|
+
*/
|
|
1161
|
+
jsWholeModuleImport(file, entry, node) {
|
|
1162
|
+
const line = node.startPosition.row + 1;
|
|
1163
|
+
const binding = (entry.importBindings || []).find(candidate => candidate.line === line &&
|
|
1164
|
+
(candidate.alias || candidate.name) === node.text && candidate.module != null &&
|
|
1165
|
+
(candidate.kind === 'default' || candidate.kind === 'require' && candidate.defaultLike));
|
|
1166
|
+
const access = binding && this.wholeModuleValue(entry, binding);
|
|
1167
|
+
return access ? { ...access, accessKind: 'import' } : null;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/** The access verdict when `binding` imports a module whose whole value
|
|
1171
|
+
* (`module.exports`, the default export) is the pin, else null. */
|
|
1172
|
+
wholeModuleValue(entry, binding) {
|
|
1173
|
+
const module = this.moduleFile(entry, binding.module);
|
|
1174
|
+
if (!module || module !== this.def.file || this.def.className || this.def.lexicalScopeStartLine != null) return null;
|
|
1175
|
+
const whole = (this.index.files.get(module)?.exportDetails || []).filter(detail =>
|
|
1176
|
+
binding.kind === 'default' ? detail.type === 'default'
|
|
1177
|
+
: detail.type === 'module.exports' && detail.defaultLike);
|
|
1178
|
+
if (whole.length === 0) return null;
|
|
1179
|
+
const names = whole.map(detail => detail.localName || detail.name);
|
|
1180
|
+
if (names.every(name => name === this.def.name)) return { verdict: 'yes' };
|
|
1181
|
+
return names.some(name => name === this.def.name)
|
|
1182
|
+
? { verdict: 'unknown', reason: 'module-value-alternatives' } : null;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/** A whole-module import binds the module's value under a name of the
|
|
1186
|
+
* importer's own (fix #431): its reads depend on the pin, a rename keeps
|
|
1187
|
+
* them. */
|
|
1188
|
+
importBindingVerdict(file, entry, binding, name) {
|
|
1189
|
+
if (this.family === 'js' && !this.owner && binding.module != null &&
|
|
1190
|
+
(binding.kind === 'default' || binding.kind === 'require' && binding.defaultLike)) {
|
|
1191
|
+
const access = this.wholeModuleValue(entry, binding);
|
|
1192
|
+
if (access) return { verdict: 'no', reason: 'import-alias', access };
|
|
1193
|
+
}
|
|
1194
|
+
return super.importBindingVerdict(file, entry, binding, name);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* A JS/TS member access `R.name` against a module-scope value (fix
|
|
1199
|
+
* #420): only a module namespace or exports object holds the module's
|
|
1200
|
+
* bindings as members. A receiver a local binding shadows, or an
|
|
1201
|
+
* imported value (named or default import of a declaration), holds
|
|
1202
|
+
* ordinary properties. A CommonJS export key (`exports.name = ..`) is
|
|
1203
|
+
* a property of the exports object, not the module's binding. Null when
|
|
1204
|
+
* the module-attribute lookup decides.
|
|
1205
|
+
*/
|
|
1206
|
+
jsModuleMemberAt(file, receiver) {
|
|
1207
|
+
// `require('./m').name` reads what module m binds (fix #434): its
|
|
1208
|
+
// own declaration, or a CommonJS export key of its exports object.
|
|
1209
|
+
if (receiver.type === 'call_expression') {
|
|
1210
|
+
const callee = receiver.childForFieldName('function');
|
|
1211
|
+
const args = (receiver.childForFieldName('arguments')?.namedChildren || [])
|
|
1212
|
+
.filter(arg => !arg.type.endsWith('comment'));
|
|
1213
|
+
if (callee?.type !== 'identifier' || callee.text !== 'require' || args.length !== 1 ||
|
|
1214
|
+
args[0].type !== 'string') return null;
|
|
1215
|
+
const language = this.index.files.get(file)?.language;
|
|
1216
|
+
const entry = this.index.files.get(file);
|
|
1217
|
+
// The module scope's `require`, never a local or a module's own.
|
|
1218
|
+
if (referenceScope(callee, language === 'html' ? 'javascript' : language, this.lexicalMemo(callee),
|
|
1219
|
+
'require') !== 'module' || (entry?.symbols || []).some(def => def.name === 'require' && !def.className) ||
|
|
1220
|
+
(entry?.importBindings || []).some(binding => (binding.alias || binding.name) === 'require')) {
|
|
1221
|
+
return { verdict: 'unknown', reason: 'receiver-scope' };
|
|
1222
|
+
}
|
|
1223
|
+
const spec = args[0].namedChildren.find(child => child.type === 'string_fragment')?.text ?? '';
|
|
1224
|
+
const target = this.moduleFile(entry, spec);
|
|
1225
|
+
if (!target) {
|
|
1226
|
+
const { _unresolvedModuleIsGap } = require('./callers');
|
|
1227
|
+
return _unresolvedModuleIsGap(this.index, spec) ? { verdict: 'unknown', reason: 'module-attribute' }
|
|
1228
|
+
: { verdict: 'no', reason: 'other-module' };
|
|
1229
|
+
}
|
|
1230
|
+
const own = (this.index.files.get(target)?.exportDetails || []).filter(detail =>
|
|
1231
|
+
(detail.alias || detail.name) === this.name);
|
|
1232
|
+
const keyed = own.filter(detail => detail.type === 'exports' || detail.type === 'module.exports');
|
|
1233
|
+
if (own.length > 0 && keyed.length === own.length) return { verdict: 'no', reason: 'export-key' };
|
|
1234
|
+
if (keyed.length > 0) return { verdict: 'unknown', reason: 'export-key' };
|
|
1235
|
+
const reach = this.chase(target, this.name);
|
|
1236
|
+
return reach === 'yes' ? { verdict: 'yes' } : reach === 'no' ? { verdict: 'no', reason: 'other-module' }
|
|
1237
|
+
: { verdict: 'unknown', reason: 'module-attribute' };
|
|
1238
|
+
}
|
|
1239
|
+
if (receiver.type === 'member_expression') {
|
|
1240
|
+
// `z.core.name` through a re-exported namespace (fix #431).
|
|
1241
|
+
const holds = this.jsNamespaceHolds(file, receiver);
|
|
1242
|
+
if (!holds) return null;
|
|
1243
|
+
if (holds === 'value') return { verdict: 'no', reason: 'member' };
|
|
1244
|
+
if (!holds.modules) return { verdict: 'unknown', reason: 'module-attribute' };
|
|
1245
|
+
const reach = holds.modules.map(module => this.chase(module, this.name));
|
|
1246
|
+
return reach.every(verdict => verdict === 'yes') ? { verdict: 'yes' }
|
|
1247
|
+
: reach.every(verdict => verdict === 'no') ? { verdict: 'no', reason: 'other-module' }
|
|
1248
|
+
: { verdict: 'unknown', reason: 'module-attribute' };
|
|
1249
|
+
}
|
|
1250
|
+
if (receiver.type !== 'identifier') return null;
|
|
1251
|
+
const language = this.index.files.get(file)?.language;
|
|
1252
|
+
const lexical = referenceScope(receiver, language === 'html' ? 'javascript' : language,
|
|
1253
|
+
this.lexicalMemo(receiver), receiver.text);
|
|
1254
|
+
if (lexical === 'none') return null;
|
|
1255
|
+
if (!lexical || lexical === 'unknown') return { verdict: 'unknown', reason: 'receiver-scope' };
|
|
1256
|
+
if (lexical !== 'module') return { verdict: 'no', reason: 'member' };
|
|
1257
|
+
const entry = this.index.files.get(file);
|
|
1258
|
+
const bindings = (entry?.importBindings || []).filter(binding =>
|
|
1259
|
+
(binding.alias || binding.name) === receiver.text && binding.module != null);
|
|
1260
|
+
if (bindings.length === 0) return null;
|
|
1261
|
+
const holds = bindings.map(binding => this.jsBindingHolds(file, binding));
|
|
1262
|
+
if (holds.every(hold => hold === 'value')) return { verdict: 'no', reason: 'member' };
|
|
1263
|
+
// A re-exported namespace names the modules it re-exports.
|
|
1264
|
+
if (holds.every(hold => hold?.modules)) {
|
|
1265
|
+
const reach = holds.flatMap(hold => hold.modules).map(module => this.chase(module, this.name));
|
|
1266
|
+
return reach.every(verdict => verdict === 'yes') ? { verdict: 'yes' }
|
|
1267
|
+
: reach.every(verdict => verdict === 'no') ? { verdict: 'no', reason: 'other-module' }
|
|
1268
|
+
: { verdict: 'unknown', reason: 'module-attribute' };
|
|
1269
|
+
}
|
|
1270
|
+
if (holds.some(hold => hold !== 'namespace')) return { verdict: 'unknown', reason: 'import-binding-value' };
|
|
1271
|
+
const keyed = bindings.filter(binding => binding.kind === 'require').map(binding => {
|
|
1272
|
+
const target = this.moduleFile(entry, binding.module);
|
|
1273
|
+
const exports = target ? this.index.files.get(target)?.exportDetails || [] : [];
|
|
1274
|
+
const own = exports.filter(detail => (detail.alias || detail.name) === this.name);
|
|
1275
|
+
return own.length > 0 && own.every(detail => detail.type === 'exports' || detail.type === 'module.exports');
|
|
1276
|
+
});
|
|
1277
|
+
if (keyed.length > 0 && keyed.every(Boolean)) return { verdict: 'no', reason: 'export-key' };
|
|
1278
|
+
if (keyed.some(Boolean)) return { verdict: 'unknown', reason: 'export-key' };
|
|
1279
|
+
return null;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/**
|
|
1283
|
+
* A bare name that provably never denotes a class or enum member here
|
|
1284
|
+
* (fix #420): a language without implicit `this` reaches member values
|
|
1285
|
+
* by name only inside the declaring enum or namespace (JS/TS), directly
|
|
1286
|
+
* in a class body (Python), through imported variants (Rust), or never.
|
|
1287
|
+
*/
|
|
1288
|
+
bareMemberVerdict(file, node, language) {
|
|
1289
|
+
const traits = langTraits(language);
|
|
1290
|
+
const miss = { verdict: 'no', reason: 'bare-name-not-member' };
|
|
1291
|
+
if (!traits || traits.bareCallReachesMethods) return null;
|
|
1292
|
+
switch (traits.bareValueMemberScope) {
|
|
1293
|
+
case 'none': return miss;
|
|
1294
|
+
case 'imports': return this.def.type !== 'variant' ? miss : null;
|
|
1295
|
+
case 'class-body': {
|
|
1296
|
+
const lexical = this.pythonScope(node, this.name);
|
|
1297
|
+
// A class body read after its binding reads that class's
|
|
1298
|
+
// attribute (statement order).
|
|
1299
|
+
if (lexical?.classBody) return this.memberAt(this.enclosingOwner(file, node));
|
|
1300
|
+
return lexical === 'module' || (!!lexical && typeof lexical === 'object') ? miss : null;
|
|
1301
|
+
}
|
|
1302
|
+
case 'declaration-body':
|
|
1303
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
1304
|
+
if (['enum_declaration', 'enum_body', 'internal_module', 'module'].includes(parent.type)) return null;
|
|
1305
|
+
}
|
|
1306
|
+
return miss;
|
|
1307
|
+
default: return null;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* The declared type of the object literal holding a JS/TS property
|
|
1313
|
+
* (fix #420): `const v: T = { key: .. }`, `{ .. } as T`, `{ .. }
|
|
1314
|
+
* satisfies T`. Its key is that type's member. Null when no written
|
|
1315
|
+
* type gives the literal its contract.
|
|
1316
|
+
*/
|
|
1317
|
+
objectLiteralOwner(file, pair) {
|
|
1318
|
+
const object = pair.parent;
|
|
1319
|
+
if (object?.type !== 'object') return null;
|
|
1320
|
+
const holder = object.parent;
|
|
1321
|
+
let type = null;
|
|
1322
|
+
if (holder?.type === 'variable_declarator' && sameNode(holder.childForFieldName('value'), object)) {
|
|
1323
|
+
type = holder.childForFieldName('type');
|
|
1324
|
+
} else if ((holder?.type === 'as_expression' || holder?.type === 'satisfies_expression') &&
|
|
1325
|
+
sameNode(holder.namedChild(0), object)) {
|
|
1326
|
+
type = holder.namedChild(1);
|
|
1327
|
+
}
|
|
1328
|
+
const syntax = type && typeSyntax(type);
|
|
1329
|
+
return syntax ? this.classForType(file, syntax) : null;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
declarationNode(definition) {
|
|
1333
|
+
const file = definition.file, source = this.index._readFile(file);
|
|
1334
|
+
const language = this.index.files.get(file)?.language;
|
|
1335
|
+
for (const tree of declarationTrees(this.index, file, source, language)) {
|
|
1336
|
+
const declarations = this.syntaxNodes(file, tree, definition.name).filter(token =>
|
|
1337
|
+
token.node.startPosition.row + 1 === (definition.nameLine || definition.startLine) &&
|
|
1338
|
+
(token.role.role === 'decl' || token.role.binding ||
|
|
1339
|
+
['field-name', 'member-name', 'property-name'].includes(token.role.why)));
|
|
1340
|
+
if (declarations.length > 1) return null;
|
|
1341
|
+
if (declarations.length === 1) return declarations[0].role.declNode || declarations[0].node.parent;
|
|
1342
|
+
}
|
|
1343
|
+
return null;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
typeVerdict(file, syntax, definition, useFile = file) {
|
|
1347
|
+
const resolver = new TypeReferenceResolver(this.index, definition, syntax.name);
|
|
1348
|
+
if (langTraits(this.index.files.get(file)?.language)?.textualIncludes &&
|
|
1349
|
+
TYPE_KINDS.has(definition.type) && !definition.aliasOf) {
|
|
1350
|
+
const canonical = completeForwardDeclaration(this.index, definition, useFile);
|
|
1351
|
+
resolver.group = new Set([canonical]);
|
|
1352
|
+
for (const other of resolver.all) {
|
|
1353
|
+
if (other.forwardDeclaration &&
|
|
1354
|
+
completeForwardDeclaration(this.index, other, useFile) === canonical) resolver.group.add(other);
|
|
1355
|
+
}
|
|
1356
|
+
resolver.pinFiles = new Set([...resolver.group].map(d => d.file));
|
|
1357
|
+
}
|
|
1358
|
+
resolver.siteNode = syntax.node;
|
|
1359
|
+
resolver.siteFile = file;
|
|
1360
|
+
return langTraits(this.index.files.get(file)?.language)?.textualIncludes
|
|
1361
|
+
? resolver.resolveCFamily(file, syntax.node.startPosition.row + 1,
|
|
1362
|
+
syntax.name, syntax.qualifierText || null,
|
|
1363
|
+
referenceRole(syntax.node, this.index.files.get(file)?.language))
|
|
1364
|
+
: syntax.qualifierText
|
|
1365
|
+
? resolver.resolveQualified(file, syntax.node.startPosition.row + 1,
|
|
1366
|
+
syntax.qualifier, syntax.qualifierText, { role: 'type' })
|
|
1367
|
+
: resolver.resolveName(file, syntax.node.startPosition.row + 1, syntax.name);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
classForType(file, syntax, runtime = false, useFile = file) {
|
|
1371
|
+
if (!syntax) return null;
|
|
1372
|
+
const unit = langTraits(this.language)?.textualIncludes ? useFile : '';
|
|
1373
|
+
const key = `${file}\0${syntax.node.startIndex}\0${syntax.name}\0${syntax.qualifierText || ''}\0${runtime}\0${unit}`;
|
|
1374
|
+
if (this.typeMemo.has(key)) return this.typeMemo.get(key);
|
|
1375
|
+
this.typeMemo.set(key, null); // Alias cycles never prove an owner.
|
|
1376
|
+
let localTypes = null;
|
|
1377
|
+
if (runtime && langTraits(this.index.files.get(file)?.language)?.typeSystem === 'structural') {
|
|
1378
|
+
const local = this.localBinding(file, syntax.node, syntax.name, true);
|
|
1379
|
+
if (local) {
|
|
1380
|
+
const scope = referenceScope(syntax.node,
|
|
1381
|
+
GRAMMAR_LANGUAGES.get(syntax.node.tree.language?.name) || this.index.files.get(file)?.language,
|
|
1382
|
+
this.lexicalMemo(syntax.node), syntax.name, { includeClasses: true });
|
|
1383
|
+
// A local class declaration is the constructor binding,
|
|
1384
|
+
// not a shadow of it. Parameters, assignments and mixed
|
|
1385
|
+
// bindings have no declaration-only scope proof.
|
|
1386
|
+
if (local.binding || !scope?.local || !scope.defRows?.length) return null;
|
|
1387
|
+
const rows = new Set(scope.defRows.map(row => row + 1));
|
|
1388
|
+
localTypes = new Set(classDefsNamed(this.index, syntax.name).entries
|
|
1389
|
+
.filter(entry => entry.def.file === file &&
|
|
1390
|
+
rows.has(entry.def.nameLine || entry.def.startLine)).map(entry => entry.def));
|
|
1391
|
+
if (localTypes.size !== 1 || rows.size !== 1) return null;
|
|
1392
|
+
}
|
|
1393
|
+
const rebound = this.syntaxNodes(file, syntax.node.tree, syntax.name).some(token =>
|
|
1394
|
+
isWrittenBinding(token.node) && !this.localBinding(file, token.node, syntax.name, true));
|
|
1395
|
+
if (rebound) return null;
|
|
1396
|
+
}
|
|
1397
|
+
const parameters = this.syntaxNodes(file, syntax.node.tree, syntax.name).some(token =>
|
|
1398
|
+
token.role.typeParam && contains(referenceTypeParameterScope(token.node), syntax.node));
|
|
1399
|
+
const matches = [];
|
|
1400
|
+
let uncertain = parameters;
|
|
1401
|
+
if (this.ambiguousClassFamily({ name: syntax.name })) uncertain = true;
|
|
1402
|
+
let classes = classDefsNamed(this.index, syntax.name).entries;
|
|
1403
|
+
let aliases = (this.index.symbols.get(syntax.name) || []).filter(d => d.type === 'type');
|
|
1404
|
+
let namedTypes = localTypes;
|
|
1405
|
+
if (localTypes) {
|
|
1406
|
+
classes = classes.filter(entry => localTypes.has(entry.def));
|
|
1407
|
+
aliases = [];
|
|
1408
|
+
}
|
|
1409
|
+
const language = this.index.files.get(file)?.language;
|
|
1410
|
+
if (!parameters && langTraits(language)?.itemScopeResolution) {
|
|
1411
|
+
const written = [syntax.qualifierText, syntax.name].filter(Boolean).join('::');
|
|
1412
|
+
const binding = LANGUAGES[language].module().typePathScopeBinding?.(syntax.node, written);
|
|
1413
|
+
const named = require('./rust-names').resolveRustPathQualifier(this.index, file,
|
|
1414
|
+
syntax.node.startPosition.row + 1, written, binding);
|
|
1415
|
+
// Type lookup follows import aliases; a rename of the original
|
|
1416
|
+
// type deliberately leaves those aliases alone. Reuse the name
|
|
1417
|
+
// resolver's binding here instead of its rename-token verdict.
|
|
1418
|
+
if (named) {
|
|
1419
|
+
namedTypes = new Set(named.defs || []);
|
|
1420
|
+
classes = [...namedTypes].flatMap(def => classDefsNamed(this.index, def.name).entries
|
|
1421
|
+
.filter(entry => entry.def === def));
|
|
1422
|
+
aliases = [...namedTypes].filter(def => def.type === 'type');
|
|
1423
|
+
} else if (binding) return null;
|
|
1424
|
+
}
|
|
1425
|
+
if (!parameters) for (const entry of classes) {
|
|
1426
|
+
if (entry.def.file === file && this.scopeRelation(file,
|
|
1427
|
+
entry.def.nameLine || entry.def.startLine, syntax.node) !== 'yes') {
|
|
1428
|
+
uncertain = true;
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
1431
|
+
const verdict = namedTypes?.has(entry.def) ? { verdict: 'yes' }
|
|
1432
|
+
: this.typeVerdict(file, syntax, entry.def, useFile);
|
|
1433
|
+
if (entry.def.forwardDeclaration && verdict.verdict !== 'no') this.sawForwardDeclaration = true;
|
|
1434
|
+
if (verdict.verdict === 'yes') {
|
|
1435
|
+
const complete = completeForwardDeclaration(this.index, entry.def, useFile);
|
|
1436
|
+
const resolved = complete === entry.def ? entry :
|
|
1437
|
+
classDefsNamed(this.index, complete.name).entries.find(candidate => candidate.def === complete);
|
|
1438
|
+
if (resolved) matches.push(resolved);
|
|
1439
|
+
else uncertain = true;
|
|
1440
|
+
}
|
|
1441
|
+
if (verdict.verdict === 'unknown') uncertain = true;
|
|
1442
|
+
}
|
|
1443
|
+
if (!parameters) for (const alias of aliases) {
|
|
1444
|
+
const verdict = namedTypes?.has(alias) ? { verdict: 'yes' }
|
|
1445
|
+
: this.typeVerdict(file, syntax, alias, useFile);
|
|
1446
|
+
if (verdict.verdict === 'unknown') uncertain = true;
|
|
1447
|
+
if (verdict.verdict !== 'yes') continue;
|
|
1448
|
+
const declaration = this.declarationNode(alias);
|
|
1449
|
+
const target = declaration?.childForFieldName('type') || declaration?.childForFieldName('right');
|
|
1450
|
+
const owner = this.classForType(alias.file, typeSyntax(target), false, useFile);
|
|
1451
|
+
if (owner) matches.push(owner);
|
|
1452
|
+
else uncertain = true;
|
|
1453
|
+
}
|
|
1454
|
+
const keys = new Set(matches.map(entry => entry.key));
|
|
1455
|
+
const result = !uncertain && keys.size === 1
|
|
1456
|
+
? { name: matches[0].def.name, key: matches[0].key, def: matches[0].def } : null;
|
|
1457
|
+
this.typeMemo.set(key, result);
|
|
1458
|
+
return result;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
enclosingOwner(file, node, fallback = true) {
|
|
1462
|
+
const implementation = langTraits(this.index.files.get(file)?.language)?.implementationOwnerSyntax;
|
|
1463
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
1464
|
+
if (implementation && current.type === implementation.node) {
|
|
1465
|
+
// A separately declared implementation may name an imported
|
|
1466
|
+
// or aliased type. A project namesake is no owner evidence.
|
|
1467
|
+
const owner = this.classForType(file, typeSyntax(current.childForFieldName(implementation.field)));
|
|
1468
|
+
return owner ? { ...owner, node: current } : null;
|
|
1469
|
+
}
|
|
1470
|
+
if (!TYPE_BODIES.has(current.type)) continue;
|
|
1471
|
+
const nameNode = current.childForFieldName('name');
|
|
1472
|
+
const name = typeSyntax(nameNode)?.name || nameNode?.text;
|
|
1473
|
+
if (!name) {
|
|
1474
|
+
// Anonymous typedef records and class expressions acquire
|
|
1475
|
+
// their indexed name from the declaration that binds them.
|
|
1476
|
+
// Only the exact indexed body span proves their owner.
|
|
1477
|
+
const owners = this.members.filter(member => member.file === file &&
|
|
1478
|
+
member.startLine >= current.startPosition.row + 1 &&
|
|
1479
|
+
member.endLine <= current.endPosition.row + 1)
|
|
1480
|
+
.map(member => ownerRefOf(this.index, member)).filter(owner =>
|
|
1481
|
+
owner?.def?.startLine === current.startPosition.row + 1 &&
|
|
1482
|
+
owner.def.endLine === current.endPosition.row + 1);
|
|
1483
|
+
if (new Set(owners.map(owner => owner.key)).size === 1) return { ...owners[0], node: current };
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
const matches = classDefsNamed(this.index, name).entries.filter(entry =>
|
|
1487
|
+
entry.def.file === file && entry.def.startLine <= current.startPosition.row + 1 &&
|
|
1488
|
+
entry.def.endLine >= current.endPosition.row + 1);
|
|
1489
|
+
if (new Set(matches.map(entry => entry.key)).size === 1) return {
|
|
1490
|
+
name, key: matches[0].key, def: matches[0].def, node: current,
|
|
1491
|
+
};
|
|
1492
|
+
return null;
|
|
1493
|
+
}
|
|
1494
|
+
if (!fallback) return null;
|
|
1495
|
+
const member = this.index.findEnclosingFunction(file, node.startPosition.row + 1, true);
|
|
1496
|
+
return member?.className ? ownerRefOf(this.index, member) : null;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
declarationOwner(file, node) {
|
|
1500
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
1501
|
+
if (CALLABLE_SCOPES.has(current.type)) return null;
|
|
1502
|
+
if (current.type === 'object_type' || current.type === 'struct_type' &&
|
|
1503
|
+
current.parent?.type !== 'type_spec') return null;
|
|
1504
|
+
if (TYPE_BODIES.has(current.type)) return this.enclosingOwner(file, node);
|
|
1505
|
+
}
|
|
1506
|
+
return null;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
privateValueAt(file, node) {
|
|
1510
|
+
let position = node;
|
|
1511
|
+
const visited = new Set();
|
|
1512
|
+
while (position) {
|
|
1513
|
+
const owner = this.enclosingOwner(file, position, false);
|
|
1514
|
+
if (!owner?.key || !owner.node || visited.has(owner.node.startIndex)) break;
|
|
1515
|
+
visited.add(owner.node.startIndex);
|
|
1516
|
+
const declared = this.members.filter(member => ownerRefOf(this.index, member)?.key === owner.key);
|
|
1517
|
+
if (declared.length) return this.judge(declared, 'other-private-owner');
|
|
1518
|
+
position = owner.node;
|
|
1519
|
+
}
|
|
1520
|
+
return { verdict: 'unknown', reason: 'private-member-owner-unresolved' };
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
memberAt(owner) {
|
|
1524
|
+
if (langTraits(this.language)?.typeArgumentValues === 'ordinary') return this.cppMemberAt(owner);
|
|
1525
|
+
if (!owner?.key || !this.owner?.key) return { verdict: 'unknown', reason: 'member-owner-unresolved' };
|
|
1526
|
+
if (this.ambiguousClassFamily(owner)) return { verdict: 'unknown', reason: 'value-owner-alternatives' };
|
|
1527
|
+
if (langTraits(this.language)?.separateStaticStorage && owner.valueStorage &&
|
|
1528
|
+
owner.valueStorage !== this.targetStorage) return { verdict: 'no', reason: 'other-value-storage' };
|
|
1529
|
+
if (this.memberMemo.has(owner.key)) return this.memberMemo.get(owner.key);
|
|
1530
|
+
const declaring = findDeclaringClass(this.index, owner, this.memberCandidates);
|
|
1531
|
+
if (!declaring || declaring.uncertain) return { verdict: 'unknown', reason: 'member-owner-unresolved' };
|
|
1532
|
+
if (declaring.members.length !== 1) return { verdict: 'unknown', reason: 'ambiguous-value-declaration' };
|
|
1533
|
+
const result = this.group.has(declaring.members[0])
|
|
1534
|
+
? declaring.members[0] === this.def ? { verdict: 'yes' }
|
|
1535
|
+
: { verdict: 'yes', access: this.slotAccess(owner, declaring.members[0]) }
|
|
1536
|
+
: langTraits(this.language)?.typeSystem === 'structural' &&
|
|
1537
|
+
(relationToTargets(this.index, owner, [this.def]) !== 'unrelated' ||
|
|
1538
|
+
relationToTargets(this.index, this.owner, declaring.members) !== 'unrelated')
|
|
1539
|
+
? { verdict: 'unknown', reason: 'inherited-value-shadow' }
|
|
1540
|
+
: { verdict: 'no', reason: 'other-member-owner' };
|
|
1541
|
+
this.memberMemo.set(owner.key, result);
|
|
1542
|
+
return result;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
/**
|
|
1546
|
+
* The field a keyword argument names (fix #431), Python: the keyword of
|
|
1547
|
+
* a construction `C(.., name=v)` binds a parameter of the initializer
|
|
1548
|
+
* the call runs. A class the language's field initializers synthesize
|
|
1549
|
+
* (trait `fieldInitializers`: a dataclass, a NamedTuple or TypedDict
|
|
1550
|
+
* subclass) takes its own fields as parameters, so the keyword names
|
|
1551
|
+
* the field C's member lookup finds; an explicit `__init__` takes
|
|
1552
|
+
* parameters of its own. An initializer C inherits, a class decorator
|
|
1553
|
+
* that may replace C, an external base or a `cls(..)` construction
|
|
1554
|
+
* (a subclass may run) leave the binding for review.
|
|
1555
|
+
*/
|
|
1556
|
+
keywordFieldAt(file, entry, node) {
|
|
1557
|
+
const other = { verdict: 'no', reason: 'keyword-argument' };
|
|
1558
|
+
const list = node.parent?.parent;
|
|
1559
|
+
const call = list?.type === 'argument_list' ? list.parent : null;
|
|
1560
|
+
const callee = call?.type === 'call' ? call.childForFieldName('function') : null;
|
|
1561
|
+
if (!callee) return other;
|
|
1562
|
+
const traits = langTraits(entry.language);
|
|
1563
|
+
let owner;
|
|
1564
|
+
let receiverClass = false;
|
|
1565
|
+
if (callee.type === 'identifier' && traits.selfParam?.includes(callee.text)) {
|
|
1566
|
+
owner = this.enclosingOwner(file, callee);
|
|
1567
|
+
receiverClass = true;
|
|
1568
|
+
} else {
|
|
1569
|
+
// Only a class of the slot, or one below it, can take the field.
|
|
1570
|
+
const calleeName = callee.type === 'attribute' ? callee.childForFieldName('attribute')?.text : callee.text;
|
|
1571
|
+
if (!this.keywordOwnerNames().has(calleeName) && !this.keywordAliasNames().has(calleeName)) return other;
|
|
1572
|
+
const syntax = callee.type === 'attribute'
|
|
1573
|
+
? (() => {
|
|
1574
|
+
const attribute = callee.childForFieldName('attribute');
|
|
1575
|
+
const object = callee.childForFieldName('object');
|
|
1576
|
+
return attribute && object ? { name: attribute.text, node: attribute, qualifier: object,
|
|
1577
|
+
qualifierText: object.text } : null;
|
|
1578
|
+
})()
|
|
1579
|
+
: typeSyntax(callee);
|
|
1580
|
+
owner = syntax ? this.classForType(file, syntax, true) : null;
|
|
1581
|
+
// A module value naming a class (`Seg = Segment`, fix #434).
|
|
1582
|
+
if (!owner && callee.type === 'identifier' && this.keywordAliasNames().has(callee.text) &&
|
|
1583
|
+
this.pythonScope(callee, callee.text) === 'module') {
|
|
1584
|
+
const aliased = require('./callers')._moduleValueAliasType(this.index, file, callee.text);
|
|
1585
|
+
const entries = aliased ? classDefsNamed(this.index, aliased.type).entries
|
|
1586
|
+
.filter(candidate => candidate.def.file === aliased.fromFile && !candidate.def.className) : [];
|
|
1587
|
+
if (entries.length !== 1) return { verdict: 'unknown', reason: 'construction-alias' };
|
|
1588
|
+
owner = { name: aliased.type, key: entries[0].key, def: entries[0].def };
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
if (!owner?.key || !owner.def) return { verdict: 'unknown', reason: 'construction-owner-unresolved' };
|
|
1592
|
+
const declaring = findDeclaringClass(this.index, owner, this.memberCandidates);
|
|
1593
|
+
if (!declaring) return other;
|
|
1594
|
+
if (declaring.uncertain || declaring.members.length !== 1) {
|
|
1595
|
+
return { verdict: 'unknown', reason: 'member-owner-unresolved' };
|
|
1596
|
+
}
|
|
1597
|
+
const member = declaring.members[0];
|
|
1598
|
+
if (!this.group.has(member)) return { verdict: 'no', reason: 'other-member-owner' };
|
|
1599
|
+
const initializer = this.synthesizedInitializer(owner);
|
|
1600
|
+
if (initializer === 'explicit') return other;
|
|
1601
|
+
if (initializer !== 'own') return { verdict: 'unknown', reason: initializer };
|
|
1602
|
+
// A field a base declares reaches the synthesized parameter through
|
|
1603
|
+
// the base's fields.
|
|
1604
|
+
if (declaring.ref?.key !== owner.key) return { verdict: 'unknown', reason: 'inherited-field' };
|
|
1605
|
+
if (receiverClass) return { verdict: 'unknown', reason: 'receiver-class-call' };
|
|
1606
|
+
return member === this.def ? { verdict: 'yes' }
|
|
1607
|
+
: { verdict: 'yes', access: this.slotAccess(owner, member) };
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
/** Value aliases of the classes keywordOwnerNames lists (fix #434). */
|
|
1611
|
+
keywordAliasNames() {
|
|
1612
|
+
if (this._keywordAliasNames) return this._keywordAliasNames;
|
|
1613
|
+
const classes = [...this.keywordOwnerNames()].flatMap(name => classDefsNamed(this.index, name).entries
|
|
1614
|
+
.map(entry => entry.def));
|
|
1615
|
+
this._keywordAliasNames = classes.length > 0
|
|
1616
|
+
? require('./constructor-calls').constructionAliasNames(this.index, classes) : new Set();
|
|
1617
|
+
return this._keywordAliasNames;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
/** Names of the classes owning a member of the pin's slot and of their
|
|
1621
|
+
* descendants (by name): the only constructions whose keywords can name
|
|
1622
|
+
* the field. */
|
|
1623
|
+
keywordOwnerNames() {
|
|
1624
|
+
if (this._keywordOwnerNames) return this._keywordOwnerNames;
|
|
1625
|
+
const names = new Set();
|
|
1626
|
+
const queue = [];
|
|
1627
|
+
for (const member of this.group) {
|
|
1628
|
+
if (member.className && !names.has(member.className)) { names.add(member.className); queue.push(member.className); }
|
|
1629
|
+
}
|
|
1630
|
+
while (queue.length) {
|
|
1631
|
+
for (const child of this.index.extendedByGraph?.get(queue.shift()) || []) {
|
|
1632
|
+
const name = typeof child === 'string' ? child : child?.name;
|
|
1633
|
+
if (name && !names.has(name)) { names.add(name); queue.push(name); }
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
this._keywordOwnerNames = names;
|
|
1637
|
+
return names;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* Who builds the initializer a construction of `owner` runs (fix #431):
|
|
1642
|
+
* 'own' (synthesized from the class's own fields), 'explicit' (a
|
|
1643
|
+
* project `__init__` the class or a base defines, or none at all), or
|
|
1644
|
+
* the review reason ('inherited-initializer', 'class-decorator',
|
|
1645
|
+
* 'external-initializer').
|
|
1646
|
+
*/
|
|
1647
|
+
synthesizedInitializer(owner) {
|
|
1648
|
+
const memo = this._initializers ||= new Map();
|
|
1649
|
+
if (memo.has(owner.key)) return memo.get(owner.key);
|
|
1650
|
+
const result = this.synthesizedInitializerUncached(owner);
|
|
1651
|
+
memo.set(owner.key, result);
|
|
1652
|
+
return result;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
synthesizedInitializerUncached(owner) {
|
|
1656
|
+
const vocabulary = langTraits(this.index.files.get(owner.def.file)?.language)?.fieldInitializers;
|
|
1657
|
+
const decorators = langTraits(this.index.files.get(owner.def.file)?.language)?.callableDecorators || {};
|
|
1658
|
+
if (!vocabulary) return 'external-initializer';
|
|
1659
|
+
const { resolveDecorator } = require('./async-producers');
|
|
1660
|
+
const classInfo = ref => {
|
|
1661
|
+
const fileEntry = this.index.files.get(ref.def.file);
|
|
1662
|
+
const node = this.declarationNode(ref.def);
|
|
1663
|
+
const decorated = node?.parent?.type === 'decorated_definition' ? node.parent : null;
|
|
1664
|
+
let synthesizes = false, replaces = false;
|
|
1665
|
+
for (const decorator of decorated ? decorated.namedChildren.filter(child => child.type === 'decorator') : []) {
|
|
1666
|
+
let expression = decorator.namedChild(0);
|
|
1667
|
+
const called = expression?.type === 'call';
|
|
1668
|
+
const args = called ? expression.childForFieldName('arguments') : null;
|
|
1669
|
+
if (called) expression = expression.childForFieldName('function');
|
|
1670
|
+
const resolved = expression && ['identifier', 'attribute'].includes(expression.type)
|
|
1671
|
+
? resolveDecorator(expression.text, fileEntry, decorators) : null;
|
|
1672
|
+
const key = resolved?.name;
|
|
1673
|
+
if (key && vocabulary.decorators.includes(key)) {
|
|
1674
|
+
// `@dataclass(init=False)` generates no initializer.
|
|
1675
|
+
const noInit = (args?.namedChildren || []).some(arg => arg.type === 'keyword_argument' &&
|
|
1676
|
+
arg.childForFieldName('name')?.text === 'init' &&
|
|
1677
|
+
arg.childForFieldName('value')?.type === 'false');
|
|
1678
|
+
if (!noInit) synthesizes = true;
|
|
1679
|
+
} else if ((!key || ![...(decorators.transparent || []), ...(decorators.namePreserving || [])]
|
|
1680
|
+
.some(known => known === key || known === `${key}()`)) &&
|
|
1681
|
+
!require('./decorator-binding').classPreservingDecorator(this.index, ref.def, decorator.namedChild(0)?.text || '')) {
|
|
1682
|
+
replaces = true;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
const bases = ref.def.extends ? String(ref.def.extends).split(',').map(base => base.trim()) : [];
|
|
1686
|
+
if (bases.some(base => {
|
|
1687
|
+
const resolved = resolveDecorator(base.replace(/\[.*$/s, ''), fileEntry, {});
|
|
1688
|
+
return resolved && vocabulary.bases.includes(resolved.name);
|
|
1689
|
+
})) synthesizes = true;
|
|
1690
|
+
const explicit = (this.index.symbols.get('__init__') || []).some(definition =>
|
|
1691
|
+
definition.file === ref.def.file && ownerRefOf(this.index, definition)?.key === ref.key);
|
|
1692
|
+
return { synthesizes, replaces, explicit };
|
|
1693
|
+
};
|
|
1694
|
+
// A class's own `__init__` takes its parameters, whatever its
|
|
1695
|
+
// decorators (a dataclass keeps a defined initializer).
|
|
1696
|
+
const own = classInfo(owner);
|
|
1697
|
+
if (own.explicit) return 'explicit';
|
|
1698
|
+
if (own.replaces) return 'class-decorator';
|
|
1699
|
+
if (own.synthesizes) return 'own';
|
|
1700
|
+
// The initializer comes from a base: the first class of the chain
|
|
1701
|
+
// that defines or synthesizes one. `object` takes no keywords.
|
|
1702
|
+
const queue = parentRefsOf(this.index, owner).map(parent => ({ parent, from: owner.def.file }));
|
|
1703
|
+
const seen = new Set([owner.key]);
|
|
1704
|
+
while (queue.length) {
|
|
1705
|
+
const { parent, from } = queue.shift();
|
|
1706
|
+
if (parent.external || !parent.key || !parent.def) {
|
|
1707
|
+
const resolved = resolveDecorator(String(parent.name || ''), this.index.files.get(from),
|
|
1708
|
+
{ builtins: ['object'] });
|
|
1709
|
+
if (resolved?.name === 'builtins.object') continue;
|
|
1710
|
+
if (resolved && vocabulary.bases.includes(resolved.name)) return 'inherited-initializer';
|
|
1711
|
+
return 'external-initializer';
|
|
1712
|
+
}
|
|
1713
|
+
if (seen.has(parent.key)) continue;
|
|
1714
|
+
seen.add(parent.key);
|
|
1715
|
+
const info = classInfo(parent);
|
|
1716
|
+
if (info.explicit) return 'explicit';
|
|
1717
|
+
if (info.replaces) return 'class-decorator';
|
|
1718
|
+
if (info.synthesizes) return 'inherited-initializer';
|
|
1719
|
+
queue.push(...parentRefsOf(this.index, parent).map(next => ({ parent: next, from: parent.def.file })));
|
|
1720
|
+
}
|
|
1721
|
+
return 'explicit';
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
/**
|
|
1725
|
+
* What an access whose lookup finds another member of the pin's slot
|
|
1726
|
+
* reaches (fix #431). A rename edits the whole slot; impact tiers it as
|
|
1727
|
+
* the caller contract does: a receiver of an ancestor or interface class
|
|
1728
|
+
* may hold an instance of the target's class (possible dispatch), an
|
|
1729
|
+
* override below the target and an unrelated class's own member are
|
|
1730
|
+
* other members (excluded), unless a project class descends from both.
|
|
1731
|
+
*/
|
|
1732
|
+
slotAccess(owner, member) {
|
|
1733
|
+
if (!owner?.key || !this.owner?.key) return { verdict: 'unknown', reason: 'member-owner-unresolved' };
|
|
1734
|
+
if (owner.key === this.owner.key) return { verdict: 'unknown', reason: 'value-declaration-identity' };
|
|
1735
|
+
const below = descendsFromTargets(this.index, owner, [this.def]);
|
|
1736
|
+
if (below === 'yes') {
|
|
1737
|
+
// A Python instance attribute shadows the class attribute only
|
|
1738
|
+
// once assigned.
|
|
1739
|
+
return member.instanceAttribute || this.def.instanceAttribute
|
|
1740
|
+
? { verdict: 'unknown', reason: 'instance-attribute-shadow' }
|
|
1741
|
+
: { verdict: 'no', reason: 'slot-override' };
|
|
1742
|
+
}
|
|
1743
|
+
const relation = this.supertypeOfOwner(owner) || relationToTargets(this.index, owner, [this.def]);
|
|
1744
|
+
if (relation === 'ancestor') return { verdict: 'unknown', reason: 'possible-dispatch' };
|
|
1745
|
+
if (relation === 'maybe' || below === 'maybe') return { verdict: 'unknown', reason: 'member-owner-unresolved' };
|
|
1746
|
+
return shareDescendant(this.index, owner, [this.def])
|
|
1747
|
+
? { verdict: 'unknown', reason: 'possible-dispatch' }
|
|
1748
|
+
: { verdict: 'no', reason: 'other-slot-member' };
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
/** 'ancestor' when `owner` is a supertype of the pin's class through its
|
|
1752
|
+
* base and implements clauses, 'maybe' when an unresolved supertype of
|
|
1753
|
+
* that name is on the way, else null. */
|
|
1754
|
+
supertypeOfOwner(owner) {
|
|
1755
|
+
const queue = [...supertypeRefsOf(this.index, this.owner, { implements: true })];
|
|
1756
|
+
const seen = new Set([this.owner.key]);
|
|
1757
|
+
let maybe = false;
|
|
1758
|
+
while (queue.length) {
|
|
1759
|
+
const parent = queue.shift();
|
|
1760
|
+
if (parent.key && parent.key === owner.key) return 'ancestor';
|
|
1761
|
+
if (!parent.key || parent.external) {
|
|
1762
|
+
if (parent.name === owner.name) maybe = true;
|
|
1763
|
+
continue;
|
|
1764
|
+
}
|
|
1765
|
+
if (seen.has(parent.key)) continue;
|
|
1766
|
+
seen.add(parent.key);
|
|
1767
|
+
queue.push(...supertypeRefsOf(this.index, parent, { implements: true }));
|
|
1768
|
+
}
|
|
1769
|
+
return maybe ? 'maybe' : null;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
/** The access verdict of another slot member's declaration token. */
|
|
1773
|
+
slotDeclarationAccess(file, node, otherOwner) {
|
|
1774
|
+
if (!this.owner?.key || !otherOwner?.key || otherOwner.key === this.owner.key) return null;
|
|
1775
|
+
return { verdict: 'no', reason: 'slot-member-declaration' };
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
receiverOwner(file, receiver) {
|
|
1779
|
+
if (!receiver) return null;
|
|
1780
|
+
const traits = langTraits(this.index.files.get(file)?.language);
|
|
1781
|
+
if (receiver.type === 'base' || receiver.type === 'super') {
|
|
1782
|
+
if (traits.separateStaticStorage) return null;
|
|
1783
|
+
const owner = this.enclosingOwner(file, receiver);
|
|
1784
|
+
const bases = owner && parentRefsOf(this.index, owner).filter(ref =>
|
|
1785
|
+
ref.key && ['class', 'struct', 'record'].includes(ref.def?.type));
|
|
1786
|
+
return bases?.length === 1 ? bases[0] : null;
|
|
1787
|
+
}
|
|
1788
|
+
if (receiver.type === 'this' || traits.selfParam?.includes(receiver.text)) {
|
|
1789
|
+
if (traits.assignmentDeclaresLocal && traits.ownClassMethod) {
|
|
1790
|
+
const local = this.localBinding(file, receiver, receiver.text, true);
|
|
1791
|
+
const scope = local?.binding?.scope;
|
|
1792
|
+
const method = scope && (this.index.files.get(file)?.symbols || []).find(def =>
|
|
1793
|
+
def.className && def.name === scope.childForFieldName('name')?.text &&
|
|
1794
|
+
def.startLine <= scope.startPosition.row + 1 && def.endLine === scope.endPosition.row + 1);
|
|
1795
|
+
if (!method || method.memberType === 'static' || !traits.ownClassMethod(method,
|
|
1796
|
+
method.type === 'classmethod' ? 'class' : 'instance', receiver.text)) return null;
|
|
1797
|
+
}
|
|
1798
|
+
if (traits.typeSystem === 'structural' && receiver.type === 'this') {
|
|
1799
|
+
for (let scope = receiver.parent; scope && scope.type !== 'class_body'; scope = scope.parent) {
|
|
1800
|
+
if (FUNCTION_NODES.has(scope.type) ||
|
|
1801
|
+
scope.type === 'method_definition' && scope.parent?.type !== 'class_body') return null;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
const owner = this.enclosingOwner(file, receiver);
|
|
1805
|
+
if (traits.separateStaticStorage) {
|
|
1806
|
+
const storage = this.thisStorage(receiver);
|
|
1807
|
+
return owner && storage ? { ...owner, valueStorage: storage } : null;
|
|
1808
|
+
}
|
|
1809
|
+
return owner;
|
|
1810
|
+
}
|
|
1811
|
+
// An aliased, default or namespace-chain import (fix #434).
|
|
1812
|
+
if (traits.nameReferenceFamily === 'js' && ['identifier', 'member_expression'].includes(receiver.type)) {
|
|
1813
|
+
const imported = this.jsImportedOwner(file, receiver);
|
|
1814
|
+
if (imported) return traits.separateStaticStorage ? { ...imported, valueStorage: 'class' } : imported;
|
|
1815
|
+
}
|
|
1816
|
+
if (!['identifier', 'type_identifier', 'package_identifier'].includes(receiver.type)) return null;
|
|
1817
|
+
const local = this.localBinding(file, receiver, receiver.text, true);
|
|
1818
|
+
if (local?.binding) {
|
|
1819
|
+
const type = declarationType(local.binding.node);
|
|
1820
|
+
if (!type?.syntax) return null;
|
|
1821
|
+
if (type.origin === 'constructor' && this.syntaxNodes(file, receiver.tree, receiver.text)
|
|
1822
|
+
.some(token => isWrittenBinding(token.node) && sameNode(
|
|
1823
|
+
this.localBinding(file, token.node, receiver.text, true)?.binding?.node,
|
|
1824
|
+
local.binding.node))) return null;
|
|
1825
|
+
const owner = this.classForType(file, type.syntax, type.origin === 'constructor');
|
|
1826
|
+
return owner && traits.separateStaticStorage ? { ...owner, valueStorage: 'instance' } : owner;
|
|
1827
|
+
}
|
|
1828
|
+
if (local) return null;
|
|
1829
|
+
if (traits.bareCallReachesMethods) {
|
|
1830
|
+
const owner = this.enclosingOwner(file, receiver);
|
|
1831
|
+
const members = (this.index.symbols.get(receiver.text) || []).filter(d => d.className);
|
|
1832
|
+
if (owner && findDeclaringClass(this.index, owner, members)) return null;
|
|
1833
|
+
}
|
|
1834
|
+
const owner = this.classForType(file, { name: receiver.text, node: receiver }, true);
|
|
1835
|
+
return owner && traits.separateStaticStorage ? { ...owner, valueStorage: 'class' } : owner;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
ambiguousClassFamily(owner) {
|
|
1839
|
+
if (langTraits(this.language)?.typeArgumentValues !== 'ordinary' || !owner?.name) return false;
|
|
1840
|
+
const info = classDefsNamed(this.index, owner.name);
|
|
1841
|
+
// A forward declaration of the class is no alternative of it (fix #427).
|
|
1842
|
+
return info.unique && info.entries.filter(entry => !entry.def.forwardDeclaration).length > 1;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
classValueMembers() {
|
|
1846
|
+
if (this._classValueMembers) return this._classValueMembers;
|
|
1847
|
+
const members = [], originals = new Map(), unresolved = [];
|
|
1848
|
+
for (const definition of this.index.symbols.get(this.name) || []) {
|
|
1849
|
+
if (TYPE_KINDS.has(definition.type) ||
|
|
1850
|
+
langTraits(this.index.files.get(definition.file)?.language)?.typeArgumentValues !== 'ordinary') continue;
|
|
1851
|
+
const info = isValueDefinition(definition) && this.enumInfo(definition);
|
|
1852
|
+
if (definition.className) members.push(definition);
|
|
1853
|
+
if (info?.unscoped) {
|
|
1854
|
+
if (info.scope?.type !== 'field_declaration_list') continue;
|
|
1855
|
+
let parent = this.enclosingOwner(definition.file, info.scope);
|
|
1856
|
+
if (!parent) parent = lexicalClassRefOf(this.index, ownerRefOf(this.index, definition)?.def);
|
|
1857
|
+
const body = info.scope.parent;
|
|
1858
|
+
if (!parent?.def || parent.def.startLine > body.startPosition.row + 1 ||
|
|
1859
|
+
parent.def.endLine < body.endPosition.row + 1) {
|
|
1860
|
+
unresolved.push(definition);
|
|
1861
|
+
continue;
|
|
1862
|
+
}
|
|
1863
|
+
const member = { ...definition, className: parent.name, namespace: parent.def.namespace };
|
|
1864
|
+
members.push(member);
|
|
1865
|
+
originals.set(member, definition);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
this._classValueMembers = { members, originals, unresolved };
|
|
1869
|
+
return this._classValueMembers;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
classBindingsVerdict(members, originals) {
|
|
1873
|
+
const definitions = members.map(member => originals.get(member) || member);
|
|
1874
|
+
if (!this.owner && !definitions.some(definition => this.group.has(definition)) &&
|
|
1875
|
+
!this.declarationNode(this.def)) return { verdict: 'unknown', reason: 'member-declaration-identity' };
|
|
1876
|
+
return this.judge(definitions, 'other-member-owner');
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
/** Whether a member of another path through unnamed structs or unions
|
|
1880
|
+
* of the pin's class is declared at this token (fix #430). */
|
|
1881
|
+
otherPathMemberAt(file, node) {
|
|
1882
|
+
const line = node.startPosition.row + 1;
|
|
1883
|
+
const paths = JSON.stringify(this.def.memberPaths || null);
|
|
1884
|
+
return this.members.some(member => member !== this.def && member.file === file &&
|
|
1885
|
+
(member.nameLine || member.startLine) === line && member.className === this.def.className &&
|
|
1886
|
+
JSON.stringify(member.memberPaths || null) !== paths);
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
/**
|
|
1890
|
+
* The member a `receiver.name` access names when a member of an unnamed
|
|
1891
|
+
* struct or union has the name (fix #430): such a member is reached
|
|
1892
|
+
* through the declarators of its type (`e->data.stream_start.encoding`),
|
|
1893
|
+
* so the receiver must end with one of its member paths, and the object
|
|
1894
|
+
* that path starts from must be of the enclosing class. Null when no
|
|
1895
|
+
* member of the name has a path (the ordinary member lookup decides).
|
|
1896
|
+
*/
|
|
1897
|
+
pathMemberAt(file, receiver) {
|
|
1898
|
+
const ordinary = langTraits(this.language)?.typeArgumentValues === 'ordinary';
|
|
1899
|
+
const bindings = ordinary ? this.classValueMembers() : null;
|
|
1900
|
+
const candidates = ordinary ? bindings.members : this.memberCandidates;
|
|
1901
|
+
if (!candidates.some(member => member.memberPaths)) return null;
|
|
1902
|
+
const names = [], bases = [receiver];
|
|
1903
|
+
for (let base = receiver; base?.type === 'field_expression' && names.length < 16;) {
|
|
1904
|
+
names.unshift(base.childForFieldName('field')?.text);
|
|
1905
|
+
base = base.childForFieldName('argument');
|
|
1906
|
+
bases.unshift(base);
|
|
1907
|
+
}
|
|
1908
|
+
// A bare name starting the chain inside a member function is an
|
|
1909
|
+
// implicit `this->name` unless a local binds it.
|
|
1910
|
+
const root = bases[0];
|
|
1911
|
+
const implicitOwner = root?.type === 'identifier' && ordinary &&
|
|
1912
|
+
!this.localBinding(file, root, root.text, true) ? this.enclosingOwner(file, root) : null;
|
|
1913
|
+
if (implicitOwner) {
|
|
1914
|
+
names.unshift(root.text);
|
|
1915
|
+
bases.unshift(null);
|
|
1916
|
+
}
|
|
1917
|
+
const endsWith = path => path.length <= names.length &&
|
|
1918
|
+
path.every((part, i) => names[names.length - path.length + i] === part);
|
|
1919
|
+
const byDepth = new Map();
|
|
1920
|
+
for (const member of candidates) {
|
|
1921
|
+
for (const path of member.memberPaths || [[]]) {
|
|
1922
|
+
if (!endsWith(path)) continue;
|
|
1923
|
+
if (!byDepth.has(path.length)) byDepth.set(path.length, new Set());
|
|
1924
|
+
byDepth.get(path.length).add(member);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
const reached = new Set();
|
|
1928
|
+
let unresolved = false;
|
|
1929
|
+
for (const [depth, members] of byDepth) {
|
|
1930
|
+
const base = bases[names.length - depth];
|
|
1931
|
+
const owner = base === null ? implicitOwner : this.receiverOwner(file, base);
|
|
1932
|
+
if (!owner?.key || this.ambiguousClassFamily(owner)) { unresolved = true; continue; }
|
|
1933
|
+
const declaring = findDeclaringClass(this.index, owner, [...members]);
|
|
1934
|
+
if (!declaring || declaring.uncertain) { unresolved = true; continue; }
|
|
1935
|
+
for (const member of declaring.members) reached.add(member);
|
|
1936
|
+
}
|
|
1937
|
+
if (unresolved) return { verdict: 'unknown', reason: 'member-owner-unresolved' };
|
|
1938
|
+
if (reached.size === 0) return { verdict: 'no', reason: 'other-member-owner' };
|
|
1939
|
+
if (ordinary) return this.classBindingsVerdict([...reached], bindings.originals);
|
|
1940
|
+
if (reached.size !== 1) return { verdict: 'unknown', reason: 'ambiguous-value-declaration' };
|
|
1941
|
+
return this.group.has([...reached][0]) ? { verdict: 'yes' } : { verdict: 'no', reason: 'other-member-owner' };
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
cppMemberAt(owner) {
|
|
1945
|
+
if (!owner?.key || this.ambiguousClassFamily(owner)) return { verdict: 'unknown', reason: 'member-owner-unresolved' };
|
|
1946
|
+
const bindings = this.classValueMembers();
|
|
1947
|
+
const declaring = findDeclaringClass(this.index, owner, bindings.members);
|
|
1948
|
+
return !declaring || declaring.uncertain ? { verdict: 'unknown', reason: 'member-owner-unresolved' }
|
|
1949
|
+
: this.classBindingsVerdict(declaring.members, bindings.originals);
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
qualifiedValueAt(file, qualifier, role) {
|
|
1953
|
+
const scopes = [];
|
|
1954
|
+
let global = false;
|
|
1955
|
+
for (let current = templateIdOf(this.siteNode); current?.parent?.type === 'qualified_identifier' &&
|
|
1956
|
+
sameNode(current.parent.childForFieldName('name'), current); current = current.parent) {
|
|
1957
|
+
const scope = current.parent.childForFieldName('scope');
|
|
1958
|
+
if (scope) scopes.unshift(scope);
|
|
1959
|
+
else if (current.parent.children.some(child => child.type === '::')) global = true;
|
|
1960
|
+
}
|
|
1961
|
+
let syntax = typeSyntax(qualifier), text = qualifier.text;
|
|
1962
|
+
if (scopes.length && sameNode(scopes[scopes.length - 1], qualifier)) {
|
|
1963
|
+
text = `${global ? '::' : ''}${scopes.map(scope => scope.text).join('::')}`;
|
|
1964
|
+
if (syntax && (global || scopes.length > 1)) syntax = { ...syntax,
|
|
1965
|
+
qualifier: syntax.qualifier || scopes[scopes.length - 2],
|
|
1966
|
+
qualifierText: `${global ? '::' : ''}${[...scopes.slice(0, -1).map(scope => scope.text),
|
|
1967
|
+
syntax.qualifierText].filter(Boolean).join('::')}` };
|
|
1968
|
+
}
|
|
1969
|
+
const owner = this.classForType(file, syntax);
|
|
1970
|
+
return owner ? this.cppMemberAt(owner)
|
|
1971
|
+
: this.resolveQualified(file, this.siteNode.startPosition.row + 1, qualifier, text, role);
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
classValueAt(file, node) {
|
|
1975
|
+
let owner = this.enclosingOwner(file, node);
|
|
1976
|
+
if (!owner) {
|
|
1977
|
+
for (let scope = node.parent; scope; scope = scope.parent) {
|
|
1978
|
+
if (TYPE_BODIES.has(scope.type)) return { verdict: 'unknown', reason: 'member-owner-unresolved' };
|
|
1979
|
+
}
|
|
1980
|
+
return null;
|
|
1981
|
+
}
|
|
1982
|
+
const bindings = this.classValueMembers();
|
|
1983
|
+
if (bindings.unresolved.length) {
|
|
1984
|
+
const visible = require('./cpp-scope').includeClosure(this.index, file);
|
|
1985
|
+
if (bindings.unresolved.some(definition => definition.file === file || visible.has(definition.file))) {
|
|
1986
|
+
return { verdict: 'unknown', reason: 'enum-member-owner-unresolved' };
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
const lexical = new Set();
|
|
1990
|
+
while (owner) {
|
|
1991
|
+
if (!owner.key || lexical.has(owner.key) || this.ambiguousClassFamily(owner)) {
|
|
1992
|
+
return { verdict: 'unknown', reason: 'value-owner-alternatives' };
|
|
1993
|
+
}
|
|
1994
|
+
lexical.add(owner.key);
|
|
1995
|
+
const declaring = findDeclaringClass(this.index, owner, bindings.members);
|
|
1996
|
+
if (declaring?.uncertain) return { verdict: 'unknown', reason: 'member-owner-unresolved' };
|
|
1997
|
+
if (declaring?.members.length) return this.classBindingsVerdict(declaring.members, bindings.originals);
|
|
1998
|
+
const queue = [owner], visited = new Set();
|
|
1999
|
+
while (queue.length) {
|
|
2000
|
+
const current = queue.pop();
|
|
2001
|
+
if (visited.has(current.key)) continue;
|
|
2002
|
+
visited.add(current.key);
|
|
2003
|
+
for (const parent of parentRefsOf(this.index, current)) {
|
|
2004
|
+
if (!parent.key || parent.external ||
|
|
2005
|
+
langTraits(this.index.files.get(parent.def?.file)?.language)?.typeArgumentValues !== 'ordinary') {
|
|
2006
|
+
return { verdict: 'unknown', reason: 'open-value-owner' };
|
|
2007
|
+
}
|
|
2008
|
+
queue.push(parent);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
// An owner known by key alone (no definition to read its
|
|
2012
|
+
// enclosing scope from) leaves the lookup undecided (fix #430).
|
|
2013
|
+
if (!owner.node && !owner.def) return { verdict: 'unknown', reason: 'member-owner-unresolved' };
|
|
2014
|
+
const body = owner.node || this.declarationNode(owner.def);
|
|
2015
|
+
const sourceFile = owner.def?.file || file;
|
|
2016
|
+
let outer = body && TYPE_BODIES.has(body.type) ? this.enclosingOwner(sourceFile, body, false) : null;
|
|
2017
|
+
if (!outer && body) {
|
|
2018
|
+
const qualifier = typeSyntax(body.childForFieldName('name'))?.qualifier;
|
|
2019
|
+
if (qualifier) outer = this.classForType(sourceFile, typeSyntax(qualifier));
|
|
2020
|
+
}
|
|
2021
|
+
if (!outer && owner.def?.enclosingType) return { verdict: 'unknown', reason: 'member-owner-unresolved' };
|
|
2022
|
+
owner = outer;
|
|
2023
|
+
}
|
|
2024
|
+
return null;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
/**
|
|
2028
|
+
* A body a namespace-scope macro invocation wraps (`TEST_F(Fixture,
|
|
2029
|
+
* Name) { .. }`) may be expanded as a member function of a class an
|
|
2030
|
+
* argument names (a fixture subclass), as the caller side reads it
|
|
2031
|
+
* (fix #396): a bare name the class chain of such an argument declares
|
|
2032
|
+
* may be that member (fix #431). Visible, never confirmed or excluded.
|
|
2033
|
+
*/
|
|
2034
|
+
macroScopeValueAt(file, node) {
|
|
2035
|
+
if (!this.owner) return null;
|
|
2036
|
+
let body = null;
|
|
2037
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
2038
|
+
if (current.type === 'function_definition') { body = current; break; }
|
|
2039
|
+
if (TYPE_BODIES.has(current.type) || current.type === 'lambda_expression') return null;
|
|
2040
|
+
}
|
|
2041
|
+
if (!body || body.parent && !['translation_unit', 'declaration_list'].includes(body.parent.type) &&
|
|
2042
|
+
!String(body.parent.type).startsWith('preproc_')) return null;
|
|
2043
|
+
const enclosing = this.index.findEnclosingFunction(file, node.startPosition.row + 1, true);
|
|
2044
|
+
const args = enclosing?.generatedByMacro?.args;
|
|
2045
|
+
if (!Array.isArray(args) || enclosing.className || enclosing.startLine !== body.startPosition.row + 1) return null;
|
|
2046
|
+
const declarator = body.childForFieldName('declarator');
|
|
2047
|
+
if (!declarator) return null;
|
|
2048
|
+
const names = new Set(args);
|
|
2049
|
+
const tokens = [];
|
|
2050
|
+
const visit = current => {
|
|
2051
|
+
if (['identifier', 'type_identifier'].includes(current.type) && names.has(current.text)) tokens.push(current);
|
|
2052
|
+
for (const child of current.namedChildren || []) visit(child);
|
|
2053
|
+
};
|
|
2054
|
+
visit(declarator);
|
|
2055
|
+
for (const token of tokens) {
|
|
2056
|
+
const owner = this.classForType(file, typeSyntax(token));
|
|
2057
|
+
if (!owner?.key) continue;
|
|
2058
|
+
const declaring = findDeclaringClass(this.index, owner, this.memberCandidates);
|
|
2059
|
+
if (declaring?.members.some(member => this.group.has(member))) {
|
|
2060
|
+
return { verdict: 'unknown', reason: 'macro-generated-scope' };
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
return null;
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
localUsingAt(file, node) {
|
|
2067
|
+
if (!this._usingMemo) this._usingMemo = new Map();
|
|
2068
|
+
const key = `${file}\0${node.tree.rootNode.id}`;
|
|
2069
|
+
if (!this._usingMemo.has(key)) this._usingMemo.set(key, node.tree.rootNode.descendantsOfType('using_declaration'));
|
|
2070
|
+
const visible = this._usingMemo.get(key).filter(declaration => declaration.startIndex < node.startIndex)
|
|
2071
|
+
.map(declaration => ({ declaration, scope: referenceBindingScope(declaration,
|
|
2072
|
+
this.index.files.get(file)?.language, { role: 'import' }) }))
|
|
2073
|
+
.filter(({ scope }) => scope && !ROOTS.has(scope.type) &&
|
|
2074
|
+
scope.type !== 'field_declaration_list' && contains(scope, node))
|
|
2075
|
+
.sort((a, b) => (a.scope.endIndex - a.scope.startIndex) -
|
|
2076
|
+
(b.scope.endIndex - b.scope.startIndex) || b.declaration.startIndex - a.declaration.startIndex);
|
|
2077
|
+
for (const { declaration } of visible) {
|
|
2078
|
+
const syntax = typeSyntax(declaration.namedChild(0));
|
|
2079
|
+
if (declaration.children.some(child => child.type === 'enum')) {
|
|
2080
|
+
const owner = this.classForType(file, syntax);
|
|
2081
|
+
if (!owner || owner.def.type !== 'enum') return { verdict: 'unknown', reason: 'using-enum-binding' };
|
|
2082
|
+
const declared = (this.index.symbols.get(this.name) || []).filter(definition =>
|
|
2083
|
+
isValueDefinition(definition) && ownerRefOf(this.index, definition)?.key === owner.key);
|
|
2084
|
+
if (declared.length) return this.judge(declared);
|
|
2085
|
+
} else if (syntax?.name === this.name) {
|
|
2086
|
+
if (this.ordinaryArgumentPosition(file, declaration.namedChild(0)) !== 'value') {
|
|
2087
|
+
return { verdict: 'unknown', reason: 'using-declaration-binding' };
|
|
2088
|
+
}
|
|
2089
|
+
return syntax.qualifier ? this.qualifiedValueAt(file, syntax.qualifier, { role: 'qualified' })
|
|
2090
|
+
: { verdict: 'unknown', reason: 'using-declaration-binding' };
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
return null;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
globalAt(file, node, contextualValue = false) {
|
|
2097
|
+
const traits = langTraits(this.index.files.get(file)?.language);
|
|
2098
|
+
const scope = file === this.def.file && this.scopeRelation(file,
|
|
2099
|
+
this.def.nameLine || this.def.startLine, node);
|
|
2100
|
+
if (scope && scope !== 'yes') return { verdict: scope === 'no' ? 'no' : 'unknown',
|
|
2101
|
+
reason: 'script-binding-scope' };
|
|
2102
|
+
const local = this.localBinding(file, node, this.name, false, contextualValue);
|
|
2103
|
+
if (local) return local.pin ? { verdict: 'yes' }
|
|
2104
|
+
: local.binding ? { verdict: 'no', reason: 'local-binding' } : local;
|
|
2105
|
+
if (traits.typeArgumentValues === 'ordinary') {
|
|
2106
|
+
const imported = this.localUsingAt(file, node);
|
|
2107
|
+
if (imported) return imported;
|
|
2108
|
+
}
|
|
2109
|
+
if (traits.textualIncludes) {
|
|
2110
|
+
const enumScope = enclosingEnum(node);
|
|
2111
|
+
if (enumScope) {
|
|
2112
|
+
const earlier = (this.index.symbols.get(this.name) || []).filter(definition => {
|
|
2113
|
+
if (definition.file !== file || !isValueDefinition(definition)) return false;
|
|
2114
|
+
const info = this.enumInfo(definition);
|
|
2115
|
+
return info && sameNode(info.enumNode, enumScope) && info.node.startIndex < node.startIndex;
|
|
2116
|
+
});
|
|
2117
|
+
return earlier.length ? this.judge(earlier)
|
|
2118
|
+
: { verdict: 'unknown', reason: 'enum-initializer-binding' };
|
|
2119
|
+
}
|
|
2120
|
+
const locals = (this.index.symbols.get(this.name) || []).filter(definition =>
|
|
2121
|
+
isValueDefinition(definition) && definition.file === file)
|
|
2122
|
+
.map(definition => ({ definition, info: this.enumInfo(definition) }))
|
|
2123
|
+
.filter(choice => choice.info?.unscoped && choice.info.scope &&
|
|
2124
|
+
!ROOTS.has(choice.info.scope.type) && choice.info.scope.type !== 'field_declaration_list' &&
|
|
2125
|
+
contains(choice.info.scope, node) && choice.info.node.startIndex < node.startIndex);
|
|
2126
|
+
if (locals.length) {
|
|
2127
|
+
const width = choice => choice.info.scope.endIndex - choice.info.scope.startIndex;
|
|
2128
|
+
const closest = Math.min(...locals.map(width));
|
|
2129
|
+
return this.judge(locals.filter(choice => width(choice) === closest).map(choice => choice.definition));
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
if (traits.typeArgumentValues === 'ordinary') {
|
|
2133
|
+
return this.classValueAt(file, node) || this.macroScopeValueAt(file, node) ||
|
|
2134
|
+
this.resolveName(file, node.startPosition.row + 1);
|
|
2135
|
+
}
|
|
2136
|
+
if (traits.bareCallReachesMethods) {
|
|
2137
|
+
const owner = this.enclosingOwner(file, node);
|
|
2138
|
+
const members = (this.index.symbols.get(this.name) || []).filter(d => d.className);
|
|
2139
|
+
if (owner && findDeclaringClass(this.index, owner, members)) {
|
|
2140
|
+
return { verdict: 'no', reason: 'member-binding' };
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
const ownEnum = this.enumInfo(this.def)?.unscoped;
|
|
2144
|
+
const localEnums = traits.textualIncludes ? (this.index.symbols.get(this.name) || []).filter(definition =>
|
|
2145
|
+
isValueDefinition(definition) && definition.file === file)
|
|
2146
|
+
.map(definition => ({ definition, info: this.enumInfo(definition) }))
|
|
2147
|
+
.filter(choice => choice.info && !ROOTS.has(choice.info.scope?.type) &&
|
|
2148
|
+
contains(choice.info.scope, node) && choice.info.node.startIndex < node.startIndex) : [];
|
|
2149
|
+
if (ownEnum || localEnums.length) {
|
|
2150
|
+
const choices = ownEnum ? this.all.filter(definition => definition.file === file)
|
|
2151
|
+
.map(definition => ({ definition, info: this.enumInfo(definition) }))
|
|
2152
|
+
.filter(choice => choice.info && contains(choice.info.scope, node) &&
|
|
2153
|
+
choice.info.node.startIndex < node.startIndex) : localEnums;
|
|
2154
|
+
if (!choices.length) return { verdict: 'unknown', reason: 'enum-value-scope' };
|
|
2155
|
+
const width = choice => choice.info.scope
|
|
2156
|
+
? choice.info.scope.endIndex - choice.info.scope.startIndex : Infinity;
|
|
2157
|
+
const closest = Math.min(...choices.map(width));
|
|
2158
|
+
return this.judge(choices.filter(choice => width(choice) === closest).map(choice => choice.definition));
|
|
2159
|
+
}
|
|
2160
|
+
if (traits?.moduleValueLookup || traits?.textualIncludes) {
|
|
2161
|
+
return this.resolveName(file, node.startPosition.row + 1);
|
|
2162
|
+
}
|
|
2163
|
+
const visible = this.visibleInFile(file, node.startPosition.row + 1);
|
|
2164
|
+
if (visible.length) return this.judge(visible);
|
|
2165
|
+
return this.resolveName(file, node.startPosition.row + 1);
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
staticImportAt(file, node, sourceToken = false) {
|
|
2169
|
+
const traits = langTraits(this.index.files.get(file)?.language);
|
|
2170
|
+
const imports = traits.staticValueImportSyntax?.(node.tree) || [];
|
|
2171
|
+
const verdicts = [];
|
|
2172
|
+
for (const imported of imports) {
|
|
2173
|
+
if (imported.member !== '*' && imported.member !== this.name) continue;
|
|
2174
|
+
if (sourceToken && imported.token && !sameNode(imported.token, node)) continue;
|
|
2175
|
+
const owner = this.classForType(file, typeSyntax(imported.type));
|
|
2176
|
+
if (!owner) { verdicts.push({ verdict: 'unknown', reason: 'static-value-import' }); continue; }
|
|
2177
|
+
const declaring = findDeclaringClass(this.index, owner, this.members);
|
|
2178
|
+
if (!declaring) continue;
|
|
2179
|
+
if (!declaring.members.every(member => member.type === 'constant' ||
|
|
2180
|
+
['enum', 'interface'].includes(declaring.ref?.def?.type) ||
|
|
2181
|
+
member.modifiers?.some(modifier => modifier === 'static' || modifier === 'const'))) continue;
|
|
2182
|
+
verdicts.push(this.memberAt(owner));
|
|
2183
|
+
}
|
|
2184
|
+
return verdicts.length ? this.combine(verdicts) : null;
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
enumCaseAt(file, node) {
|
|
2188
|
+
if (!langTraits(this.index.files.get(file)?.language)?.implicitEnumCaseLabels) return null;
|
|
2189
|
+
let label = node.parent;
|
|
2190
|
+
while (label && label.type !== 'switch_label' && label.type !== 'block') label = label.parent;
|
|
2191
|
+
if (label?.type !== 'switch_label') return null;
|
|
2192
|
+
let statement = label.parent;
|
|
2193
|
+
while (statement && !['switch_expression', 'switch_statement'].includes(statement.type)) statement = statement.parent;
|
|
2194
|
+
let selector = statement?.childForFieldName('condition') || statement?.childForFieldName('value');
|
|
2195
|
+
while (selector?.type === 'parenthesized_expression') selector = selector.namedChild(0);
|
|
2196
|
+
const owner = selector && this.receiverOwner(file, selector);
|
|
2197
|
+
return owner?.def?.type === 'enum' ? this.memberAt(owner)
|
|
2198
|
+
: { verdict: 'unknown', reason: 'enum-selector-type' };
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
ordinaryArgumentPosition(file, argument) {
|
|
2202
|
+
const syntax = typeSyntax(argument);
|
|
2203
|
+
if (!syntax) return 'unknown';
|
|
2204
|
+
for (const definition of (this.index.symbols.get(this.name) || []).filter(d => TYPE_KINDS.has(d.type) &&
|
|
2205
|
+
langTraits(this.index.files.get(d.file)?.language)?.nameReferenceFamily === 'c')) {
|
|
2206
|
+
if (this.typeVerdict(file, syntax, definition).verdict !== 'no') return 'unknown';
|
|
2207
|
+
}
|
|
2208
|
+
return 'value';
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
initializerArgumentPosition(file, node, argument, role) {
|
|
2212
|
+
const parameter = argument.parent;
|
|
2213
|
+
if (parameter?.type !== 'parameter_declaration' || parameter.childForFieldName('declarator') ||
|
|
2214
|
+
!sameNode(parameter.childForFieldName('type'), argument)) return null;
|
|
2215
|
+
const fn = parameter.parent?.parent;
|
|
2216
|
+
if (parameter.parent?.type !== 'parameter_list' || fn?.type !== 'function_declarator') return null;
|
|
2217
|
+
let name = fn.childForFieldName('declarator');
|
|
2218
|
+
while (name?.type === 'parenthesized_declarator' && name.namedChildCount === 1) name = name.namedChild(0);
|
|
2219
|
+
if (!['identifier', 'qualified_identifier'].includes(name?.type)) return null;
|
|
2220
|
+
let declarator = fn;
|
|
2221
|
+
while (['pointer_declarator', 'reference_declarator', 'parenthesized_declarator',
|
|
2222
|
+
'attributed_declarator'].includes(declarator.parent?.type)) declarator = declarator.parent;
|
|
2223
|
+
const declaration = declarator.parent;
|
|
2224
|
+
if (declaration?.type !== 'declaration') return null;
|
|
2225
|
+
const head = declaration.childForFieldName('type');
|
|
2226
|
+
if (head?.type === 'primitive_type' && head.text === 'void' && sameNode(declarator, fn)) return null;
|
|
2227
|
+
const syntax = typeSyntax(argument);
|
|
2228
|
+
if (!syntax || !sameNode(syntax.node, node)) return null;
|
|
2229
|
+
const binding = syntax.qualifier
|
|
2230
|
+
? this.qualifiedValueAt(file, syntax.qualifier, role) : this.globalAt(file, node, true);
|
|
2231
|
+
if (binding.verdict === 'no') return 'value';
|
|
2232
|
+
return binding.verdict === 'yes' && this.ordinaryArgumentPosition(file, argument) === 'value'
|
|
2233
|
+
? 'value' : 'initializer-unknown';
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
typePosition(file, node, role) {
|
|
2237
|
+
const traits = langTraits(this.index.files.get(file)?.language);
|
|
2238
|
+
let argument = node;
|
|
2239
|
+
while (['qualified_identifier', 'scoped_type_identifier', 'type_descriptor'].includes(argument.parent?.type)) {
|
|
2240
|
+
const parent = argument.parent;
|
|
2241
|
+
if (parent.type === 'type_descriptor' && parent.namedChildCount !== 1) break;
|
|
2242
|
+
argument = parent;
|
|
2243
|
+
}
|
|
2244
|
+
const list = argument.parent;
|
|
2245
|
+
if (traits.typeArgumentValues === 'ordinary') {
|
|
2246
|
+
const initializer = this.initializerArgumentPosition(file, node, argument, role);
|
|
2247
|
+
if (initializer) return initializer;
|
|
2248
|
+
}
|
|
2249
|
+
if (traits.valuePatternConstants && this.def.type === 'variant' &&
|
|
2250
|
+
['struct_expression', 'struct_pattern'].includes(list?.type) &&
|
|
2251
|
+
['name', 'type'].some(field => {
|
|
2252
|
+
const part = list.childForFieldName(field);
|
|
2253
|
+
return part && contains(part, argument);
|
|
2254
|
+
})) return 'value';
|
|
2255
|
+
if (traits.typeArgumentValues === 'ordinary' &&
|
|
2256
|
+
['sizeof_expression', 'alignof_expression', 'typeid_expression'].includes(list?.type)) {
|
|
2257
|
+
return this.ordinaryArgumentPosition(file, argument);
|
|
2258
|
+
}
|
|
2259
|
+
if (traits.typeArgumentValues && ['template_argument_list', 'type_arguments'].includes(list?.type)) {
|
|
2260
|
+
if (this.syntaxNodes(file, node.tree, this.name).some(token => token.role.typeParam &&
|
|
2261
|
+
contains(referenceTypeParameterScope(token.node), node))) return 'type';
|
|
2262
|
+
if (traits.typeArgumentValues === 'ordinary') {
|
|
2263
|
+
// A class/typedef hidden in the ordinary lookup can make
|
|
2264
|
+
// this grammar's type-like template argument a type.
|
|
2265
|
+
// Unsettled collisions remain review items.
|
|
2266
|
+
return this.ordinaryArgumentPosition(file, argument);
|
|
2267
|
+
}
|
|
2268
|
+
const head = list.parent;
|
|
2269
|
+
const syntax = typeSyntax(head?.childForFieldName('type') || head?.childForFieldName('function'));
|
|
2270
|
+
if (!syntax) return 'unknown';
|
|
2271
|
+
let definition = this.classForType(file, syntax)?.def;
|
|
2272
|
+
if (!definition && head.type === 'generic_function') {
|
|
2273
|
+
const functions = (this.index.symbols.get(syntax.name) || []).filter(d => d.type === 'function' && !d.className);
|
|
2274
|
+
const matches = [];
|
|
2275
|
+
let uncertain = false;
|
|
2276
|
+
for (const candidate of functions) {
|
|
2277
|
+
const resolver = new TypeReferenceResolver(this.index, candidate, syntax.name);
|
|
2278
|
+
resolver.all = functions;
|
|
2279
|
+
resolver.group = new Set([candidate]);
|
|
2280
|
+
resolver.declarationKinds = new Set(['function']);
|
|
2281
|
+
resolver.siteNode = syntax.node;
|
|
2282
|
+
resolver.siteFile = file;
|
|
2283
|
+
const verdict = syntax.qualifierText
|
|
2284
|
+
? resolver.resolveQualified(file, node.startPosition.row + 1,
|
|
2285
|
+
syntax.qualifier, syntax.qualifierText, { role: 'value' })
|
|
2286
|
+
: resolver.resolveName(file, node.startPosition.row + 1, syntax.name);
|
|
2287
|
+
if (verdict.verdict === 'yes') matches.push(candidate);
|
|
2288
|
+
if (verdict.verdict === 'unknown') uncertain = true;
|
|
2289
|
+
}
|
|
2290
|
+
if (!uncertain && matches.length === 1) definition = matches[0];
|
|
2291
|
+
}
|
|
2292
|
+
if (!definition) return 'unknown';
|
|
2293
|
+
const parameters = this.declarationNode(definition)?.childForFieldName('type_parameters')?.namedChildren
|
|
2294
|
+
.filter(parameter => parameter.type !== 'lifetime_parameter');
|
|
2295
|
+
const args = list.namedChildren.filter(arg => arg.type !== 'lifetime');
|
|
2296
|
+
const at = args.findIndex(arg => sameNode(arg, argument));
|
|
2297
|
+
const parameter = at >= 0 && parameters?.[at];
|
|
2298
|
+
return parameter ? parameter.type === 'const_parameter' ? 'value' : 'type' : 'unknown';
|
|
2299
|
+
}
|
|
2300
|
+
if (traits.typeArgumentValues === 'ordinary' && role.cppQualified && !role.qualifierSegment) {
|
|
2301
|
+
const parent = argument.parent;
|
|
2302
|
+
if (parent?.type === 'using_declaration') return this.ordinaryArgumentPosition(file, argument);
|
|
2303
|
+
const declaredType = parent?.childForFieldName('type');
|
|
2304
|
+
if (declaredType && contains(declaredType, argument)) return 'type';
|
|
2305
|
+
if (parent && (parent.childForFieldName('value') && contains(parent.childForFieldName('value'), argument) ||
|
|
2306
|
+
parent.childForFieldName('default_value') && contains(parent.childForFieldName('default_value'), argument) ||
|
|
2307
|
+
['return_statement', 'argument_list', 'binary_expression', 'assignment_expression',
|
|
2308
|
+
'conditional_expression', 'unary_expression', 'expression_statement', 'initializer_list',
|
|
2309
|
+
'case_statement', 'for_range_loop'].includes(parent.type) || parent.type.endsWith('_expression'))) return 'value';
|
|
2310
|
+
if (parent?.type === 'call_expression' && sameNode(parent.childForFieldName('function'), argument)) return 'value';
|
|
2311
|
+
return 'unknown';
|
|
2312
|
+
}
|
|
2313
|
+
return 'type';
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
declarationRegion(file, node, role) {
|
|
2317
|
+
if (this.owner) {
|
|
2318
|
+
const owner = this.declarationOwner(file, node);
|
|
2319
|
+
if (owner?.node) return `type:${owner.node.startIndex}:${owner.node.endIndex}`;
|
|
2320
|
+
}
|
|
2321
|
+
const namespaces = [];
|
|
2322
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
2323
|
+
if (CALLABLE_SCOPES.has(parent.type) || TYPE_BODIES.has(parent.type)) break;
|
|
2324
|
+
if (parent.type === 'namespace_definition' || parent.type === 'mod_item') {
|
|
2325
|
+
namespaces.unshift(parent.childForFieldName('name')?.text || '<anonymous>');
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
if (namespaces.length) return `namespace:${namespaces.join('::')}`;
|
|
2329
|
+
const scope = referenceBindingScope(node, this.index.files.get(file)?.language, role) || node.tree.rootNode;
|
|
2330
|
+
return `scope:${scope.type}:${scope.startIndex}:${scope.endIndex}`;
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
decideToken(file, entry, token) {
|
|
2334
|
+
const { node } = token;
|
|
2335
|
+
let { role } = token;
|
|
2336
|
+
if (role.selectorOperand && role.qualifier) {
|
|
2337
|
+
// `x[a.b]` read as an instantiation (fix #431): a value `a` (a
|
|
2338
|
+
// local, or any name no import binds) makes `a.b` a selector.
|
|
2339
|
+
const operand = role.qualifier;
|
|
2340
|
+
const scope = referenceScope(operand, entry.language, this.lexicalMemo(operand), operand.text);
|
|
2341
|
+
const imported = (entry.importBindings || []).some(binding =>
|
|
2342
|
+
(binding.alias || binding.name) === operand.text && binding.module);
|
|
2343
|
+
if (scope !== 'unknown' && (!imported || (scope && scope !== 'module' && scope !== 'none'))) {
|
|
2344
|
+
role = { role: 'member', receiver: operand };
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
const position = role.role === 'type' ? this.typePosition(file, node, role) : null;
|
|
2348
|
+
const contextualValue = position === 'value';
|
|
2349
|
+
if (contextualValue) role = { ...role, role: 'value' };
|
|
2350
|
+
const receiver = memberReceiver(node, role);
|
|
2351
|
+
let declaration = false;
|
|
2352
|
+
let verdict, wholeModuleImport;
|
|
2353
|
+
const parent = node.parent;
|
|
2354
|
+
const privateToken = node.type === 'private_property_identifier';
|
|
2355
|
+
const privateDeclaration = privateToken &&
|
|
2356
|
+
['field_definition', 'public_field_definition', 'method_definition'].includes(parent?.type) &&
|
|
2357
|
+
sameNode(parent.childForFieldName('name') || parent.childForFieldName('property'), node);
|
|
2358
|
+
const pattern = ['field_initializer', 'field_pattern', 'shorthand_field_initializer',
|
|
2359
|
+
'field_designator'].includes(parent?.type);
|
|
2360
|
+
// A key the grammar reads as an expression (a Python dict display,
|
|
2361
|
+
// `{HEAVY: SQUARE}`) is a value read, not a property name (fix #431).
|
|
2362
|
+
const objectKey = (parent?.type === 'pair' || parent?.type === 'pair_pattern') &&
|
|
2363
|
+
sameNode(parent.childForFieldName('key'), node) && role.role !== 'value';
|
|
2364
|
+
const shorthand = ['shorthand_property_identifier_pattern',
|
|
2365
|
+
'shorthand_property_identifier', 'shorthand_field_identifier'].includes(node.type);
|
|
2366
|
+
const declarationRole = role.role === 'skip' &&
|
|
2367
|
+
(role.binding || ['field-name', 'member-name', 'property-name', 'enumerator',
|
|
2368
|
+
'enum-constant', 'enum-member', 'variant'].includes(role.why));
|
|
2369
|
+
const callableValue = role.role === 'skip' && role.why === 'method-call' &&
|
|
2370
|
+
!langTraits(entry.language)?.fieldsAndMethodsSeparate;
|
|
2371
|
+
// A template-id names the value it instantiates: `ns::v<T>` is
|
|
2372
|
+
// qualified as `ns::v` (fix #431).
|
|
2373
|
+
const nameExpression = templateIdOf(node);
|
|
2374
|
+
const qualified = nameExpression.parent?.type === 'qualified_identifier' &&
|
|
2375
|
+
sameNode(nameExpression.parent.childForFieldName('name'), nameExpression) ? nameExpression.parent : null;
|
|
2376
|
+
const qualifier = role.qualifier || qualified?.childForFieldName('scope');
|
|
2377
|
+
const globalQualified = langTraits(entry.language)?.typeArgumentValues === 'ordinary' &&
|
|
2378
|
+
qualified && !qualified.childForFieldName('scope') && qualified.children.some(child => child.type === '::');
|
|
2379
|
+
const shape = this.owner && !declarationRole
|
|
2380
|
+
? langTraits(entry.language)?.accessorReferenceShape?.(node.tree, {
|
|
2381
|
+
line: node.startPosition.row + 1, column: node.startPosition.column, usageType: 'reference',
|
|
2382
|
+
}) : null;
|
|
2383
|
+
const patternReference = this.patternReference(file, node, role);
|
|
2384
|
+
const scriptScope = file === this.def.file && this.scopeRelation(file,
|
|
2385
|
+
this.def.nameLine || this.def.startLine, node);
|
|
2386
|
+
const pythonBinding = this.moduleVariable && !receiver && !(qualifier && role.role !== 'import') &&
|
|
2387
|
+
!(declarationRole && this.selectedBinding(file, node))
|
|
2388
|
+
? this.pythonBindingAt(file, token) : null;
|
|
2389
|
+
if (pythonBinding) {
|
|
2390
|
+
verdict = pythonBinding;
|
|
2391
|
+
} else if (privateToken) {
|
|
2392
|
+
declaration = privateDeclaration;
|
|
2393
|
+
verdict = this.privateValueAt(file, node);
|
|
2394
|
+
} else if (position === 'type' && role.qualifier && this.owner?.def?.type === 'enum' &&
|
|
2395
|
+
langTraits(entry.language)?.enumMemberTypes) {
|
|
2396
|
+
// `Kind.Text` in a type names the enum member's type (fix #431);
|
|
2397
|
+
// `ns.Text` through a namespace import names a module export.
|
|
2398
|
+
const enumOwner = this.classForType(file, typeSyntax(role.qualifier));
|
|
2399
|
+
const namespaceQualifier = !enumOwner && !!this.jsNamespaceHolds(file, role.qualifier)?.modules;
|
|
2400
|
+
verdict = enumOwner ? this.memberAt(enumOwner)
|
|
2401
|
+
: namespaceQualifier ? { verdict: 'no', reason: 'non-value-position' }
|
|
2402
|
+
: { verdict: 'unknown', reason: 'member-owner-unresolved' };
|
|
2403
|
+
} else if (position === 'type' || role.role === 'decl') {
|
|
2404
|
+
verdict = { verdict: 'no', reason: 'non-value-position' };
|
|
2405
|
+
} else if (position === 'unknown' || position === 'initializer-unknown') {
|
|
2406
|
+
verdict = { verdict: 'unknown', reason: position === 'initializer-unknown'
|
|
2407
|
+
? 'initializer-argument-binding' : 'generic-argument-namespace' };
|
|
2408
|
+
} else if (!this.owner && scriptScope === 'no') {
|
|
2409
|
+
declaration = declarationRole;
|
|
2410
|
+
verdict = { verdict: 'no', reason: 'other-script-scope' };
|
|
2411
|
+
} else if (scriptScope === 'unknown' && node.startPosition.row + 1 ===
|
|
2412
|
+
(this.def.nameLine || this.def.startLine)) {
|
|
2413
|
+
verdict = { verdict: 'unknown', reason: 'script-binding-scope' };
|
|
2414
|
+
} else if (patternReference && patternReference.verdict !== 'no') {
|
|
2415
|
+
verdict = patternReference;
|
|
2416
|
+
} else if (shorthand && !(role.patternWrite && !this.owner)) {
|
|
2417
|
+
// `({ a } = o)` writes the binding `a` in scope (fix #431); a
|
|
2418
|
+
// member target reads the source object's property.
|
|
2419
|
+
verdict = { verdict: 'unknown', reason: 'value-key-binding' };
|
|
2420
|
+
} else if (pattern || objectKey) {
|
|
2421
|
+
const literalOwner = this.owner && objectKey && parent.type === 'pair' &&
|
|
2422
|
+
this.family === 'js' ? this.objectLiteralOwner(file, parent) : null;
|
|
2423
|
+
verdict = literalOwner ? this.memberAt(literalOwner)
|
|
2424
|
+
: this.owner ? { verdict: 'unknown', reason: 'value-initializer-owner-unresolved' }
|
|
2425
|
+
: { verdict: 'no', reason: 'object-key' };
|
|
2426
|
+
} else if (shape?.uncertainRole) {
|
|
2427
|
+
verdict = { verdict: 'unknown', reason: 'value-role-unresolved' };
|
|
2428
|
+
} else if (shape?.kind === 'initializer') {
|
|
2429
|
+
verdict = this.memberAt(this.classForType(file, shape.type));
|
|
2430
|
+
} else if (receiver) {
|
|
2431
|
+
const receiverOwner = this.owner && langTraits(entry.language)?.typeArgumentValues !== 'ordinary'
|
|
2432
|
+
? this.receiverOwner(file, receiver) : null;
|
|
2433
|
+
verdict = (this.owner && this.pathMemberAt(file, receiver)) ||
|
|
2434
|
+
(langTraits(entry.language)?.typeArgumentValues === 'ordinary'
|
|
2435
|
+
? this.cppMemberAt(this.receiverOwner(file, receiver))
|
|
2436
|
+
: this.owner ? this.memberAt(receiverOwner)
|
|
2437
|
+
: (this.family === 'js' && this.jsModuleMemberAt(file, receiver)) ||
|
|
2438
|
+
this.resolveQualified(file, node.startPosition.row + 1, receiver,
|
|
2439
|
+
receiver.text, { role: 'member' }));
|
|
2440
|
+
// An attribute a method declares by assigning it on its receiver
|
|
2441
|
+
// (a Python instance attribute, fix #430): the write at the
|
|
2442
|
+
// pin's declaration line is the declaration, the first write of
|
|
2443
|
+
// another class in the slot is that member's declaration.
|
|
2444
|
+
if (verdict?.verdict === 'yes' && receiverOwner && isWrittenMember(node)) {
|
|
2445
|
+
if (file === this.def.file && node.startPosition.row + 1 === (this.def.nameLine || this.def.startLine)) {
|
|
2446
|
+
declaration = true;
|
|
2447
|
+
} else if (this.groupMemberAt(file, node, receiverOwner) === 'group') {
|
|
2448
|
+
const access = this.slotDeclarationAccess(file, node, receiverOwner);
|
|
2449
|
+
verdict = { verdict: 'yes', declarationOf: true, ...(access && { access }) };
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
} else if (globalQualified) {
|
|
2453
|
+
// `::name` is looked up in the global namespace only (fix #420):
|
|
2454
|
+
// never a class member, and never a local or a class-scope
|
|
2455
|
+
// value the unqualified spelling would find first.
|
|
2456
|
+
verdict = this.owner ? { verdict: 'no', reason: 'global-qualified' }
|
|
2457
|
+
: this.resolveCFamily(file, node.startPosition.row + 1, this.name, '::');
|
|
2458
|
+
} else if (qualifier && role.role !== 'import' && this.owner && this.def.type === 'field' &&
|
|
2459
|
+
langTraits(entry.language)?.typePathReachesFields === false) {
|
|
2460
|
+
// A type path names an associated item, never a struct field
|
|
2461
|
+
// (fix #431: `Opt::command()` calls the derived constructor).
|
|
2462
|
+
verdict = { verdict: 'no', reason: 'associated-item-path' };
|
|
2463
|
+
} else if (qualifier && role.role !== 'import') {
|
|
2464
|
+
verdict = langTraits(entry.language)?.typeArgumentValues === 'ordinary'
|
|
2465
|
+
? this.qualifiedValueAt(file, qualifier, role)
|
|
2466
|
+
: this.owner ? this.memberAt(this.classForType(file, typeSyntax(qualifier)))
|
|
2467
|
+
: this.resolveQualified(file, node.startPosition.row + 1, qualifier,
|
|
2468
|
+
qualifier.text, role);
|
|
2469
|
+
} else if (role.role === 'qualified') {
|
|
2470
|
+
verdict = { verdict: 'unknown', reason: 'qualified-value-unresolved' };
|
|
2471
|
+
} else if (role.role === 'import' && this.owner) {
|
|
2472
|
+
verdict = ['none', 'declaration-body', 'class-body'].includes(
|
|
2473
|
+
langTraits(entry.language)?.bareValueMemberScope) && !langTraits(entry.language)?.bareCallReachesMethods
|
|
2474
|
+
? { verdict: 'no', reason: 'module-import' }
|
|
2475
|
+
: this.staticImportAt(file, node, true) ||
|
|
2476
|
+
{ verdict: 'unknown', reason: 'value-import-unresolved' };
|
|
2477
|
+
} else if (role.role === 'import') {
|
|
2478
|
+
verdict = referenceImportVerdict(this, file, entry, token);
|
|
2479
|
+
} else if (this.owner && role.role === 'skip' && role.why === 'keyword-argument' &&
|
|
2480
|
+
langTraits(entry.language)?.fieldInitializers) {
|
|
2481
|
+
// `Segment(text, style=s)`: the keyword of a construction whose
|
|
2482
|
+
// initializer the class synthesizes from its fields names the
|
|
2483
|
+
// field (fix #431).
|
|
2484
|
+
verdict = this.keywordFieldAt(file, entry, node);
|
|
2485
|
+
} else if (this.family === 'js' && !this.owner && role.role === 'skip' &&
|
|
2486
|
+
(role.why === 'variable' || role.why === 'import-local') &&
|
|
2487
|
+
(wholeModuleImport = this.jsWholeModuleImport(file, entry, node))) {
|
|
2488
|
+
// `const req = require('./request')` / `import req from ..` of a
|
|
2489
|
+
// module whose value is the pin (fix #431): a dependency site;
|
|
2490
|
+
// the local binding keeps its name in a rename.
|
|
2491
|
+
verdict = { verdict: 'no', reason: 'import-alias', access: wholeModuleImport };
|
|
2492
|
+
} else if (!callableValue && (role.role === 'skip' || role.role === 'decl')) {
|
|
2493
|
+
if (!declarationRole) verdict = { verdict: 'no', reason: 'non-value-position' };
|
|
2494
|
+
else {
|
|
2495
|
+
declaration = true;
|
|
2496
|
+
const selectedLine = this.def.nameLine || this.def.startLine;
|
|
2497
|
+
const sameOwner = this.owner && this.declarationOwner(file, node)?.key === this.owner.key;
|
|
2498
|
+
const sameDeclaration = file === this.def.file && node.startPosition.row + 1 === selectedLine;
|
|
2499
|
+
const enumDeclaration = this.enumInfo(this.def)?.node;
|
|
2500
|
+
const selectedEnum = enumDeclaration && sameDeclaration &&
|
|
2501
|
+
node.startPosition.column === enumDeclaration.startPosition.column;
|
|
2502
|
+
const scope = referenceBindingScope(node, entry.language, role);
|
|
2503
|
+
const globalDeclaration = !this.owner && !this.declarationOwner(file, node) &&
|
|
2504
|
+
(!scope || ROOTS.has(scope.type));
|
|
2505
|
+
const otherOwner = this.owner && this.declarationOwner(file, node);
|
|
2506
|
+
const related = otherOwner && langTraits(this.language)?.typeSystem === 'structural' &&
|
|
2507
|
+
(relationToTargets(this.index, otherOwner, [this.def]) !== 'unrelated' ||
|
|
2508
|
+
relationToTargets(this.index, this.owner, this.members.filter(member =>
|
|
2509
|
+
ownerRefOf(this.index, member)?.key === otherOwner.key)) !== 'unrelated');
|
|
2510
|
+
verdict = sameDeclaration && (globalDeclaration || sameOwner || selectedEnum ||
|
|
2511
|
+
this.selectedBinding(file, node))
|
|
2512
|
+
? { verdict: 'yes' } : { verdict: 'no', reason: 'other-declaration' };
|
|
2513
|
+
const member = verdict.verdict === 'no' && (globalDeclaration || otherOwner) &&
|
|
2514
|
+
this.groupMemberAt(file, node, otherOwner || null);
|
|
2515
|
+
if (member) {
|
|
2516
|
+
const access = member === 'group' && this.slotDeclarationAccess(file, node, otherOwner);
|
|
2517
|
+
verdict = member === 'group' ? { verdict: 'yes', declarationOf: true, ...(access && { access }) }
|
|
2518
|
+
: { verdict: 'unknown', reason: 'link-alternatives', declarationOf: true };
|
|
2519
|
+
declaration = false;
|
|
2520
|
+
}
|
|
2521
|
+
const declaredStorage = langTraits(this.language)?.separateStaticStorage && this.thisStorage(node);
|
|
2522
|
+
if (sameDeclaration && sameOwner && declaredStorage) this.declarationStorages.add(declaredStorage);
|
|
2523
|
+
if (verdict.verdict === 'no' && declaredStorage && declaredStorage !== this.targetStorage) {
|
|
2524
|
+
verdict = { verdict: 'no', reason: 'other-value-storage' };
|
|
2525
|
+
} else if (verdict.verdict === 'no' && sameOwner && !this.otherPathMemberAt(file, node)) {
|
|
2526
|
+
verdict = { verdict: 'unknown', reason: 'value-declaration-identity' };
|
|
2527
|
+
declaration = false;
|
|
2528
|
+
} else if (verdict.verdict === 'no' && related) {
|
|
2529
|
+
verdict = { verdict: 'unknown', reason: 'inherited-value-shadow' };
|
|
2530
|
+
declaration = false;
|
|
2531
|
+
} else if (verdict.verdict === 'no' && this.owner && !otherOwner &&
|
|
2532
|
+
['field-name', 'member-name', 'property-name'].includes(role.why)) {
|
|
2533
|
+
verdict = { verdict: 'unknown', reason: 'value-member-owner-unresolved' };
|
|
2534
|
+
declaration = false;
|
|
2535
|
+
} else if (verdict.verdict === 'no' && !this.owner &&
|
|
2536
|
+
(sameDeclaration || globalDeclaration && file === this.def.file)) {
|
|
2537
|
+
const traits = langTraits(entry.language);
|
|
2538
|
+
const lookup = traits.moduleValueLookup || traits.textualIncludes
|
|
2539
|
+
? this.resolveName(file, node.startPosition.row + 1) : null;
|
|
2540
|
+
if (lookup?.verdict !== 'no') {
|
|
2541
|
+
verdict = { verdict: 'unknown', reason: 'value-declaration-identity' };
|
|
2542
|
+
declaration = false;
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
} else if (callableValue || role.role === 'value' || role.role === 'pin-binding' || role.role === 'dunder-all') {
|
|
2547
|
+
const local = this.localBinding(file, node, this.name, false, contextualValue);
|
|
2548
|
+
const importer = !local && this.moduleVariable && file !== this.def.file &&
|
|
2549
|
+
this.pythonImporterVariable(file);
|
|
2550
|
+
if (local) verdict = local.pin ? { verdict: 'yes' }
|
|
2551
|
+
: local.binding ? { verdict: 'no', reason: 'local-binding',
|
|
2552
|
+
fileLocalOnly: this.sourceLocalBinding(file, local.binding, node) } : local;
|
|
2553
|
+
else if (importer) {
|
|
2554
|
+
verdict = importer === true ? { verdict: 'yes' } : { verdict: 'unknown', reason: 'module-rebinding' };
|
|
2555
|
+
}
|
|
2556
|
+
else if (langTraits(entry.language)?.textualIncludes && enclosingEnum(node)) {
|
|
2557
|
+
verdict = this.globalAt(file, node, contextualValue);
|
|
2558
|
+
}
|
|
2559
|
+
else if (langTraits(entry.language)?.typeArgumentValues === 'ordinary') {
|
|
2560
|
+
verdict = this.globalAt(file, node, contextualValue);
|
|
2561
|
+
}
|
|
2562
|
+
else if (this.enumInfo(this.def)?.unscoped) verdict = this.globalAt(file, node, contextualValue);
|
|
2563
|
+
else if (this.owner) {
|
|
2564
|
+
verdict = this.enumCaseAt(file, node);
|
|
2565
|
+
if (!verdict) verdict = this.bareMemberVerdict(file, node, entry.language);
|
|
2566
|
+
if (!verdict) {
|
|
2567
|
+
const owner = this.enclosingOwner(file, node);
|
|
2568
|
+
verdict = langTraits(entry.language)?.bareCallReachesMethods
|
|
2569
|
+
? this.memberAt(owner) : null;
|
|
2570
|
+
if ((!verdict || verdict.verdict === 'unknown') &&
|
|
2571
|
+
(!owner || !findDeclaringClass(this.index, owner, this.members))) {
|
|
2572
|
+
verdict = this.staticImportAt(file, node) || verdict;
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
verdict ||= { verdict: 'unknown', reason: 'implicit-value-owner-unresolved' };
|
|
2576
|
+
}
|
|
2577
|
+
else verdict = this.globalAt(file, node, contextualValue);
|
|
2578
|
+
} else if (this.owner && this.family === 'go' && goKeyedFieldKey(node)) {
|
|
2579
|
+
// `Box{limit: 5}` names Box's own field (fix #434): the
|
|
2580
|
+
// literal's written type, or the element type an enclosing
|
|
2581
|
+
// slice, array or map literal gives an elided one.
|
|
2582
|
+
const type = goCompositeLiteralType(node);
|
|
2583
|
+
const owner = type ? this.classForType(file, typeSyntax(type)) : null;
|
|
2584
|
+
verdict = owner ? this.memberAt(owner) : { verdict: 'unknown', reason: 'composite-literal-type-unresolved' };
|
|
2585
|
+
} else verdict = role.role === 'type'
|
|
2586
|
+
? { verdict: 'no', reason: 'non-value-position' }
|
|
2587
|
+
: { verdict: 'unknown', reason: 'value-binding-unresolved' };
|
|
2588
|
+
if (!declaration && verdict.verdict === 'yes' && receiver) {
|
|
2589
|
+
const line = node.startPosition.row + 1;
|
|
2590
|
+
for (const member of this.group) {
|
|
2591
|
+
if (!member.instanceAttribute || member.file !== file ||
|
|
2592
|
+
line < member.startLine || line > member.endLine) continue;
|
|
2593
|
+
if (instanceAttributeInitializerReads(this.index, member).some(site =>
|
|
2594
|
+
site.line === line && site.column === node.startPosition.column)) {
|
|
2595
|
+
verdict = { verdict: 'unknown', reason: 'instance-attribute-before-write' };
|
|
2596
|
+
break;
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
if (!declaration && verdict.verdict === 'yes' && this.moduleVariable && file === this.def.file && !receiver &&
|
|
2601
|
+
!role.binding) {
|
|
2602
|
+
verdict = this.pythonStarBindingAt(file, node) || verdict;
|
|
2603
|
+
}
|
|
2604
|
+
if (declaration && verdict.verdict === 'yes') {
|
|
2605
|
+
this.selectedDeclarations++;
|
|
2606
|
+
if (privateToken) this.privateIdentifier = true;
|
|
2607
|
+
this.declarationRegions.add(this.declarationRegion(file, node, role));
|
|
2608
|
+
}
|
|
2609
|
+
if (!declaration) {
|
|
2610
|
+
const line = node.startPosition.row + 1;
|
|
2611
|
+
this.references.push({ file: entry.relativePath, absoluteFile: file, line,
|
|
2612
|
+
column: node.startPosition.column, expression: this.index.getLineContent(file, line).trim(),
|
|
2613
|
+
...(receiver && { receiver: receiver.text }),
|
|
2614
|
+
// Another declaration of the renamed entity (a linkage
|
|
2615
|
+
// declaration, a slot member) is listed as a declaration.
|
|
2616
|
+
accessKind: verdict.access?.accessKind || (verdict.declarationOf ? 'declaration'
|
|
2617
|
+
: require('./accessors').referenceAccessKind(node.tree, { line, column: node.startPosition.column }, node)),
|
|
2618
|
+
// A rename decides the token (edit, review, skip); impact
|
|
2619
|
+
// lists it by what it reaches (fix #431): another member of
|
|
2620
|
+
// the slot is not the target's access, and an import of the
|
|
2621
|
+
// pin's module value is a dependency site the rename leaves.
|
|
2622
|
+
...(verdict.access ? { verdict: verdict.access.verdict, reason: verdict.access.reason }
|
|
2623
|
+
: { verdict: verdict.verdict, reason: verdict.reason }) });
|
|
2624
|
+
if (this.enumReferenceSyntax?.collecting(file)) {
|
|
2625
|
+
this.enumReferenceSyntax.capture(file,
|
|
2626
|
+
this.enumSyntaxFact(file, entry, token, receiver, qualifier, position, shape));
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
if (verdict.verdict === 'no' && !verdict.access && role.binding &&
|
|
2630
|
+
this.sourceLocalBinding(file, { node, role,
|
|
2631
|
+
scope: referenceBindingScope(node, entry.language, role) })) verdict.fileLocalOnly = true;
|
|
2632
|
+
return verdict;
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
function valueReferenceSites(index, def, name, groundSet) {
|
|
2637
|
+
if (!isValueDefinition(def)) return null;
|
|
2638
|
+
const resolver = new ValueReferenceResolver(index, def, name);
|
|
2639
|
+
const result = typeReferenceSites(index, def, name, groundSet, resolver);
|
|
2640
|
+
// A line handle cannot distinguish declarations in separate indexed
|
|
2641
|
+
// scopes, even when lookup has selected one of their owners internally.
|
|
2642
|
+
const selectedLine = def.nameLine || def.startLine;
|
|
2643
|
+
const indexedScopes = new Set((index.symbols.get(name) || []).filter(candidate =>
|
|
2644
|
+
isValueDefinition(candidate) && candidate.file === def.file &&
|
|
2645
|
+
(candidate.nameLine || candidate.startLine) === selectedLine &&
|
|
2646
|
+
(candidate.className || null) === (def.className || null))
|
|
2647
|
+
.map(candidate => JSON.stringify([candidate.namespace || null, candidate.enclosingType || null,
|
|
2648
|
+
candidate.lexicalScopeStartLine ?? null, candidate.lexicalScopeEndLine ?? null])));
|
|
2649
|
+
const ambiguityReason = resolver.declarationStorages.size > 1 ? 'storage'
|
|
2650
|
+
: resolver.declarationRegions.size > 1 || indexedScopes.size > 1 ? 'scope' : null;
|
|
2651
|
+
const ambiguousDeclaration = ambiguityReason !== null;
|
|
2652
|
+
if (ambiguousDeclaration) {
|
|
2653
|
+
for (const info of result.files.values()) for (const slot of info.lines.values()) {
|
|
2654
|
+
for (const column of slot.edits) slot.reviews.push({ column, reason: 'ambiguous-value-declaration' });
|
|
2655
|
+
slot.edits = [];
|
|
2656
|
+
}
|
|
2657
|
+
for (const reference of resolver.references) {
|
|
2658
|
+
if (reference.verdict === 'yes' || reference.reason === 'other-value-storage') {
|
|
2659
|
+
reference.verdict = 'unknown';
|
|
2660
|
+
reference.reason = 'ambiguous-value-declaration';
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
if (!resolver.selectedDeclarations) {
|
|
2665
|
+
const line = def.nameLine || def.startLine;
|
|
2666
|
+
const info = result.files.get(def.file);
|
|
2667
|
+
const slot = info?.lines.get(line);
|
|
2668
|
+
if (slot && !slot.reviews.some(review => review.reason === 'value-declaration-identity')) {
|
|
2669
|
+
slot.reviews.push({ column: null, reason: 'value-declaration-identity' });
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
return { ...result, ambiguousDeclaration, ambiguityReason,
|
|
2673
|
+
privateIdentifier: resolver.privateIdentifier === true, references: resolver.references };
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
function findValueReferences(index, name, def, options = {}) {
|
|
2677
|
+
if (!isValueDefinition(def)) return null;
|
|
2678
|
+
const { computeGroundSet } = require('./account');
|
|
2679
|
+
const result = valueReferenceSites(index, def, name, computeGroundSet(index, name));
|
|
2680
|
+
const byFile = new Map(), unverifiedSites = [], excluded = [];
|
|
2681
|
+
const compare = require('./shared').codeUnitCompare;
|
|
2682
|
+
const references = result.references.slice().sort((a, b) => compare(a.file, b.file) ||
|
|
2683
|
+
a.line - b.line || a.column - b.column);
|
|
2684
|
+
for (const ref of references) {
|
|
2685
|
+
if (!index.matchesFilters(ref.file, options)) continue;
|
|
2686
|
+
const { verdict, absoluteFile, ...site } = ref;
|
|
2687
|
+
if (options.files && !options.files.has(absoluteFile)) continue;
|
|
2688
|
+
if (verdict === 'yes') {
|
|
2689
|
+
if (!byFile.has(site.file)) byFile.set(site.file, []);
|
|
2690
|
+
byFile.get(site.file).push({ ...site, tier: 'confirmed' });
|
|
2691
|
+
} else if (verdict === 'unknown') unverifiedSites.push({ ...site, tier: 'unverified' });
|
|
2692
|
+
else excluded.push(site);
|
|
2693
|
+
}
|
|
2694
|
+
const sorted = [...byFile].sort((a, b) => compare(a[0], b[0]))
|
|
2695
|
+
.map(([file, sites]) => ({ file, count: sites.length, sites }));
|
|
2696
|
+
const confirmedCount = sorted.reduce((n, group) => n + group.count, 0);
|
|
2697
|
+
// Every name token the scan decided is in one bucket (fix #420): an
|
|
2698
|
+
// access (confirmed, unverified, excluded with a reason) or a declaration
|
|
2699
|
+
// token (the pin's own and other declarations of the name).
|
|
2700
|
+
let tokens = 0, lines = 0, textLines = 0;
|
|
2701
|
+
const scannedFiles = new Set();
|
|
2702
|
+
for (const [absoluteFile, info] of result.files) {
|
|
2703
|
+
if (!index.matchesFilters(info.relativePath, options) ||
|
|
2704
|
+
options.files && !options.files.has(absoluteFile)) continue;
|
|
2705
|
+
for (const slot of info.lines.values()) {
|
|
2706
|
+
tokens += slot.tokens;
|
|
2707
|
+
if (slot.tokens > 0) { lines++; scannedFiles.add(absoluteFile); } else if (slot.text) textLines++;
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
const accessTokens = confirmedCount + unverifiedSites.length + excluded.length;
|
|
2711
|
+
return { confirmedCount,
|
|
2712
|
+
unverifiedCount: unverifiedSites.length,
|
|
2713
|
+
totalCandidates: confirmedCount + unverifiedSites.length,
|
|
2714
|
+
byFile: sorted, unverifiedSites, excluded: { total: excluded.length,
|
|
2715
|
+
byReason: excluded.reduce((out, site) => {
|
|
2716
|
+
out[site.reason] = (out[site.reason] || 0) + 1; return out;
|
|
2717
|
+
}, {}) },
|
|
2718
|
+
account: { tokens, lines, files: scannedFiles.size, textLines, confirmed: confirmedCount,
|
|
2719
|
+
unverified: unverifiedSites.length, excluded: excluded.length,
|
|
2720
|
+
declarations: tokens - accessTokens, conserved: tokens >= accessTokens } };
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
/** The class a field annotation denotes, completed only in this use's unit. */
|
|
2724
|
+
function declaredFieldOwner(index, field, useFile) {
|
|
2725
|
+
const memo = index.isProvenanceFactIndex ? null
|
|
2726
|
+
: index._opMemo?.('declaredValueFieldOwners', () => new WeakMap());
|
|
2727
|
+
let byFile = memo?.get(field);
|
|
2728
|
+
if (byFile?.has(useFile)) return byFile.get(useFile);
|
|
2729
|
+
const resolver = new ValueReferenceResolver(index, field, field.name);
|
|
2730
|
+
const declaration = resolver.declarationNode(field);
|
|
2731
|
+
const type = declaration && declarationType(declaration);
|
|
2732
|
+
const owner = type?.syntax
|
|
2733
|
+
? resolver.classForType(field.file, type.syntax, false, useFile || field.file) : null;
|
|
2734
|
+
const result = { owner, forward: !!resolver.sawForwardDeclaration };
|
|
2735
|
+
if (memo) {
|
|
2736
|
+
if (!byFile) memo.set(field, byFile = new Map());
|
|
2737
|
+
byFile.set(useFile, result);
|
|
2738
|
+
}
|
|
2739
|
+
return result;
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
/** A literal function body declares callable code at this value binding.
|
|
2743
|
+
* Reading another value, including a function pointer, proves no such body. */
|
|
2744
|
+
function hasCallableInitializer(index, definition) {
|
|
2745
|
+
const memo = index._opMemo?.('valueCallableInitializers', () => new WeakMap());
|
|
2746
|
+
if (memo?.has(definition)) return memo.get(definition);
|
|
2747
|
+
const file = definition.file, source = index._readFile(file);
|
|
2748
|
+
const language = index.files.get(file)?.language;
|
|
2749
|
+
const tree = index._getParsedTree(file, source, language) || safeParse(getParser(language), source);
|
|
2750
|
+
const declarations = [];
|
|
2751
|
+
const literals = new Set(['function', 'function_expression', 'arrow_function',
|
|
2752
|
+
'generator_function', 'func_literal', 'lambda', 'closure_expression', 'lambda_expression']);
|
|
2753
|
+
if (tree) visitNameNodes(tree, source, definition.name, node => {
|
|
2754
|
+
if (node.startPosition.row + 1 !== (definition.nameLine || definition.startLine)) return true;
|
|
2755
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
2756
|
+
if (['variable_declarator', 'assignment', 'var_spec', 'const_spec',
|
|
2757
|
+
'const_item', 'static_item', 'init_declarator'].includes(parent.type)) {
|
|
2758
|
+
const name = parent.childForFieldName('name') || parent.childForFieldName('left') ||
|
|
2759
|
+
parent.childForFieldName('declarator');
|
|
2760
|
+
if (!name || !contains(name, node)) break;
|
|
2761
|
+
let value = parent.childForFieldName('value') || parent.childForFieldName('right');
|
|
2762
|
+
while (['expression_list', 'parenthesized_expression'].includes(value?.type) &&
|
|
2763
|
+
value.namedChildCount === 1) value = value.namedChild(0);
|
|
2764
|
+
declarations.push(literals.has(value?.type));
|
|
2765
|
+
break;
|
|
2766
|
+
}
|
|
2767
|
+
if (CALLABLE_SCOPES.has(parent.type) || ROOTS.has(parent.type)) break;
|
|
2768
|
+
}
|
|
2769
|
+
return true;
|
|
2770
|
+
});
|
|
2771
|
+
const result = declarations.length === 1 && declarations[0];
|
|
2772
|
+
memo?.set(definition, result);
|
|
2773
|
+
return result;
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
module.exports = { isValueDefinition, valueReferenceSites, findValueReferences, hasCallableInitializer,
|
|
2777
|
+
enumValueBindingInfo, declaredFieldOwner, pythonClassBodyOf, instanceAttributeInitializerReads };
|