gitnexus 1.6.6-rc.27 → 1.6.6-rc.28
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/dist/core/ingestion/languages/kotlin/arity-metadata.d.ts +7 -0
- package/dist/core/ingestion/languages/kotlin/arity-metadata.js +20 -0
- package/dist/core/ingestion/languages/kotlin/arity.d.ts +2 -0
- package/dist/core/ingestion/languages/kotlin/arity.js +15 -0
- package/dist/core/ingestion/languages/kotlin/cache-stats.d.ts +7 -0
- package/dist/core/ingestion/languages/kotlin/cache-stats.js +15 -0
- package/dist/core/ingestion/languages/kotlin/captures.d.ts +2 -0
- package/dist/core/ingestion/languages/kotlin/captures.js +376 -0
- package/dist/core/ingestion/languages/kotlin/import-decomposer.d.ts +3 -0
- package/dist/core/ingestion/languages/kotlin/import-decomposer.js +37 -0
- package/dist/core/ingestion/languages/kotlin/import-target.d.ts +6 -0
- package/dist/core/ingestion/languages/kotlin/import-target.js +60 -0
- package/dist/core/ingestion/languages/kotlin/index.d.ts +8 -0
- package/dist/core/ingestion/languages/kotlin/index.js +8 -0
- package/dist/core/ingestion/languages/kotlin/interpret.d.ts +4 -0
- package/dist/core/ingestion/languages/kotlin/interpret.js +70 -0
- package/dist/core/ingestion/languages/kotlin/merge-bindings.d.ts +2 -0
- package/dist/core/ingestion/languages/kotlin/merge-bindings.js +25 -0
- package/dist/core/ingestion/languages/kotlin/owners.d.ts +2 -0
- package/dist/core/ingestion/languages/kotlin/owners.js +50 -0
- package/dist/core/ingestion/languages/kotlin/query.d.ts +3 -0
- package/dist/core/ingestion/languages/kotlin/query.js +112 -0
- package/dist/core/ingestion/languages/kotlin/receiver-binding.d.ts +3 -0
- package/dist/core/ingestion/languages/kotlin/receiver-binding.js +100 -0
- package/dist/core/ingestion/languages/kotlin/scope-resolver.d.ts +17 -0
- package/dist/core/ingestion/languages/kotlin/scope-resolver.js +37 -0
- package/dist/core/ingestion/languages/kotlin/simple-hooks.d.ts +4 -0
- package/dist/core/ingestion/languages/kotlin/simple-hooks.js +19 -0
- package/dist/core/ingestion/languages/kotlin.js +10 -0
- package/dist/core/ingestion/scope-resolution/pipeline/registry.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
|
2
|
+
export interface KotlinArityMetadata {
|
|
3
|
+
readonly parameterCount: number | undefined;
|
|
4
|
+
readonly requiredParameterCount: number | undefined;
|
|
5
|
+
readonly parameterTypes: readonly string[] | undefined;
|
|
6
|
+
}
|
|
7
|
+
export declare function computeKotlinArityMetadata(fnNode: SyntaxNode): KotlinArityMetadata;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { kotlinMethodConfig } from '../../method-extractors/configs/jvm.js';
|
|
2
|
+
export function computeKotlinArityMetadata(fnNode) {
|
|
3
|
+
const params = kotlinMethodConfig.extractParameters?.(fnNode) ?? [];
|
|
4
|
+
let hasVararg = false;
|
|
5
|
+
const parameterTypes = [];
|
|
6
|
+
for (const param of params) {
|
|
7
|
+
if (param.isVariadic)
|
|
8
|
+
hasVararg = true;
|
|
9
|
+
if (param.type !== null)
|
|
10
|
+
parameterTypes.push(param.type);
|
|
11
|
+
}
|
|
12
|
+
if (hasVararg)
|
|
13
|
+
parameterTypes.push('vararg');
|
|
14
|
+
const required = params.filter((p) => !p.isOptional && !p.isVariadic).length;
|
|
15
|
+
return {
|
|
16
|
+
parameterCount: hasVararg ? undefined : params.length,
|
|
17
|
+
requiredParameterCount: required,
|
|
18
|
+
parameterTypes: parameterTypes.length > 0 ? parameterTypes : undefined,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function kotlinArityCompatibility(def, callsite) {
|
|
2
|
+
const min = def.requiredParameterCount;
|
|
3
|
+
const max = def.parameterCount;
|
|
4
|
+
if (min === undefined && max === undefined)
|
|
5
|
+
return 'unknown';
|
|
6
|
+
const argCount = callsite.arity;
|
|
7
|
+
if (!Number.isFinite(argCount) || argCount < 0)
|
|
8
|
+
return 'unknown';
|
|
9
|
+
const hasVararg = def.parameterTypes?.some((t) => t === 'vararg') ?? false;
|
|
10
|
+
if (min !== undefined && argCount < min)
|
|
11
|
+
return 'incompatible';
|
|
12
|
+
if (max !== undefined && argCount > max && !hasVararg)
|
|
13
|
+
return 'incompatible';
|
|
14
|
+
return 'compatible';
|
|
15
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function recordKotlinCacheHit(): void;
|
|
2
|
+
export declare function recordKotlinCacheMiss(): void;
|
|
3
|
+
export declare function getKotlinCaptureCacheStats(): {
|
|
4
|
+
readonly hits: number;
|
|
5
|
+
readonly misses: number;
|
|
6
|
+
};
|
|
7
|
+
export declare function resetKotlinCaptureCacheStats(): void;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
let hits = 0;
|
|
2
|
+
let misses = 0;
|
|
3
|
+
export function recordKotlinCacheHit() {
|
|
4
|
+
hits += 1;
|
|
5
|
+
}
|
|
6
|
+
export function recordKotlinCacheMiss() {
|
|
7
|
+
misses += 1;
|
|
8
|
+
}
|
|
9
|
+
export function getKotlinCaptureCacheStats() {
|
|
10
|
+
return { hits, misses };
|
|
11
|
+
}
|
|
12
|
+
export function resetKotlinCaptureCacheStats() {
|
|
13
|
+
hits = 0;
|
|
14
|
+
misses = 0;
|
|
15
|
+
}
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { findNodeAtRange, nodeToCapture, syntheticCapture, } from '../../utils/ast-helpers.js';
|
|
2
|
+
import { getTreeSitterBufferSize } from '../../constants.js';
|
|
3
|
+
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
|
4
|
+
import { computeKotlinArityMetadata } from './arity-metadata.js';
|
|
5
|
+
import { splitKotlinImportHeader } from './import-decomposer.js';
|
|
6
|
+
import { recordKotlinCacheHit, recordKotlinCacheMiss } from './cache-stats.js';
|
|
7
|
+
import { normalizeKotlinType } from './interpret.js';
|
|
8
|
+
import { synthesizeKotlinReceiverBinding } from './receiver-binding.js';
|
|
9
|
+
import { getKotlinParser, getKotlinScopeQuery } from './query.js';
|
|
10
|
+
const FUNCTION_DECL_TAGS = ['@declaration.function'];
|
|
11
|
+
export function emitKotlinScopeCaptures(sourceText, _filePath, cachedTree) {
|
|
12
|
+
let tree = cachedTree;
|
|
13
|
+
if (tree === undefined) {
|
|
14
|
+
tree = parseSourceSafe(getKotlinParser(), sourceText, undefined, {
|
|
15
|
+
bufferSize: getTreeSitterBufferSize(sourceText),
|
|
16
|
+
});
|
|
17
|
+
recordKotlinCacheMiss();
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
recordKotlinCacheHit();
|
|
21
|
+
}
|
|
22
|
+
const out = [];
|
|
23
|
+
const returnTypes = collectKotlinReturnTypeTexts(tree.rootNode);
|
|
24
|
+
out.push(...synthesizeKotlinLocalAssignmentBindings(tree.rootNode, returnTypes));
|
|
25
|
+
out.push(...synthesizeKotlinLoopBindings(tree.rootNode, returnTypes));
|
|
26
|
+
for (const match of getKotlinScopeQuery().matches(tree.rootNode)) {
|
|
27
|
+
const grouped = {};
|
|
28
|
+
for (const capture of match.captures) {
|
|
29
|
+
const tag = '@' + capture.name;
|
|
30
|
+
grouped[tag] = nodeToCapture(tag, capture.node);
|
|
31
|
+
}
|
|
32
|
+
if (Object.keys(grouped).length === 0)
|
|
33
|
+
continue;
|
|
34
|
+
if (grouped['@import.statement'] !== undefined) {
|
|
35
|
+
const importNode = findNodeAtRange(tree.rootNode, grouped['@import.statement'].range, 'import_header');
|
|
36
|
+
if (importNode !== null) {
|
|
37
|
+
const decomposed = splitKotlinImportHeader(importNode);
|
|
38
|
+
if (decomposed !== null) {
|
|
39
|
+
out.push(decomposed);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (grouped['@reference.call.free'] !== undefined &&
|
|
45
|
+
grouped['@reference.receiver'] !== undefined) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (grouped['@reference.read.member'] !== undefined) {
|
|
49
|
+
const anchor = grouped['@reference.read.member'];
|
|
50
|
+
const navNode = findNodeAtRange(tree.rootNode, anchor.range, 'navigation_expression');
|
|
51
|
+
if (navNode === null || !shouldEmitReadMember(navNode))
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (grouped['@scope.function'] !== undefined) {
|
|
55
|
+
out.push(grouped);
|
|
56
|
+
const fnNode = findNodeAtRange(tree.rootNode, grouped['@scope.function'].range, 'function_declaration');
|
|
57
|
+
if (fnNode !== null) {
|
|
58
|
+
out.push(...synthesizeKotlinReceiverBinding(fnNode));
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const declTag = FUNCTION_DECL_TAGS.find((tag) => grouped[tag] !== undefined);
|
|
63
|
+
if (declTag !== undefined) {
|
|
64
|
+
const fnNode = findNodeAtRange(tree.rootNode, grouped[declTag].range, 'function_declaration');
|
|
65
|
+
if (fnNode !== null) {
|
|
66
|
+
const arity = computeKotlinArityMetadata(fnNode);
|
|
67
|
+
if (arity.parameterCount !== undefined) {
|
|
68
|
+
grouped['@declaration.parameter-count'] = syntheticCapture('@declaration.parameter-count', fnNode, String(arity.parameterCount));
|
|
69
|
+
}
|
|
70
|
+
if (arity.requiredParameterCount !== undefined) {
|
|
71
|
+
grouped['@declaration.required-parameter-count'] = syntheticCapture('@declaration.required-parameter-count', fnNode, String(arity.requiredParameterCount));
|
|
72
|
+
}
|
|
73
|
+
if (arity.parameterTypes !== undefined) {
|
|
74
|
+
grouped['@declaration.parameter-types'] = syntheticCapture('@declaration.parameter-types', fnNode, JSON.stringify(arity.parameterTypes));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const callTag = ['@reference.call.free', '@reference.call.member', '@reference.call.constructor'].find((tag) => grouped[tag] !== undefined);
|
|
79
|
+
if (callTag !== undefined && grouped['@reference.arity'] === undefined) {
|
|
80
|
+
const callNode = findNodeAtRange(tree.rootNode, grouped[callTag].range, 'call_expression');
|
|
81
|
+
if (callNode !== null) {
|
|
82
|
+
const args = callArguments(callNode);
|
|
83
|
+
grouped['@reference.arity'] = syntheticCapture('@reference.arity', callNode, String(args.length));
|
|
84
|
+
grouped['@reference.parameter-types'] = syntheticCapture('@reference.parameter-types', callNode, JSON.stringify(args.map(inferArgType)));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
out.push(grouped);
|
|
88
|
+
const extensionFallback = extensionFreeCallFallback(grouped, tree.rootNode);
|
|
89
|
+
if (extensionFallback !== null)
|
|
90
|
+
out.push(extensionFallback);
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
function synthesizeKotlinLoopBindings(rootNode, returnTypes) {
|
|
95
|
+
const out = [];
|
|
96
|
+
for (const fnNode of descendantsOfType(rootNode, 'function_declaration')) {
|
|
97
|
+
const localTypes = collectKotlinLocalTypeTexts(fnNode, returnTypes);
|
|
98
|
+
for (const forNode of descendantsOfType(fnNode, 'for_statement')) {
|
|
99
|
+
const variable = forNode.namedChildren.find((child) => child.type === 'variable_declaration');
|
|
100
|
+
const name = variable?.namedChildren.find((child) => child.type === 'simple_identifier');
|
|
101
|
+
if (variable === undefined || name === undefined)
|
|
102
|
+
continue;
|
|
103
|
+
const explicitType = variable.namedChildren.find((child) => isKotlinTypeNode(child));
|
|
104
|
+
const iterable = forNode.namedChildren.find((child) => child.id !== variable.id && child.type !== 'control_structure_body');
|
|
105
|
+
const rawType = explicitType?.text ??
|
|
106
|
+
(iterable === undefined
|
|
107
|
+
? null
|
|
108
|
+
: inferKotlinIterableElementType(iterable, localTypes, returnTypes));
|
|
109
|
+
if (rawType === null || rawType.trim() === '')
|
|
110
|
+
continue;
|
|
111
|
+
const anchor = forNode.namedChildren.find((child) => child.type === 'control_structure_body') ?? forNode;
|
|
112
|
+
out.push({
|
|
113
|
+
'@type-binding.annotation': nodeToCapture('@type-binding.annotation', anchor),
|
|
114
|
+
'@type-binding.name': syntheticCapture('@type-binding.name', name, name.text),
|
|
115
|
+
'@type-binding.type': syntheticCapture('@type-binding.type', explicitType ?? iterable ?? name, normalizeKotlinType(rawType)),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
function synthesizeKotlinLocalAssignmentBindings(rootNode, returnTypes) {
|
|
122
|
+
const out = [];
|
|
123
|
+
for (const fnNode of descendantsOfType(rootNode, 'function_declaration')) {
|
|
124
|
+
const localTypes = new Map();
|
|
125
|
+
for (const prop of descendantsOfType(fnNode, 'property_declaration')) {
|
|
126
|
+
const inferred = inferKotlinPropertyType(prop, localTypes, returnTypes);
|
|
127
|
+
if (inferred === null)
|
|
128
|
+
continue;
|
|
129
|
+
localTypes.set(inferred.name.text, inferred.rawType);
|
|
130
|
+
if (inferred.synthetic) {
|
|
131
|
+
out.push({
|
|
132
|
+
'@type-binding.annotation': nodeToCapture('@type-binding.annotation', prop),
|
|
133
|
+
'@type-binding.name': syntheticCapture('@type-binding.name', inferred.name, inferred.name.text),
|
|
134
|
+
'@type-binding.type': syntheticCapture('@type-binding.type', inferred.source, normalizeKotlinType(inferred.rawType)),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
function collectKotlinLocalTypeTexts(fnNode, returnTypes) {
|
|
142
|
+
const out = new Map();
|
|
143
|
+
for (const node of descendants(fnNode)) {
|
|
144
|
+
if (node.type === 'parameter') {
|
|
145
|
+
const name = descendantsOfType(node, 'simple_identifier')[0];
|
|
146
|
+
const type = node.namedChildren.find((child) => isKotlinTypeNode(child));
|
|
147
|
+
if (name !== undefined && type !== undefined)
|
|
148
|
+
out.set(name.text, type.text);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (node.type === 'property_declaration') {
|
|
152
|
+
const inferred = inferKotlinPropertyType(node, out, returnTypes);
|
|
153
|
+
if (inferred !== null)
|
|
154
|
+
out.set(inferred.name.text, inferred.rawType);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
function collectKotlinReturnTypeTexts(rootNode) {
|
|
160
|
+
const out = new Map();
|
|
161
|
+
for (const fnNode of descendantsOfType(rootNode, 'function_declaration')) {
|
|
162
|
+
const name = fnNode.namedChildren.find((child) => child.type === 'simple_identifier');
|
|
163
|
+
const paramsIndex = fnNode.namedChildren.findIndex((child) => child.type === 'function_value_parameters');
|
|
164
|
+
const type = paramsIndex < 0
|
|
165
|
+
? undefined
|
|
166
|
+
: fnNode.namedChildren.slice(paramsIndex + 1).find((child) => isKotlinTypeNode(child));
|
|
167
|
+
if (name !== undefined && type !== undefined)
|
|
168
|
+
out.set(name.text, type.text);
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
function inferKotlinPropertyType(prop, localTypes, returnTypes) {
|
|
173
|
+
const variable = prop.namedChildren.find((child) => child.type === 'variable_declaration');
|
|
174
|
+
const name = variable?.namedChildren.find((child) => child.type === 'simple_identifier');
|
|
175
|
+
if (variable === undefined || name === undefined)
|
|
176
|
+
return null;
|
|
177
|
+
const explicitType = variable.namedChildren.find((child) => isKotlinTypeNode(child));
|
|
178
|
+
if (explicitType !== undefined) {
|
|
179
|
+
return { name, rawType: explicitType.text, source: explicitType, synthetic: false };
|
|
180
|
+
}
|
|
181
|
+
const value = prop.namedChildren.find((child) => child.id !== variable.id && child.type !== 'binding_pattern_kind');
|
|
182
|
+
if (value?.type === 'simple_identifier') {
|
|
183
|
+
const rawType = localTypes.get(value.text);
|
|
184
|
+
return rawType === undefined ? null : { name, rawType, source: value, synthetic: true };
|
|
185
|
+
}
|
|
186
|
+
if (value?.type === 'call_expression') {
|
|
187
|
+
const callee = value.namedChildren.find((child) => child.type === 'simple_identifier');
|
|
188
|
+
if (callee === undefined)
|
|
189
|
+
return null;
|
|
190
|
+
const rawType = returnTypes.get(callee.text) ?? (isUppercaseName(callee.text) ? callee.text : null);
|
|
191
|
+
if (rawType === null)
|
|
192
|
+
return null;
|
|
193
|
+
return { name, rawType, source: callee, synthetic: true };
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
function inferKotlinIterableElementType(iterable, localTypes, returnTypes) {
|
|
198
|
+
if (iterable.type === 'simple_identifier') {
|
|
199
|
+
const raw = localTypes.get(iterable.text);
|
|
200
|
+
return raw === undefined ? null : kotlinContainerElementType(raw, 'values');
|
|
201
|
+
}
|
|
202
|
+
if (iterable.type === 'navigation_expression') {
|
|
203
|
+
const receiver = iterable.namedChildren[0];
|
|
204
|
+
const member = iterable.namedChildren
|
|
205
|
+
.find((child) => child.type === 'navigation_suffix')
|
|
206
|
+
?.namedChildren.find((child) => child.type === 'simple_identifier')?.text;
|
|
207
|
+
if (receiver?.type !== 'simple_identifier')
|
|
208
|
+
return null;
|
|
209
|
+
const raw = localTypes.get(receiver.text);
|
|
210
|
+
return raw === undefined ? null : kotlinContainerElementType(raw, member ?? 'values');
|
|
211
|
+
}
|
|
212
|
+
if (iterable.type === 'call_expression') {
|
|
213
|
+
const callee = iterable.namedChildren.find((child) => child.type === 'simple_identifier');
|
|
214
|
+
if (callee === undefined)
|
|
215
|
+
return null;
|
|
216
|
+
const raw = returnTypes.get(callee.text);
|
|
217
|
+
return raw === undefined ? null : kotlinContainerElementType(raw, 'values');
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
function isUppercaseName(text) {
|
|
222
|
+
return /^[A-Z]/.test(text);
|
|
223
|
+
}
|
|
224
|
+
function kotlinContainerElementType(rawType, member) {
|
|
225
|
+
const parsed = parseKotlinGeneric(rawType);
|
|
226
|
+
if (parsed === null)
|
|
227
|
+
return normalizeKotlinType(rawType);
|
|
228
|
+
const base = parsed.base.split('.').pop() ?? parsed.base;
|
|
229
|
+
if (isKotlinMapType(base)) {
|
|
230
|
+
if (member === 'keys')
|
|
231
|
+
return parsed.args[0] ?? null;
|
|
232
|
+
return parsed.args[1] ?? null;
|
|
233
|
+
}
|
|
234
|
+
if (isKotlinIterableType(base))
|
|
235
|
+
return parsed.args[0] ?? null;
|
|
236
|
+
return normalizeKotlinType(rawType);
|
|
237
|
+
}
|
|
238
|
+
function parseKotlinGeneric(text) {
|
|
239
|
+
const trimmed = text.trim().replace(/\?$/, '');
|
|
240
|
+
const open = trimmed.indexOf('<');
|
|
241
|
+
const close = trimmed.lastIndexOf('>');
|
|
242
|
+
if (open < 0 || close < open)
|
|
243
|
+
return null;
|
|
244
|
+
return {
|
|
245
|
+
base: trimmed.slice(0, open).trim(),
|
|
246
|
+
args: splitTopLevelKotlinArgs(trimmed.slice(open + 1, close)),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function splitTopLevelKotlinArgs(text) {
|
|
250
|
+
const out = [];
|
|
251
|
+
let depth = 0;
|
|
252
|
+
let start = 0;
|
|
253
|
+
for (let i = 0; i < text.length; i++) {
|
|
254
|
+
const ch = text[i];
|
|
255
|
+
if (ch === '<')
|
|
256
|
+
depth++;
|
|
257
|
+
else if (ch === '>')
|
|
258
|
+
depth--;
|
|
259
|
+
else if (ch === ',' && depth === 0) {
|
|
260
|
+
out.push(text.slice(start, i).trim());
|
|
261
|
+
start = i + 1;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
out.push(text.slice(start).trim());
|
|
265
|
+
return out.filter((arg) => arg.length > 0);
|
|
266
|
+
}
|
|
267
|
+
function isKotlinMapType(base) {
|
|
268
|
+
return ['Map', 'MutableMap', 'HashMap', 'LinkedHashMap'].includes(base);
|
|
269
|
+
}
|
|
270
|
+
function isKotlinIterableType(base) {
|
|
271
|
+
return [
|
|
272
|
+
'List',
|
|
273
|
+
'MutableList',
|
|
274
|
+
'ArrayList',
|
|
275
|
+
'Set',
|
|
276
|
+
'MutableSet',
|
|
277
|
+
'Collection',
|
|
278
|
+
'Iterable',
|
|
279
|
+
'Sequence',
|
|
280
|
+
'Array',
|
|
281
|
+
].includes(base);
|
|
282
|
+
}
|
|
283
|
+
function isKotlinTypeNode(node) {
|
|
284
|
+
return (node.type === 'user_type' || node.type === 'nullable_type' || node.type === 'function_type');
|
|
285
|
+
}
|
|
286
|
+
function descendantsOfType(node, type) {
|
|
287
|
+
return descendants(node).filter((child) => child.type === type);
|
|
288
|
+
}
|
|
289
|
+
function descendants(node) {
|
|
290
|
+
const out = [];
|
|
291
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
292
|
+
const child = node.namedChild(i);
|
|
293
|
+
if (child === null)
|
|
294
|
+
continue;
|
|
295
|
+
out.push(child, ...descendants(child));
|
|
296
|
+
}
|
|
297
|
+
return out;
|
|
298
|
+
}
|
|
299
|
+
function shouldEmitReadMember(navNode) {
|
|
300
|
+
const parent = navNode.parent;
|
|
301
|
+
if (parent === null)
|
|
302
|
+
return true;
|
|
303
|
+
if (parent.type === 'call_expression')
|
|
304
|
+
return false;
|
|
305
|
+
if (parent.type === 'directly_assignable_expression')
|
|
306
|
+
return false;
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
function callArguments(callNode) {
|
|
310
|
+
const suffix = callNode.namedChildren.find((child) => child.type === 'call_suffix');
|
|
311
|
+
if (suffix === undefined)
|
|
312
|
+
return [];
|
|
313
|
+
const valueArgs = suffix?.namedChildren.find((child) => child.type === 'value_arguments');
|
|
314
|
+
const args = valueArgs?.namedChildren.filter((child) => child.type === 'value_argument') ?? [];
|
|
315
|
+
const trailingLambdas = suffix.namedChildren.filter((child) => child.type === 'annotated_lambda');
|
|
316
|
+
return [...args, ...trailingLambdas];
|
|
317
|
+
}
|
|
318
|
+
function inferArgType(argNode) {
|
|
319
|
+
const value = argNode.namedChild(0) ?? argNode;
|
|
320
|
+
switch (value.type) {
|
|
321
|
+
case 'integer_literal':
|
|
322
|
+
case 'long_literal':
|
|
323
|
+
return 'Int';
|
|
324
|
+
case 'real_literal':
|
|
325
|
+
return 'Double';
|
|
326
|
+
case 'string_literal':
|
|
327
|
+
case 'line_string_literal':
|
|
328
|
+
case 'multi_line_string_literal':
|
|
329
|
+
return 'String';
|
|
330
|
+
case 'character_literal':
|
|
331
|
+
return 'Char';
|
|
332
|
+
case 'boolean_literal':
|
|
333
|
+
return 'Boolean';
|
|
334
|
+
case 'call_expression': {
|
|
335
|
+
const first = value.namedChild(0);
|
|
336
|
+
return first?.type === 'simple_identifier' ? first.text : '';
|
|
337
|
+
}
|
|
338
|
+
default:
|
|
339
|
+
return '';
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function extensionFreeCallFallback(grouped, rootNode) {
|
|
343
|
+
const member = grouped['@reference.call.member'];
|
|
344
|
+
const receiver = grouped['@reference.receiver'];
|
|
345
|
+
const name = grouped['@reference.name'];
|
|
346
|
+
if (member === undefined || receiver === undefined || name === undefined)
|
|
347
|
+
return null;
|
|
348
|
+
const callNode = findNodeAtRange(rootNode, member.range, 'call_expression');
|
|
349
|
+
if (callNode === null)
|
|
350
|
+
return null;
|
|
351
|
+
const receiverNode = findNodeAtRange(rootNode, receiver.range);
|
|
352
|
+
if (receiverNode === null || !isLiteralReceiver(receiverNode))
|
|
353
|
+
return null;
|
|
354
|
+
const out = {
|
|
355
|
+
'@reference.call.free': syntheticCapture('@reference.call.free', callNode, callNode.text),
|
|
356
|
+
'@reference.name': syntheticCapture('@reference.name', callNode, name.text),
|
|
357
|
+
};
|
|
358
|
+
if (grouped['@reference.arity'] !== undefined)
|
|
359
|
+
out['@reference.arity'] = grouped['@reference.arity'];
|
|
360
|
+
if (grouped['@reference.parameter-types'] !== undefined) {
|
|
361
|
+
out['@reference.parameter-types'] = grouped['@reference.parameter-types'];
|
|
362
|
+
}
|
|
363
|
+
return out;
|
|
364
|
+
}
|
|
365
|
+
function isLiteralReceiver(node) {
|
|
366
|
+
return [
|
|
367
|
+
'integer_literal',
|
|
368
|
+
'long_literal',
|
|
369
|
+
'real_literal',
|
|
370
|
+
'string_literal',
|
|
371
|
+
'line_string_literal',
|
|
372
|
+
'multi_line_string_literal',
|
|
373
|
+
'character_literal',
|
|
374
|
+
'boolean_literal',
|
|
375
|
+
].includes(node.type);
|
|
376
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { nodeToCapture, syntheticCapture } from '../../utils/ast-helpers.js';
|
|
2
|
+
export function splitKotlinImportHeader(importNode) {
|
|
3
|
+
if (importNode.type !== 'import_header')
|
|
4
|
+
return null;
|
|
5
|
+
const spec = parseKotlinImport(importNode);
|
|
6
|
+
if (spec === null)
|
|
7
|
+
return null;
|
|
8
|
+
const out = {
|
|
9
|
+
'@import.statement': nodeToCapture('@import.statement', importNode),
|
|
10
|
+
'@import.kind': syntheticCapture('@import.kind', spec.atNode, spec.kind),
|
|
11
|
+
'@import.source': syntheticCapture('@import.source', spec.atNode, spec.source),
|
|
12
|
+
'@import.name': syntheticCapture('@import.name', spec.atNode, spec.name),
|
|
13
|
+
};
|
|
14
|
+
if (spec.alias !== undefined) {
|
|
15
|
+
out['@import.alias'] = syntheticCapture('@import.alias', spec.atNode, spec.alias);
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
function parseKotlinImport(node) {
|
|
20
|
+
const identifier = node.namedChildren.find((child) => child.type === 'identifier');
|
|
21
|
+
if (identifier === undefined)
|
|
22
|
+
return null;
|
|
23
|
+
const source = identifier.text.trim();
|
|
24
|
+
if (source.length === 0)
|
|
25
|
+
return null;
|
|
26
|
+
const hasWildcard = node.namedChildren.some((child) => child.type === 'wildcard_import');
|
|
27
|
+
if (hasWildcard) {
|
|
28
|
+
return { kind: 'wildcard', source, name: '*', atNode: node };
|
|
29
|
+
}
|
|
30
|
+
const aliasNode = node.namedChildren.find((child) => child.type === 'import_alias');
|
|
31
|
+
const alias = aliasNode?.namedChildren.find((child) => child.type === 'type_identifier')?.text;
|
|
32
|
+
const importedName = source.split('.').pop() ?? source;
|
|
33
|
+
if (alias !== undefined && alias.length > 0) {
|
|
34
|
+
return { kind: 'alias', source, name: importedName, alias, atNode: node };
|
|
35
|
+
}
|
|
36
|
+
return { kind: 'named', source, name: importedName, atNode: node };
|
|
37
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ParsedImport, WorkspaceIndex } from '../../../../_shared/index.js';
|
|
2
|
+
export interface KotlinResolveContext {
|
|
3
|
+
readonly fromFile: string;
|
|
4
|
+
readonly allFilePaths: ReadonlySet<string>;
|
|
5
|
+
}
|
|
6
|
+
export declare function resolveKotlinImportTarget(parsedImport: ParsedImport, workspaceIndex: WorkspaceIndex): string | null;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export function resolveKotlinImportTarget(parsedImport, workspaceIndex) {
|
|
2
|
+
const ctx = workspaceIndex;
|
|
3
|
+
if (ctx === undefined ||
|
|
4
|
+
typeof ctx.fromFile !== 'string' ||
|
|
5
|
+
!(ctx.allFilePaths instanceof Set)) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
if (parsedImport.kind === 'dynamic-unresolved')
|
|
9
|
+
return null;
|
|
10
|
+
if (parsedImport.targetRaw === null || parsedImport.targetRaw === '')
|
|
11
|
+
return null;
|
|
12
|
+
const target = parsedImport.targetRaw.endsWith('.*')
|
|
13
|
+
? parsedImport.targetRaw.slice(0, -2)
|
|
14
|
+
: parsedImport.targetRaw;
|
|
15
|
+
const pathLike = target.replace(/\./g, '/');
|
|
16
|
+
return (findKotlinFile(ctx.allFilePaths, pathLike) ??
|
|
17
|
+
findKotlinFile(ctx.allFilePaths, pathLike.split('/').slice(0, -1).join('/')) ??
|
|
18
|
+
findByProgressivePrefixStrip(ctx.allFilePaths, pathLike));
|
|
19
|
+
}
|
|
20
|
+
function findKotlinFile(allFilePaths, pathLike) {
|
|
21
|
+
if (pathLike === '')
|
|
22
|
+
return null;
|
|
23
|
+
const extensions = ['.kt', '.kts'];
|
|
24
|
+
const suffix = `/${pathLike}`;
|
|
25
|
+
const dirPrefix = `${pathLike}/`;
|
|
26
|
+
const suffixDirPrefix = `/${dirPrefix}`;
|
|
27
|
+
let suffixFile = null;
|
|
28
|
+
let directoryChild = null;
|
|
29
|
+
for (const raw of allFilePaths) {
|
|
30
|
+
const file = raw.replace(/\\/g, '/');
|
|
31
|
+
if (!extensions.some((ext) => file.endsWith(ext)))
|
|
32
|
+
continue;
|
|
33
|
+
for (const ext of extensions) {
|
|
34
|
+
if (file === `${pathLike}${ext}`)
|
|
35
|
+
return raw;
|
|
36
|
+
if (suffixFile === null && file.endsWith(`${suffix}${ext}`))
|
|
37
|
+
suffixFile = raw;
|
|
38
|
+
}
|
|
39
|
+
if (directoryChild === null) {
|
|
40
|
+
const atRoot = file.startsWith(dirPrefix);
|
|
41
|
+
const atNested = file.includes(suffixDirPrefix);
|
|
42
|
+
if (atRoot || atNested) {
|
|
43
|
+
const idx = atRoot ? 0 : file.indexOf(suffixDirPrefix) + 1;
|
|
44
|
+
const after = file.slice(idx + dirPrefix.length);
|
|
45
|
+
if (after.length > 0 && !after.includes('/'))
|
|
46
|
+
directoryChild = raw;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return suffixFile ?? directoryChild;
|
|
51
|
+
}
|
|
52
|
+
function findByProgressivePrefixStrip(allFilePaths, pathLike) {
|
|
53
|
+
const segments = pathLike.split('/').filter(Boolean);
|
|
54
|
+
for (let skip = 1; skip < segments.length; skip++) {
|
|
55
|
+
const found = findKotlinFile(allFilePaths, segments.slice(skip).join('/'));
|
|
56
|
+
if (found !== null)
|
|
57
|
+
return found;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { emitKotlinScopeCaptures } from './captures.js';
|
|
2
|
+
export { getKotlinCaptureCacheStats, resetKotlinCaptureCacheStats } from './cache-stats.js';
|
|
3
|
+
export { interpretKotlinImport, interpretKotlinTypeBinding } from './interpret.js';
|
|
4
|
+
export { kotlinArityCompatibility } from './arity.js';
|
|
5
|
+
export { resolveKotlinImportTarget, type KotlinResolveContext } from './import-target.js';
|
|
6
|
+
export { kotlinMergeBindings } from './merge-bindings.js';
|
|
7
|
+
export { populateKotlinOwners } from './owners.js';
|
|
8
|
+
export { kotlinBindingScopeFor, kotlinImportOwningScope, kotlinReceiverBinding, } from './simple-hooks.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { emitKotlinScopeCaptures } from './captures.js';
|
|
2
|
+
export { getKotlinCaptureCacheStats, resetKotlinCaptureCacheStats } from './cache-stats.js';
|
|
3
|
+
export { interpretKotlinImport, interpretKotlinTypeBinding } from './interpret.js';
|
|
4
|
+
export { kotlinArityCompatibility } from './arity.js';
|
|
5
|
+
export { resolveKotlinImportTarget } from './import-target.js';
|
|
6
|
+
export { kotlinMergeBindings } from './merge-bindings.js';
|
|
7
|
+
export { populateKotlinOwners } from './owners.js';
|
|
8
|
+
export { kotlinBindingScopeFor, kotlinImportOwningScope, kotlinReceiverBinding, } from './simple-hooks.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CaptureMatch, ParsedImport, ParsedTypeBinding } from '../../../../_shared/index.js';
|
|
2
|
+
export declare function interpretKotlinImport(captures: CaptureMatch): ParsedImport | null;
|
|
3
|
+
export declare function interpretKotlinTypeBinding(captures: CaptureMatch): ParsedTypeBinding | null;
|
|
4
|
+
export declare function normalizeKotlinType(text: string): string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export function interpretKotlinImport(captures) {
|
|
2
|
+
const kind = captures['@import.kind']?.text;
|
|
3
|
+
const source = captures['@import.source']?.text;
|
|
4
|
+
const name = captures['@import.name']?.text;
|
|
5
|
+
if (kind === undefined || source === undefined)
|
|
6
|
+
return null;
|
|
7
|
+
switch (kind) {
|
|
8
|
+
case 'named':
|
|
9
|
+
return {
|
|
10
|
+
kind: 'named',
|
|
11
|
+
localName: name ?? source.split('.').pop() ?? source,
|
|
12
|
+
importedName: name ?? source.split('.').pop() ?? source,
|
|
13
|
+
targetRaw: source,
|
|
14
|
+
};
|
|
15
|
+
case 'alias': {
|
|
16
|
+
const alias = captures['@import.alias']?.text;
|
|
17
|
+
if (alias === undefined || name === undefined)
|
|
18
|
+
return null;
|
|
19
|
+
return {
|
|
20
|
+
kind: 'alias',
|
|
21
|
+
localName: alias,
|
|
22
|
+
importedName: name,
|
|
23
|
+
alias,
|
|
24
|
+
targetRaw: source,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
case 'wildcard':
|
|
28
|
+
return { kind: 'wildcard', targetRaw: source.endsWith('.*') ? source : `${source}.*` };
|
|
29
|
+
default:
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function interpretKotlinTypeBinding(captures) {
|
|
34
|
+
const nameCap = captures['@type-binding.name'];
|
|
35
|
+
const typeCap = captures['@type-binding.type'];
|
|
36
|
+
if (nameCap === undefined || typeCap === undefined)
|
|
37
|
+
return null;
|
|
38
|
+
let source = 'annotation';
|
|
39
|
+
if (captures['@type-binding.self'] !== undefined)
|
|
40
|
+
source = 'self';
|
|
41
|
+
else if (captures['@type-binding.parameter'] !== undefined)
|
|
42
|
+
source = 'parameter-annotation';
|
|
43
|
+
else if (captures['@type-binding.return'] !== undefined)
|
|
44
|
+
source = 'return-annotation';
|
|
45
|
+
else if (captures['@type-binding.constructor'] !== undefined)
|
|
46
|
+
source = 'constructor-inferred';
|
|
47
|
+
return {
|
|
48
|
+
boundName: nameCap.text,
|
|
49
|
+
rawTypeName: normalizeKotlinType(typeCap.text),
|
|
50
|
+
source,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function normalizeKotlinType(text) {
|
|
54
|
+
let out = text.trim();
|
|
55
|
+
while (out.endsWith('?'))
|
|
56
|
+
out = out.slice(0, -1).trim();
|
|
57
|
+
const lastDot = out.lastIndexOf('.');
|
|
58
|
+
if (lastDot >= 0)
|
|
59
|
+
out = out.slice(lastDot + 1);
|
|
60
|
+
const collection = out.match(/^(?:List|MutableList|ArrayList|Set|MutableSet|Collection|Iterable|Sequence|Array)<([^,<>]+)>$/);
|
|
61
|
+
if (collection !== null)
|
|
62
|
+
return normalizeKotlinType(collection[1]);
|
|
63
|
+
const map = out.match(/^(?:Map|MutableMap|HashMap|LinkedHashMap)<[^,<>]+,\s*([^,<>]+)>$/);
|
|
64
|
+
if (map !== null)
|
|
65
|
+
return normalizeKotlinType(map[1]);
|
|
66
|
+
const erased = out.match(/^([A-Za-z_][A-Za-z0-9_]*)<.+>$/s);
|
|
67
|
+
if (erased !== null)
|
|
68
|
+
return erased[1];
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
function tierOf(binding) {
|
|
2
|
+
switch (binding.origin) {
|
|
3
|
+
case 'local':
|
|
4
|
+
return 0;
|
|
5
|
+
case 'import':
|
|
6
|
+
case 'namespace':
|
|
7
|
+
case 'reexport':
|
|
8
|
+
return 1;
|
|
9
|
+
case 'wildcard':
|
|
10
|
+
return 2;
|
|
11
|
+
default:
|
|
12
|
+
return 3;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function kotlinMergeBindings(bindings) {
|
|
16
|
+
if (bindings.length === 0)
|
|
17
|
+
return bindings;
|
|
18
|
+
const best = Math.min(...bindings.map(tierOf));
|
|
19
|
+
const seen = new Map();
|
|
20
|
+
for (const binding of bindings) {
|
|
21
|
+
if (tierOf(binding) === best)
|
|
22
|
+
seen.set(binding.def.nodeId, binding);
|
|
23
|
+
}
|
|
24
|
+
return [...seen.values()];
|
|
25
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { isClassLike, populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js';
|
|
2
|
+
export function populateKotlinOwners(parsed) {
|
|
3
|
+
populateClassOwnedMembers(parsed);
|
|
4
|
+
populateCompanionMembersOnEnclosingClass(parsed);
|
|
5
|
+
}
|
|
6
|
+
function populateCompanionMembersOnEnclosingClass(parsed) {
|
|
7
|
+
const scopesById = new Map();
|
|
8
|
+
for (const scope of parsed.scopes)
|
|
9
|
+
scopesById.set(scope.id, scope);
|
|
10
|
+
for (const scope of parsed.scopes) {
|
|
11
|
+
if (scope.kind !== 'Function' || scope.parent === null)
|
|
12
|
+
continue;
|
|
13
|
+
const parent = scopesById.get(scope.parent);
|
|
14
|
+
if (parent === undefined || parent.kind !== 'Class')
|
|
15
|
+
continue;
|
|
16
|
+
if (parent.ownedDefs.some((def) => isClassLike(def.type)))
|
|
17
|
+
continue;
|
|
18
|
+
const enclosing = findEnclosingClassWithDef(parent.parent, scopesById);
|
|
19
|
+
if (enclosing === undefined)
|
|
20
|
+
continue;
|
|
21
|
+
for (const def of scope.ownedDefs) {
|
|
22
|
+
if (def.ownerId !== undefined)
|
|
23
|
+
continue;
|
|
24
|
+
def.ownerId = enclosing.nodeId;
|
|
25
|
+
qualify(def, enclosing);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function findEnclosingClassWithDef(start, scopesById) {
|
|
30
|
+
let current = start;
|
|
31
|
+
while (current !== null) {
|
|
32
|
+
const scope = scopesById.get(current);
|
|
33
|
+
if (scope === undefined)
|
|
34
|
+
return undefined;
|
|
35
|
+
if (scope.kind === 'Class') {
|
|
36
|
+
const classDef = scope.ownedDefs.find((def) => isClassLike(def.type));
|
|
37
|
+
if (classDef !== undefined)
|
|
38
|
+
return classDef;
|
|
39
|
+
}
|
|
40
|
+
current = scope.parent;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
function qualify(def, owner) {
|
|
45
|
+
if (def.qualifiedName === undefined || def.qualifiedName.includes('.'))
|
|
46
|
+
return;
|
|
47
|
+
if (owner.qualifiedName === undefined || owner.qualifiedName.length === 0)
|
|
48
|
+
return;
|
|
49
|
+
def.qualifiedName = `${owner.qualifiedName}.${def.qualifiedName}`;
|
|
50
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import Parser from 'tree-sitter';
|
|
2
|
+
import Kotlin from 'tree-sitter-kotlin';
|
|
3
|
+
const KOTLIN_SCOPE_QUERY = `
|
|
4
|
+
;; Scopes
|
|
5
|
+
(source_file) @scope.module
|
|
6
|
+
(class_declaration) @scope.class
|
|
7
|
+
(object_declaration) @scope.class
|
|
8
|
+
(companion_object) @scope.class
|
|
9
|
+
(function_declaration) @scope.function
|
|
10
|
+
|
|
11
|
+
;; Declarations — types
|
|
12
|
+
(class_declaration
|
|
13
|
+
"interface"
|
|
14
|
+
(type_identifier) @declaration.name) @declaration.interface
|
|
15
|
+
|
|
16
|
+
(class_declaration
|
|
17
|
+
"class"
|
|
18
|
+
(type_identifier) @declaration.name) @declaration.class
|
|
19
|
+
|
|
20
|
+
(object_declaration
|
|
21
|
+
(type_identifier) @declaration.name) @declaration.class
|
|
22
|
+
|
|
23
|
+
(companion_object
|
|
24
|
+
(type_identifier) @declaration.name) @declaration.class
|
|
25
|
+
|
|
26
|
+
(type_alias
|
|
27
|
+
(type_identifier) @declaration.name) @declaration.type_alias
|
|
28
|
+
|
|
29
|
+
;; Declarations — functions / methods / properties
|
|
30
|
+
(function_declaration
|
|
31
|
+
(simple_identifier) @declaration.name) @declaration.function
|
|
32
|
+
|
|
33
|
+
(property_declaration
|
|
34
|
+
(variable_declaration
|
|
35
|
+
(simple_identifier) @declaration.name)) @declaration.property
|
|
36
|
+
|
|
37
|
+
(class_parameter
|
|
38
|
+
(binding_pattern_kind)
|
|
39
|
+
(simple_identifier) @declaration.name) @declaration.property
|
|
40
|
+
|
|
41
|
+
;; Imports
|
|
42
|
+
(import_header) @import.statement
|
|
43
|
+
|
|
44
|
+
;; Type bindings — parameters
|
|
45
|
+
(parameter
|
|
46
|
+
(simple_identifier) @type-binding.name
|
|
47
|
+
[(user_type) (nullable_type) (function_type)] @type-binding.type) @type-binding.parameter
|
|
48
|
+
|
|
49
|
+
;; Type bindings — property / local annotations
|
|
50
|
+
(property_declaration
|
|
51
|
+
(variable_declaration
|
|
52
|
+
(simple_identifier) @type-binding.name
|
|
53
|
+
[(user_type) (nullable_type) (function_type)] @type-binding.type)) @type-binding.annotation
|
|
54
|
+
|
|
55
|
+
(class_parameter
|
|
56
|
+
(binding_pattern_kind)
|
|
57
|
+
(simple_identifier) @type-binding.name
|
|
58
|
+
[(user_type) (nullable_type) (function_type)] @type-binding.type) @type-binding.annotation
|
|
59
|
+
|
|
60
|
+
;; Type bindings — constructor-inferred val user = User(...)
|
|
61
|
+
(property_declaration
|
|
62
|
+
(variable_declaration
|
|
63
|
+
(simple_identifier) @type-binding.name)
|
|
64
|
+
(call_expression
|
|
65
|
+
(simple_identifier) @type-binding.type)) @type-binding.constructor
|
|
66
|
+
|
|
67
|
+
;; Type bindings — return annotations after function parameters
|
|
68
|
+
(function_declaration
|
|
69
|
+
(simple_identifier) @type-binding.name
|
|
70
|
+
(function_value_parameters)
|
|
71
|
+
[(user_type) (nullable_type) (function_type)] @type-binding.type) @type-binding.return
|
|
72
|
+
|
|
73
|
+
;; References — direct calls / constructor syntax
|
|
74
|
+
(call_expression
|
|
75
|
+
(simple_identifier) @reference.name) @reference.call.free
|
|
76
|
+
|
|
77
|
+
;; References — member calls: obj.method()
|
|
78
|
+
(call_expression
|
|
79
|
+
(navigation_expression
|
|
80
|
+
(_) @reference.receiver
|
|
81
|
+
(navigation_suffix
|
|
82
|
+
(simple_identifier) @reference.name))) @reference.call.member
|
|
83
|
+
|
|
84
|
+
;; References — property writes
|
|
85
|
+
(assignment
|
|
86
|
+
(directly_assignable_expression
|
|
87
|
+
(_) @reference.receiver
|
|
88
|
+
(navigation_suffix
|
|
89
|
+
(simple_identifier) @reference.name))
|
|
90
|
+
(_)) @reference.write.member
|
|
91
|
+
|
|
92
|
+
;; References — property reads
|
|
93
|
+
(navigation_expression
|
|
94
|
+
(_) @reference.receiver
|
|
95
|
+
(navigation_suffix
|
|
96
|
+
(simple_identifier) @reference.name)) @reference.read.member
|
|
97
|
+
`;
|
|
98
|
+
let parser = null;
|
|
99
|
+
let query = null;
|
|
100
|
+
export function getKotlinParser() {
|
|
101
|
+
if (parser === null) {
|
|
102
|
+
parser = new Parser();
|
|
103
|
+
parser.setLanguage(Kotlin);
|
|
104
|
+
}
|
|
105
|
+
return parser;
|
|
106
|
+
}
|
|
107
|
+
export function getKotlinScopeQuery() {
|
|
108
|
+
if (query === null) {
|
|
109
|
+
query = new Parser.Query(Kotlin, KOTLIN_SCOPE_QUERY);
|
|
110
|
+
}
|
|
111
|
+
return query;
|
|
112
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { nodeToCapture, syntheticCapture } from '../../utils/ast-helpers.js';
|
|
2
|
+
import { normalizeKotlinType } from './interpret.js';
|
|
3
|
+
const TYPE_DECL_NODE_TYPES = new Set([
|
|
4
|
+
'class_declaration',
|
|
5
|
+
'object_declaration',
|
|
6
|
+
'companion_object',
|
|
7
|
+
]);
|
|
8
|
+
export function synthesizeKotlinReceiverBinding(fnNode) {
|
|
9
|
+
if (fnNode.type !== 'function_declaration')
|
|
10
|
+
return [];
|
|
11
|
+
const anchorNode = findFunctionBody(fnNode);
|
|
12
|
+
if (anchorNode === null)
|
|
13
|
+
return [];
|
|
14
|
+
const extensionReceiver = extensionReceiverType(fnNode);
|
|
15
|
+
if (extensionReceiver !== null) {
|
|
16
|
+
return [buildReceiverMatch(anchorNode, 'this', extensionReceiver)];
|
|
17
|
+
}
|
|
18
|
+
const enclosingType = findEnclosingTypeDeclaration(fnNode);
|
|
19
|
+
if (enclosingType === null)
|
|
20
|
+
return [];
|
|
21
|
+
const enclosingName = typeDeclarationName(enclosingType);
|
|
22
|
+
if (enclosingName === null)
|
|
23
|
+
return [];
|
|
24
|
+
const out = [buildReceiverMatch(anchorNode, 'this', enclosingName)];
|
|
25
|
+
const superName = firstSuperclassText(enclosingType);
|
|
26
|
+
if (superName !== null)
|
|
27
|
+
out.push(buildReceiverMatch(anchorNode, 'super', superName));
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
function findFunctionBody(fnNode) {
|
|
31
|
+
for (let i = 0; i < fnNode.namedChildCount; i++) {
|
|
32
|
+
const child = fnNode.namedChild(i);
|
|
33
|
+
if (child?.type === 'function_body')
|
|
34
|
+
return child;
|
|
35
|
+
}
|
|
36
|
+
return fnNode;
|
|
37
|
+
}
|
|
38
|
+
function extensionReceiverType(fnNode) {
|
|
39
|
+
for (let i = 0; i < fnNode.namedChildCount; i++) {
|
|
40
|
+
const child = fnNode.namedChild(i);
|
|
41
|
+
if (child === null)
|
|
42
|
+
continue;
|
|
43
|
+
if (child.type === 'simple_identifier')
|
|
44
|
+
return null;
|
|
45
|
+
if (child.type === 'user_type' || child.type === 'nullable_type') {
|
|
46
|
+
return normalizeKotlinType(child.text);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function findEnclosingTypeDeclaration(node) {
|
|
52
|
+
let current = node.parent;
|
|
53
|
+
while (current !== null) {
|
|
54
|
+
if (TYPE_DECL_NODE_TYPES.has(current.type))
|
|
55
|
+
return current;
|
|
56
|
+
current = current.parent;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
function typeDeclarationName(typeNode) {
|
|
61
|
+
if (typeNode.type === 'companion_object') {
|
|
62
|
+
return (typeNode.namedChildren.find((child) => child.type === 'type_identifier')?.text ??
|
|
63
|
+
enclosingNonCompanionTypeName(typeNode) ??
|
|
64
|
+
'Companion');
|
|
65
|
+
}
|
|
66
|
+
return typeNode.namedChildren.find((child) => child.type === 'type_identifier')?.text ?? null;
|
|
67
|
+
}
|
|
68
|
+
function enclosingNonCompanionTypeName(node) {
|
|
69
|
+
let current = node.parent;
|
|
70
|
+
while (current !== null) {
|
|
71
|
+
if (current.type === 'class_declaration' || current.type === 'object_declaration') {
|
|
72
|
+
return current.namedChildren.find((child) => child.type === 'type_identifier')?.text ?? null;
|
|
73
|
+
}
|
|
74
|
+
current = current.parent;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
function firstSuperclassText(typeNode) {
|
|
79
|
+
if (typeNode.type !== 'class_declaration')
|
|
80
|
+
return null;
|
|
81
|
+
for (const child of typeNode.namedChildren) {
|
|
82
|
+
if (child.type !== 'delegation_specifier')
|
|
83
|
+
continue;
|
|
84
|
+
const ctor = child.namedChildren.find((n) => n.type === 'constructor_invocation');
|
|
85
|
+
const userType = ctor?.namedChildren.find((n) => n.type === 'user_type') ??
|
|
86
|
+
child.namedChildren.find((n) => n.type === 'user_type');
|
|
87
|
+
const name = userType?.namedChildren.find((n) => n.type === 'type_identifier')?.text;
|
|
88
|
+
if (name !== undefined)
|
|
89
|
+
return normalizeKotlinType(name);
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
function buildReceiverMatch(anchorNode, name, typeText) {
|
|
94
|
+
const out = {
|
|
95
|
+
'@type-binding.self': nodeToCapture('@type-binding.self', anchorNode),
|
|
96
|
+
'@type-binding.name': syntheticCapture('@type-binding.name', anchorNode, name),
|
|
97
|
+
'@type-binding.type': syntheticCapture('@type-binding.type', anchorNode, typeText),
|
|
98
|
+
};
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js';
|
|
2
|
+
/**
|
|
3
|
+
* Kotlin scope resolver for RFC #909 Ring 3.
|
|
4
|
+
*
|
|
5
|
+
* Kotlin is intentionally registered but not yet listed in
|
|
6
|
+
* `MIGRATED_LANGUAGES`, matching the Java migration pattern from #1482:
|
|
7
|
+
* the resolver can run in shadow/forced mode, while production default
|
|
8
|
+
* stays on the legacy DAG until registry-primary parity reaches the
|
|
9
|
+
* RFC threshold. Forced mode currently passes 154/175 fixtures (88%),
|
|
10
|
+
* including core import, receiver, companion, default-param, vararg,
|
|
11
|
+
* constructor, local assignment-chain, and collection-iteration fixtures.
|
|
12
|
+
* Remaining gaps are advanced TypeEnv behaviors such as smart casts,
|
|
13
|
+
* cross-file iterable return propagation, method-chain fixpoint cases,
|
|
14
|
+
* overload target-id selection, virtual dispatch, and interface default
|
|
15
|
+
* method dispatch.
|
|
16
|
+
*/
|
|
17
|
+
export declare const kotlinScopeResolver: ScopeResolver;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { SupportedLanguages } from '../../../../_shared/index.js';
|
|
2
|
+
import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js';
|
|
3
|
+
import { kotlinProvider } from '../kotlin.js';
|
|
4
|
+
import { kotlinArityCompatibility, kotlinMergeBindings, populateKotlinOwners, resolveKotlinImportTarget, } from './index.js';
|
|
5
|
+
/**
|
|
6
|
+
* Kotlin scope resolver for RFC #909 Ring 3.
|
|
7
|
+
*
|
|
8
|
+
* Kotlin is intentionally registered but not yet listed in
|
|
9
|
+
* `MIGRATED_LANGUAGES`, matching the Java migration pattern from #1482:
|
|
10
|
+
* the resolver can run in shadow/forced mode, while production default
|
|
11
|
+
* stays on the legacy DAG until registry-primary parity reaches the
|
|
12
|
+
* RFC threshold. Forced mode currently passes 154/175 fixtures (88%),
|
|
13
|
+
* including core import, receiver, companion, default-param, vararg,
|
|
14
|
+
* constructor, local assignment-chain, and collection-iteration fixtures.
|
|
15
|
+
* Remaining gaps are advanced TypeEnv behaviors such as smart casts,
|
|
16
|
+
* cross-file iterable return propagation, method-chain fixpoint cases,
|
|
17
|
+
* overload target-id selection, virtual dispatch, and interface default
|
|
18
|
+
* method dispatch.
|
|
19
|
+
*/
|
|
20
|
+
export const kotlinScopeResolver = {
|
|
21
|
+
language: SupportedLanguages.Kotlin,
|
|
22
|
+
languageProvider: kotlinProvider,
|
|
23
|
+
importEdgeReason: 'kotlin-scope: import',
|
|
24
|
+
resolveImportTarget: (targetRaw, fromFile, allFilePaths) => {
|
|
25
|
+
const ws = { fromFile, allFilePaths };
|
|
26
|
+
return resolveKotlinImportTarget({ kind: 'named', localName: '_', importedName: '_', targetRaw }, ws);
|
|
27
|
+
},
|
|
28
|
+
mergeBindings: (existing, incoming) => [...kotlinMergeBindings([...existing, ...incoming])],
|
|
29
|
+
arityCompatibility: (callsite, def) => kotlinArityCompatibility(def, callsite),
|
|
30
|
+
buildMro: (graph, parsedFiles, nodeLookup) => buildMro(graph, parsedFiles, nodeLookup, defaultLinearize),
|
|
31
|
+
populateOwners: (parsed) => populateKotlinOwners(parsed),
|
|
32
|
+
isSuperReceiver: (text) => text.trim() === 'super',
|
|
33
|
+
fieldFallbackOnMethodLookup: false,
|
|
34
|
+
propagatesReturnTypesAcrossImports: true,
|
|
35
|
+
collapseMemberCallsByCallerTarget: false,
|
|
36
|
+
hoistTypeBindingsToModule: true,
|
|
37
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CaptureMatch, ParsedImport, Scope, ScopeId, ScopeTree, TypeRef } from '../../../../_shared/index.js';
|
|
2
|
+
export declare function kotlinBindingScopeFor(decl: CaptureMatch, innermost: Scope, tree: ScopeTree): ScopeId | null;
|
|
3
|
+
export declare function kotlinImportOwningScope(_imp: ParsedImport, _innermost: Scope, _tree: ScopeTree): ScopeId | null;
|
|
4
|
+
export declare function kotlinReceiverBinding(functionScope: Scope): TypeRef | null;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function kotlinBindingScopeFor(decl, innermost, tree) {
|
|
2
|
+
if (decl['@type-binding.return'] === undefined)
|
|
3
|
+
return null;
|
|
4
|
+
let current = innermost;
|
|
5
|
+
while (current !== undefined && current.kind !== 'Module') {
|
|
6
|
+
if (current.parent === null)
|
|
7
|
+
break;
|
|
8
|
+
current = tree.getScope(current.parent);
|
|
9
|
+
}
|
|
10
|
+
return current?.kind === 'Module' ? current.id : null;
|
|
11
|
+
}
|
|
12
|
+
export function kotlinImportOwningScope(_imp, _innermost, _tree) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
export function kotlinReceiverBinding(functionScope) {
|
|
16
|
+
if (functionScope.kind !== 'Function')
|
|
17
|
+
return null;
|
|
18
|
+
return functionScope.typeBindings.get('this') ?? functionScope.typeBindings.get('super') ?? null;
|
|
19
|
+
}
|
|
@@ -26,6 +26,7 @@ import { kotlinMethodConfig } from '../method-extractors/configs/jvm.js';
|
|
|
26
26
|
import { createVariableExtractor } from '../variable-extractors/generic.js';
|
|
27
27
|
import { kotlinVariableConfig } from '../variable-extractors/configs/jvm.js';
|
|
28
28
|
import { createHeritageExtractor } from '../heritage-extractors/generic.js';
|
|
29
|
+
import { emitKotlinScopeCaptures, interpretKotlinImport, interpretKotlinTypeBinding, kotlinArityCompatibility, kotlinBindingScopeFor, kotlinImportOwningScope, kotlinMergeBindings, kotlinReceiverBinding, } from './kotlin/index.js';
|
|
29
30
|
/** Check if a Kotlin function_declaration capture is inside a class_body (i.e., a method).
|
|
30
31
|
* Kotlin grammar uses function_declaration for both top-level functions and class methods.
|
|
31
32
|
* Returns true when the captured definition node has a class_body ancestor. */
|
|
@@ -161,4 +162,13 @@ export const kotlinProvider = defineLanguage({
|
|
|
161
162
|
return 'Method';
|
|
162
163
|
return defaultLabel;
|
|
163
164
|
},
|
|
165
|
+
// ── RFC #909 Ring 3: scope-based resolution hooks ──
|
|
166
|
+
emitScopeCaptures: emitKotlinScopeCaptures,
|
|
167
|
+
interpretImport: interpretKotlinImport,
|
|
168
|
+
interpretTypeBinding: interpretKotlinTypeBinding,
|
|
169
|
+
bindingScopeFor: kotlinBindingScopeFor,
|
|
170
|
+
importOwningScope: kotlinImportOwningScope,
|
|
171
|
+
mergeBindings: (_scope, bindings) => kotlinMergeBindings(bindings),
|
|
172
|
+
receiverBinding: kotlinReceiverBinding,
|
|
173
|
+
arityCompatibility: kotlinArityCompatibility,
|
|
164
174
|
});
|
|
@@ -18,6 +18,7 @@ import { cScopeResolver } from '../../languages/c/scope-resolver.js';
|
|
|
18
18
|
import { cppScopeResolver } from '../../languages/cpp/scope-resolver.js';
|
|
19
19
|
import { phpScopeResolver } from '../../languages/php/scope-resolver.js';
|
|
20
20
|
import { javascriptScopeResolver } from '../../languages/javascript/scope-resolver.js';
|
|
21
|
+
import { kotlinScopeResolver } from '../../languages/kotlin/scope-resolver.js';
|
|
21
22
|
/** Map of `SupportedLanguages` → `ScopeResolver`. The phase iterates
|
|
22
23
|
* this map intersected with `MIGRATED_LANGUAGES` (the per-language
|
|
23
24
|
* flag set) so adding a resolver here without flipping the flag is
|
|
@@ -32,4 +33,5 @@ export const SCOPE_RESOLVERS = new Map([
|
|
|
32
33
|
[SupportedLanguages.CPlusPlus, cppScopeResolver],
|
|
33
34
|
[SupportedLanguages.PHP, phpScopeResolver],
|
|
34
35
|
[SupportedLanguages.JavaScript, javascriptScopeResolver],
|
|
36
|
+
[SupportedLanguages.Kotlin, kotlinScopeResolver],
|
|
35
37
|
]);
|
package/package.json
CHANGED