gitnexus 1.6.6-rc.50 → 1.6.6-rc.52
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/java/captures.js +115 -5
- package/dist/core/ingestion/languages/java/interpret.js +12 -8
- package/dist/core/ingestion/languages/java/package-siblings.d.ts +17 -0
- package/dist/core/ingestion/languages/java/package-siblings.js +134 -0
- package/dist/core/ingestion/languages/java/query.js +75 -0
- package/dist/core/ingestion/languages/java/scope-resolver.d.ts +5 -39
- package/dist/core/ingestion/languages/java/scope-resolver.js +157 -45
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +36 -10
- package/dist/core/ingestion/registry-primary-flag.js +1 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +13 -2
- package/package.json +3 -1
- package/scripts/cross-platform-tests.ts +131 -0
- package/scripts/run-cross-platform.ts +44 -0
- package/scripts/run-parity.ts +128 -0
|
@@ -33,10 +33,6 @@ function shouldEmitReadMember(memberNode) {
|
|
|
33
33
|
if (parent === null)
|
|
34
34
|
return true;
|
|
35
35
|
switch (parent.type) {
|
|
36
|
-
case 'method_invocation':
|
|
37
|
-
// Don't emit read.member when the field_access is the object of a method_invocation
|
|
38
|
-
// (the method call already handles this relationship)
|
|
39
|
-
return parent.childForFieldName('object')?.id !== memberNode.id;
|
|
40
36
|
case 'assignment_expression':
|
|
41
37
|
return parent.childForFieldName('left')?.id !== memberNode.id;
|
|
42
38
|
default:
|
|
@@ -142,11 +138,125 @@ export function emitJavaScopeCaptures(sourceText, _filePath, cachedTree) {
|
|
|
142
138
|
grouped['@reference.arity'] = syntheticCapture('@reference.arity', callNode, String(args.length));
|
|
143
139
|
const argTypes = args.map((arg) => inferArgType(arg));
|
|
144
140
|
grouped['@reference.parameter-types'] = syntheticCapture('@reference.parameter-types', callNode, JSON.stringify(argTypes));
|
|
141
|
+
const argNames = args.map((a) => (a.type === 'identifier' ? a.text : ''));
|
|
142
|
+
if (argNames.some((n) => n !== '')) {
|
|
143
|
+
grouped['@reference.arg-names'] = syntheticCapture('@reference.arg-names', callNode, JSON.stringify(argNames));
|
|
144
|
+
}
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
out.push(grouped);
|
|
148
148
|
}
|
|
149
|
-
return out;
|
|
149
|
+
return resolveVarTypeBindings(out);
|
|
150
|
+
}
|
|
151
|
+
function resolveVarTypeBindings(matches) {
|
|
152
|
+
const returnTypes = new Map();
|
|
153
|
+
const varTypes = new Map();
|
|
154
|
+
const ambiguousReturns = new Set();
|
|
155
|
+
const ambiguousVars = new Set();
|
|
156
|
+
for (const m of matches) {
|
|
157
|
+
if (m['@type-binding.return'] !== undefined &&
|
|
158
|
+
m['@type-binding.type'] !== undefined &&
|
|
159
|
+
m['@type-binding.name'] !== undefined) {
|
|
160
|
+
const name = m['@type-binding.name'].text;
|
|
161
|
+
const type = m['@type-binding.type'].text;
|
|
162
|
+
const existing = returnTypes.get(name);
|
|
163
|
+
if (existing !== undefined && existing !== type) {
|
|
164
|
+
ambiguousReturns.add(name);
|
|
165
|
+
returnTypes.delete(name);
|
|
166
|
+
}
|
|
167
|
+
else if (!ambiguousReturns.has(name)) {
|
|
168
|
+
returnTypes.set(name, type);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (m['@type-binding.annotation'] !== undefined &&
|
|
172
|
+
m['@type-binding.type'] !== undefined &&
|
|
173
|
+
m['@type-binding.name'] !== undefined) {
|
|
174
|
+
const name = m['@type-binding.name'].text;
|
|
175
|
+
const t = m['@type-binding.type'].text;
|
|
176
|
+
if (t !== 'var') {
|
|
177
|
+
const existing = varTypes.get(name);
|
|
178
|
+
if (existing !== undefined && existing !== t) {
|
|
179
|
+
ambiguousVars.add(name);
|
|
180
|
+
varTypes.delete(name);
|
|
181
|
+
}
|
|
182
|
+
else if (!ambiguousVars.has(name)) {
|
|
183
|
+
varTypes.set(name, t);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (m['@type-binding.constructor'] !== undefined &&
|
|
188
|
+
m['@type-binding.type'] !== undefined &&
|
|
189
|
+
m['@type-binding.name'] !== undefined) {
|
|
190
|
+
const name = m['@type-binding.name'].text;
|
|
191
|
+
const type = m['@type-binding.type'].text;
|
|
192
|
+
const existing = varTypes.get(name);
|
|
193
|
+
if (existing !== undefined && existing !== type) {
|
|
194
|
+
ambiguousVars.add(name);
|
|
195
|
+
varTypes.delete(name);
|
|
196
|
+
}
|
|
197
|
+
else if (!ambiguousVars.has(name)) {
|
|
198
|
+
varTypes.set(name, type);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const resolved = [];
|
|
203
|
+
for (const m of matches) {
|
|
204
|
+
if (m['@type-binding.call-result'] !== undefined && m['@type-binding.type'] !== undefined) {
|
|
205
|
+
const methodName = m['@type-binding.type'].text;
|
|
206
|
+
const resolvedType = returnTypes.get(methodName);
|
|
207
|
+
if (resolvedType !== undefined) {
|
|
208
|
+
const patched = { ...m };
|
|
209
|
+
patched['@type-binding.type'] = { ...m['@type-binding.type'], text: resolvedType };
|
|
210
|
+
patched['@type-binding.annotation'] = m['@type-binding.call-result'];
|
|
211
|
+
delete patched['@type-binding.call-result'];
|
|
212
|
+
resolved.push(patched);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (m['@type-binding.alias'] !== undefined && m['@type-binding.type'] !== undefined) {
|
|
217
|
+
const sourceName = m['@type-binding.type'].text;
|
|
218
|
+
const resolvedType = varTypes.get(sourceName);
|
|
219
|
+
if (resolvedType !== undefined) {
|
|
220
|
+
const patched = { ...m };
|
|
221
|
+
patched['@type-binding.type'] = { ...m['@type-binding.type'], text: resolvedType };
|
|
222
|
+
patched['@type-binding.annotation'] = m['@type-binding.alias'];
|
|
223
|
+
delete patched['@type-binding.alias'];
|
|
224
|
+
resolved.push(patched);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (m['@reference.arg-names'] !== undefined && m['@reference.parameter-types'] !== undefined) {
|
|
229
|
+
try {
|
|
230
|
+
const types = JSON.parse(m['@reference.parameter-types'].text);
|
|
231
|
+
const names = JSON.parse(m['@reference.arg-names'].text);
|
|
232
|
+
let patched = false;
|
|
233
|
+
for (let i = 0; i < types.length; i++) {
|
|
234
|
+
if (types[i] === '' && names[i] !== undefined && names[i] !== '') {
|
|
235
|
+
const rt = varTypes.get(names[i]);
|
|
236
|
+
if (rt !== undefined) {
|
|
237
|
+
types[i] = rt;
|
|
238
|
+
patched = true;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (patched) {
|
|
243
|
+
const patchedMatch = { ...m };
|
|
244
|
+
patchedMatch['@reference.parameter-types'] = {
|
|
245
|
+
...m['@reference.parameter-types'],
|
|
246
|
+
text: JSON.stringify(types),
|
|
247
|
+
};
|
|
248
|
+
delete patchedMatch['@reference.arg-names'];
|
|
249
|
+
resolved.push(patchedMatch);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// pass through
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
resolved.push(m);
|
|
258
|
+
}
|
|
259
|
+
return resolved;
|
|
150
260
|
}
|
|
151
261
|
/** Infer a Java argument's static type from literal patterns. */
|
|
152
262
|
function inferArgType(argNode) {
|
|
@@ -19,10 +19,11 @@ export function interpretJavaImport(captures) {
|
|
|
19
19
|
switch (kind) {
|
|
20
20
|
case 'named': {
|
|
21
21
|
// `import com.example.User;`
|
|
22
|
+
const simpleName = sourceCap.text.split('.').pop() ?? sourceCap.text;
|
|
22
23
|
return {
|
|
23
24
|
kind: 'named',
|
|
24
|
-
localName: nameCap?.text ??
|
|
25
|
-
importedName:
|
|
25
|
+
localName: nameCap?.text ?? simpleName,
|
|
26
|
+
importedName: simpleName,
|
|
26
27
|
targetRaw: sourceCap.text,
|
|
27
28
|
};
|
|
28
29
|
}
|
|
@@ -35,17 +36,14 @@ export function interpretJavaImport(captures) {
|
|
|
35
36
|
}
|
|
36
37
|
case 'static': {
|
|
37
38
|
// `import static com.example.Utils.format;`
|
|
38
|
-
// The source contains the full path including the member name
|
|
39
|
-
// (e.g. `com.example.Utils.format`). For file resolution we need
|
|
40
|
-
// the class path (`com.example.Utils`), so strip the final member
|
|
41
|
-
// segment. The local binding name is the member itself.
|
|
42
39
|
const fullSource = sourceCap.text;
|
|
43
40
|
const lastDot = fullSource.lastIndexOf('.');
|
|
41
|
+
const memberName = lastDot >= 0 ? fullSource.slice(lastDot + 1) : fullSource;
|
|
44
42
|
const classPath = lastDot >= 0 ? fullSource.slice(0, lastDot) : fullSource;
|
|
45
43
|
return {
|
|
46
44
|
kind: 'named',
|
|
47
|
-
localName: nameCap?.text ??
|
|
48
|
-
importedName:
|
|
45
|
+
localName: nameCap?.text ?? memberName,
|
|
46
|
+
importedName: memberName,
|
|
49
47
|
targetRaw: classPath,
|
|
50
48
|
};
|
|
51
49
|
}
|
|
@@ -83,6 +81,12 @@ export function interpretJavaTypeBinding(captures) {
|
|
|
83
81
|
source = 'self';
|
|
84
82
|
else if (captures['@type-binding.constructor'] !== undefined)
|
|
85
83
|
source = 'constructor-inferred';
|
|
84
|
+
else if (captures['@type-binding.pattern'] !== undefined)
|
|
85
|
+
source = 'annotation';
|
|
86
|
+
else if (captures['@type-binding.call-result'] !== undefined)
|
|
87
|
+
source = 'annotation';
|
|
88
|
+
else if (captures['@type-binding.alias'] !== undefined)
|
|
89
|
+
source = 'annotation';
|
|
86
90
|
else if (captures['@type-binding.annotation'] !== undefined)
|
|
87
91
|
source = 'annotation';
|
|
88
92
|
else if (captures['@type-binding.return'] !== undefined)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Java package-scope implicit visibility.
|
|
3
|
+
*
|
|
4
|
+
* Classes in the same Java package see each other without explicit
|
|
5
|
+
* `import` statements. This hook groups files by `package` declaration,
|
|
6
|
+
* then injects cross-file class defs into each file's module-scope
|
|
7
|
+
* `bindingAugmentations` and mirrors type-bindings across same-package
|
|
8
|
+
* files — the Java equivalent of C#'s `populateNamespaceSiblings`.
|
|
9
|
+
*/
|
|
10
|
+
import type { ParsedFile } from '../../../../_shared/index.js';
|
|
11
|
+
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
|
12
|
+
export declare function populateJavaPackageSiblings(parsedFiles: readonly ParsedFile[], indexes: ScopeResolutionIndexes, ctx: {
|
|
13
|
+
readonly fileContents: ReadonlyMap<string, string>;
|
|
14
|
+
readonly treeCache?: {
|
|
15
|
+
get(filePath: string): unknown;
|
|
16
|
+
};
|
|
17
|
+
}): void;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Java package-scope implicit visibility.
|
|
3
|
+
*
|
|
4
|
+
* Classes in the same Java package see each other without explicit
|
|
5
|
+
* `import` statements. This hook groups files by `package` declaration,
|
|
6
|
+
* then injects cross-file class defs into each file's module-scope
|
|
7
|
+
* `bindingAugmentations` and mirrors type-bindings across same-package
|
|
8
|
+
* files — the Java equivalent of C#'s `populateNamespaceSiblings`.
|
|
9
|
+
*/
|
|
10
|
+
import { isClassLike } from '../../scope-resolution/scope/walkers.js';
|
|
11
|
+
import { getJavaParser } from './query.js';
|
|
12
|
+
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
|
13
|
+
import { logger } from '../../../logger.js';
|
|
14
|
+
function extractPackageName(content, cachedTree) {
|
|
15
|
+
const tree = cachedTree ??
|
|
16
|
+
parseSourceSafe(getJavaParser(), content);
|
|
17
|
+
for (const child of tree.rootNode.namedChildren) {
|
|
18
|
+
if (child.type === 'package_declaration') {
|
|
19
|
+
const scoped = child.namedChildren.find((c) => c.type === 'scoped_identifier' || c.type === 'identifier');
|
|
20
|
+
return scoped?.text ?? '';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
export function populateJavaPackageSiblings(parsedFiles, indexes, ctx) {
|
|
26
|
+
const buckets = new Map();
|
|
27
|
+
for (const parsed of parsedFiles) {
|
|
28
|
+
const content = ctx.fileContents.get(parsed.filePath);
|
|
29
|
+
if (content === undefined)
|
|
30
|
+
continue;
|
|
31
|
+
const pkg = extractPackageName(content, ctx.treeCache?.get(parsed.filePath));
|
|
32
|
+
let bucket = buckets.get(pkg);
|
|
33
|
+
if (bucket === undefined) {
|
|
34
|
+
bucket = { parsed: [], moduleScopes: [] };
|
|
35
|
+
buckets.set(pkg, bucket);
|
|
36
|
+
}
|
|
37
|
+
bucket.parsed.push(parsed);
|
|
38
|
+
const ms = parsed.scopes.find((s) => s.kind === 'Module');
|
|
39
|
+
if (ms !== undefined) {
|
|
40
|
+
bucket.moduleScopes.push({ filePath: parsed.filePath, scope: ms });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const augmentations = indexes.bindingAugmentations;
|
|
44
|
+
const MAX_PACKAGE_FILES = 500;
|
|
45
|
+
for (const bucket of buckets.values()) {
|
|
46
|
+
if (bucket.moduleScopes.length < 2)
|
|
47
|
+
continue;
|
|
48
|
+
if (bucket.moduleScopes.length > MAX_PACKAGE_FILES) {
|
|
49
|
+
logger.warn(`[java-package-siblings] skipping package with ${bucket.moduleScopes.length} files (cap=${MAX_PACKAGE_FILES}); same-package implicit visibility disabled for this package`);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const classDefs = [];
|
|
53
|
+
for (const parsed of bucket.parsed) {
|
|
54
|
+
const moduleScope = parsed.scopes.find((s) => s.kind === 'Module');
|
|
55
|
+
const moduleScopeId = moduleScope?.id;
|
|
56
|
+
for (const scope of parsed.scopes) {
|
|
57
|
+
if (scope.kind !== 'Class')
|
|
58
|
+
continue;
|
|
59
|
+
if (scope.parent !== moduleScopeId)
|
|
60
|
+
continue;
|
|
61
|
+
for (const def of scope.ownedDefs) {
|
|
62
|
+
if (isClassLike(def.type)) {
|
|
63
|
+
classDefs.push({ def, filePath: parsed.filePath });
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (const { filePath, scope } of bucket.moduleScopes) {
|
|
70
|
+
let scopeAug = augmentations.get(scope.id);
|
|
71
|
+
if (scopeAug === undefined) {
|
|
72
|
+
scopeAug = new Map();
|
|
73
|
+
augmentations.set(scope.id, scopeAug);
|
|
74
|
+
}
|
|
75
|
+
const candidates = classDefs.filter((d) => d.filePath !== filePath);
|
|
76
|
+
const proximityCache = new Map();
|
|
77
|
+
for (const c of candidates) {
|
|
78
|
+
if (!proximityCache.has(c.filePath)) {
|
|
79
|
+
proximityCache.set(c.filePath, sharedSegmentCount(c.filePath, filePath));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const sorted = candidates.sort((a, b) => (proximityCache.get(b.filePath) ?? 0) - (proximityCache.get(a.filePath) ?? 0));
|
|
83
|
+
const injectedIds = new Set();
|
|
84
|
+
for (const { def } of sorted) {
|
|
85
|
+
if (injectedIds.has(def.nodeId))
|
|
86
|
+
continue;
|
|
87
|
+
const qn = def.qualifiedName;
|
|
88
|
+
if (qn === undefined)
|
|
89
|
+
continue;
|
|
90
|
+
injectedIds.add(def.nodeId);
|
|
91
|
+
const simpleName = qn.includes('.') ? qn.slice(qn.lastIndexOf('.') + 1) : qn;
|
|
92
|
+
let list = scopeAug.get(simpleName);
|
|
93
|
+
if (list === undefined) {
|
|
94
|
+
list = [];
|
|
95
|
+
scopeAug.set(simpleName, list);
|
|
96
|
+
}
|
|
97
|
+
list.push({ def, origin: 'namespace' });
|
|
98
|
+
}
|
|
99
|
+
const tb = scope.typeBindings;
|
|
100
|
+
for (const sibling of bucket.moduleScopes) {
|
|
101
|
+
if (sibling.filePath === filePath)
|
|
102
|
+
continue;
|
|
103
|
+
for (const [name, ref] of sibling.scope.typeBindings) {
|
|
104
|
+
if (tb.has(name))
|
|
105
|
+
continue;
|
|
106
|
+
tb.set(name, ref);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const sibParsed of bucket.parsed) {
|
|
110
|
+
if (sibParsed.filePath === filePath)
|
|
111
|
+
continue;
|
|
112
|
+
for (const sibScope of sibParsed.scopes) {
|
|
113
|
+
if (sibScope.kind !== 'Class')
|
|
114
|
+
continue;
|
|
115
|
+
for (const [name, ref] of sibScope.typeBindings) {
|
|
116
|
+
if (ref.source === 'self')
|
|
117
|
+
continue;
|
|
118
|
+
if (tb.has(name))
|
|
119
|
+
continue;
|
|
120
|
+
tb.set(name, ref);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function sharedSegmentCount(a, b) {
|
|
128
|
+
const sa = a.replace(/\\/g, '/').split('/');
|
|
129
|
+
const sb = b.replace(/\\/g, '/').split('/');
|
|
130
|
+
let i = 0;
|
|
131
|
+
while (i < sa.length && i < sb.length && sa[i] === sb[i])
|
|
132
|
+
i++;
|
|
133
|
+
return i;
|
|
134
|
+
}
|
|
@@ -100,6 +100,47 @@ const JAVA_SCOPE_QUERY = `
|
|
|
100
100
|
declarator: (variable_declarator
|
|
101
101
|
name: (identifier) @type-binding.name)) @type-binding.annotation
|
|
102
102
|
|
|
103
|
+
;; Type bindings — var u = svc.getUser(); (Java 10+ call-result inference)
|
|
104
|
+
(local_variable_declaration
|
|
105
|
+
type: (type_identifier) @_var_type
|
|
106
|
+
(#eq? @_var_type "var")
|
|
107
|
+
declarator: (variable_declarator
|
|
108
|
+
name: (identifier) @type-binding.name
|
|
109
|
+
value: (method_invocation
|
|
110
|
+
name: (identifier) @type-binding.type))) @type-binding.call-result
|
|
111
|
+
|
|
112
|
+
;; Type bindings — var alias = u; (Java 10+ alias inference)
|
|
113
|
+
(local_variable_declaration
|
|
114
|
+
type: (type_identifier) @_var_type
|
|
115
|
+
(#eq? @_var_type "var")
|
|
116
|
+
declarator: (variable_declarator
|
|
117
|
+
name: (identifier) @type-binding.name
|
|
118
|
+
value: (identifier) @type-binding.type)) @type-binding.alias
|
|
119
|
+
|
|
120
|
+
;; Type bindings — var addr = user.address; (Java 10+ field-access alias)
|
|
121
|
+
(local_variable_declaration
|
|
122
|
+
type: (type_identifier) @_var_type
|
|
123
|
+
(#eq? @_var_type "var")
|
|
124
|
+
declarator: (variable_declarator
|
|
125
|
+
name: (identifier) @type-binding.name
|
|
126
|
+
value: (field_access
|
|
127
|
+
field: (identifier) @type-binding.type))) @type-binding.alias
|
|
128
|
+
|
|
129
|
+
;; Type bindings — enhanced-for with var: for (var user : users)
|
|
130
|
+
(enhanced_for_statement
|
|
131
|
+
(type_identifier) @_var_type
|
|
132
|
+
(#eq? @_var_type "var")
|
|
133
|
+
(identifier) @type-binding.name
|
|
134
|
+
(identifier) @type-binding.type) @type-binding.alias
|
|
135
|
+
|
|
136
|
+
;; Enhanced-for with var + method iterable: for (var user : data.values())
|
|
137
|
+
(enhanced_for_statement
|
|
138
|
+
(type_identifier) @_var_type
|
|
139
|
+
(#eq? @_var_type "var")
|
|
140
|
+
(identifier) @type-binding.name
|
|
141
|
+
(method_invocation
|
|
142
|
+
object: (identifier) @type-binding.type)) @type-binding.alias
|
|
143
|
+
|
|
103
144
|
;; Type bindings — var u = new User(); (Java 10+ local variable type inference)
|
|
104
145
|
;; tree-sitter-java parses \`var\` as a \`type_identifier\` with text "var".
|
|
105
146
|
;; The type-binding.constructor anchor fires when the rhs is an
|
|
@@ -141,6 +182,16 @@ const JAVA_SCOPE_QUERY = `
|
|
|
141
182
|
type: (generic_type) @type-binding.type
|
|
142
183
|
name: (identifier) @type-binding.name) @type-binding.annotation
|
|
143
184
|
|
|
185
|
+
;; Type bindings — instanceof pattern (Java 16+): if (obj instanceof User user)
|
|
186
|
+
(instanceof_expression
|
|
187
|
+
(type_identifier) @type-binding.type
|
|
188
|
+
(identifier) @type-binding.name) @type-binding.pattern
|
|
189
|
+
|
|
190
|
+
;; Type bindings — switch case pattern (Java 21+): case User user ->
|
|
191
|
+
(type_pattern
|
|
192
|
+
(type_identifier) @type-binding.type
|
|
193
|
+
(identifier) @type-binding.name) @type-binding.pattern
|
|
194
|
+
|
|
144
195
|
;; References — all method calls: foo() and obj.method()
|
|
145
196
|
;; tree-sitter-java's query engine drops negation-based \`!object\`
|
|
146
197
|
;; patterns when a positive \`object:\` pattern exists for the same
|
|
@@ -164,6 +215,30 @@ const JAVA_SCOPE_QUERY = `
|
|
|
164
215
|
(object_creation_expression
|
|
165
216
|
type: (scoped_type_identifier) @reference.call.constructor.qualified) @reference.call.constructor
|
|
166
217
|
|
|
218
|
+
;; References — method references: User::getName, obj::method
|
|
219
|
+
(method_reference
|
|
220
|
+
(identifier) @reference.receiver
|
|
221
|
+
(identifier) @reference.name) @reference.call.member
|
|
222
|
+
|
|
223
|
+
;; References — this::method and super::method
|
|
224
|
+
(method_reference
|
|
225
|
+
(this) @reference.receiver
|
|
226
|
+
(identifier) @reference.name) @reference.call.member
|
|
227
|
+
|
|
228
|
+
(method_reference
|
|
229
|
+
(super) @reference.receiver
|
|
230
|
+
(identifier) @reference.name) @reference.call.member
|
|
231
|
+
|
|
232
|
+
;; References — field_access::method: responseBuilder::buildResponse
|
|
233
|
+
(method_reference
|
|
234
|
+
(field_access) @reference.receiver
|
|
235
|
+
(identifier) @reference.name) @reference.call.member
|
|
236
|
+
|
|
237
|
+
;; References — constructor references: User::new
|
|
238
|
+
(method_reference
|
|
239
|
+
(identifier) @reference.name
|
|
240
|
+
"new") @reference.call.constructor
|
|
241
|
+
|
|
167
242
|
;; References — field/property writes: obj.name = "x"
|
|
168
243
|
(assignment_expression
|
|
169
244
|
left: (field_access
|
|
@@ -4,46 +4,12 @@
|
|
|
4
4
|
*
|
|
5
5
|
* ## Registry-primary parity status
|
|
6
6
|
*
|
|
7
|
-
* Java is
|
|
8
|
-
*
|
|
9
|
-
* (`REGISTRY_PRIMARY_JAVA=1`) is 143/172 (83%). The 29 gaps fall into:
|
|
7
|
+
* Java is in `MIGRATED_LANGUAGES` — the scope-resolution registry is
|
|
8
|
+
* the primary call-resolution path. Parity: 178/178 (100%).
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* - virtual dispatch / interface default methods
|
|
15
|
-
*
|
|
16
|
-
* These are the same category of advanced-resolution gaps seen in prior
|
|
17
|
-
* migrations (Python, C#, Go). Parity is below the ≥99% flip threshold
|
|
18
|
-
* per RFC §6.4.
|
|
19
|
-
*
|
|
20
|
-
* **CI visibility:** Because Java is absent from `MIGRATED_LANGUAGES`,
|
|
21
|
-
* the parity CI workflow (`ci-scope-parity.yml`) does not run Java in
|
|
22
|
-
* either `REGISTRY_PRIMARY_JAVA=0` or `=1` mode. Regressions in forced
|
|
23
|
-
* mode are only visible via manual `REGISTRY_PRIMARY_JAVA=1 npx vitest
|
|
24
|
-
* run java.test.ts`. Before flipping Java to registry-primary, a
|
|
25
|
-
* non-required CI step should be added to run Java tests in forced mode
|
|
26
|
-
* and report parity as a dashboard input.
|
|
27
|
-
*
|
|
28
|
-
* **Parity baseline (29 failures):** The 29 gaps in forced registry mode
|
|
29
|
-
* are tracked in this PR (#1482) and this JSDoc. If the gap count
|
|
30
|
-
* changes (up or down), update this baseline accordingly.
|
|
31
|
-
*
|
|
32
|
-
* ### Known flip-blockers (must fix before adding to MIGRATED_LANGUAGES)
|
|
33
|
-
*
|
|
34
|
-
* - Varargs arity: fixed-prefix count is now preserved, but no
|
|
35
|
-
* integration fixture exercises the 0-arg rejection path yet.
|
|
36
|
-
* - Static import resolution: `import static X.Y.m` now correctly
|
|
37
|
-
* resolves to `X/Y.java` (the class), not `X/Y/m.java` (the member).
|
|
38
|
-
* Edge cases with nested classes may remain.
|
|
39
|
-
* - Generic superclass receiver binding: `BaseModel<T>` now strips
|
|
40
|
-
* to `BaseModel` via JVM type-erasure fallback in `stripGeneric`.
|
|
41
|
-
* - Wildcard import (`import com.example.*`) file selection is
|
|
42
|
-
* nondeterministic when multiple classes share a package directory.
|
|
43
|
-
* May produce wrong-file edges in forced mode.
|
|
44
|
-
* - Qualified generic type parameters in field/parameter annotations
|
|
45
|
-
* (`com.example.BaseModel<T>`) — rare in practice but may miss
|
|
46
|
-
* resolution when the full qualifier is present with generics.
|
|
10
|
+
* **CI visibility:** The parity CI workflow (`ci-scope-parity.yml`)
|
|
11
|
+
* runs Java tests in both `REGISTRY_PRIMARY_JAVA=0` and `=1` modes
|
|
12
|
+
* automatically.
|
|
47
13
|
*/
|
|
48
14
|
import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js';
|
|
49
15
|
declare const javaScopeResolver: ScopeResolver;
|
|
@@ -4,52 +4,21 @@
|
|
|
4
4
|
*
|
|
5
5
|
* ## Registry-primary parity status
|
|
6
6
|
*
|
|
7
|
-
* Java is
|
|
8
|
-
*
|
|
9
|
-
* (`REGISTRY_PRIMARY_JAVA=1`) is 143/172 (83%). The 29 gaps fall into:
|
|
7
|
+
* Java is in `MIGRATED_LANGUAGES` — the scope-resolution registry is
|
|
8
|
+
* the primary call-resolution path. Parity: 178/178 (100%).
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* - virtual dispatch / interface default methods
|
|
15
|
-
*
|
|
16
|
-
* These are the same category of advanced-resolution gaps seen in prior
|
|
17
|
-
* migrations (Python, C#, Go). Parity is below the ≥99% flip threshold
|
|
18
|
-
* per RFC §6.4.
|
|
19
|
-
*
|
|
20
|
-
* **CI visibility:** Because Java is absent from `MIGRATED_LANGUAGES`,
|
|
21
|
-
* the parity CI workflow (`ci-scope-parity.yml`) does not run Java in
|
|
22
|
-
* either `REGISTRY_PRIMARY_JAVA=0` or `=1` mode. Regressions in forced
|
|
23
|
-
* mode are only visible via manual `REGISTRY_PRIMARY_JAVA=1 npx vitest
|
|
24
|
-
* run java.test.ts`. Before flipping Java to registry-primary, a
|
|
25
|
-
* non-required CI step should be added to run Java tests in forced mode
|
|
26
|
-
* and report parity as a dashboard input.
|
|
27
|
-
*
|
|
28
|
-
* **Parity baseline (29 failures):** The 29 gaps in forced registry mode
|
|
29
|
-
* are tracked in this PR (#1482) and this JSDoc. If the gap count
|
|
30
|
-
* changes (up or down), update this baseline accordingly.
|
|
31
|
-
*
|
|
32
|
-
* ### Known flip-blockers (must fix before adding to MIGRATED_LANGUAGES)
|
|
33
|
-
*
|
|
34
|
-
* - Varargs arity: fixed-prefix count is now preserved, but no
|
|
35
|
-
* integration fixture exercises the 0-arg rejection path yet.
|
|
36
|
-
* - Static import resolution: `import static X.Y.m` now correctly
|
|
37
|
-
* resolves to `X/Y.java` (the class), not `X/Y/m.java` (the member).
|
|
38
|
-
* Edge cases with nested classes may remain.
|
|
39
|
-
* - Generic superclass receiver binding: `BaseModel<T>` now strips
|
|
40
|
-
* to `BaseModel` via JVM type-erasure fallback in `stripGeneric`.
|
|
41
|
-
* - Wildcard import (`import com.example.*`) file selection is
|
|
42
|
-
* nondeterministic when multiple classes share a package directory.
|
|
43
|
-
* May produce wrong-file edges in forced mode.
|
|
44
|
-
* - Qualified generic type parameters in field/parameter annotations
|
|
45
|
-
* (`com.example.BaseModel<T>`) — rare in practice but may miss
|
|
46
|
-
* resolution when the full qualifier is present with generics.
|
|
10
|
+
* **CI visibility:** The parity CI workflow (`ci-scope-parity.yml`)
|
|
11
|
+
* runs Java tests in both `REGISTRY_PRIMARY_JAVA=0` and `=1` modes
|
|
12
|
+
* automatically.
|
|
47
13
|
*/
|
|
48
14
|
import { SupportedLanguages } from '../../../../_shared/index.js';
|
|
49
15
|
import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js';
|
|
50
|
-
import {
|
|
16
|
+
import { resolveDefGraphId } from '../../scope-resolution/graph-bridge/ids.js';
|
|
17
|
+
import { isClassLike, lookupBindingsAt, namesAtScope, populateClassOwnedMembers, } from '../../scope-resolution/scope/walkers.js';
|
|
18
|
+
import { followChainPostFinalize } from '../../scope-resolution/passes/imported-return-types.js';
|
|
51
19
|
import { javaProvider } from '../java.js';
|
|
52
20
|
import { javaArityCompatibility, javaMergeBindings, resolveJavaImportTarget, } from './index.js';
|
|
21
|
+
import { populateJavaPackageSiblings } from './package-siblings.js';
|
|
53
22
|
const javaScopeResolver = {
|
|
54
23
|
language: SupportedLanguages.Java,
|
|
55
24
|
languageProvider: javaProvider,
|
|
@@ -60,15 +29,158 @@ const javaScopeResolver = {
|
|
|
60
29
|
},
|
|
61
30
|
mergeBindings: (existing, incoming) => [...javaMergeBindings([...existing, ...incoming])],
|
|
62
31
|
arityCompatibility: (callsite, def) => javaArityCompatibility(def, callsite),
|
|
63
|
-
buildMro:
|
|
32
|
+
buildMro: buildJavaMro,
|
|
64
33
|
populateOwners: (parsed) => populateClassOwnedMembers(parsed),
|
|
65
34
|
isSuperReceiver: (text) => text.trim() === 'super',
|
|
66
|
-
// Java is statically typed — field-fallback heuristic stays off
|
|
67
35
|
fieldFallbackOnMethodLookup: false,
|
|
68
36
|
propagatesReturnTypesAcrossImports: true,
|
|
69
|
-
|
|
70
|
-
collapseMemberCallsByCallerTarget: false,
|
|
71
|
-
// Hoist return-type bindings to Module scope for cross-file propagation
|
|
37
|
+
collapseMemberCallsByCallerTarget: true,
|
|
72
38
|
hoistTypeBindingsToModule: true,
|
|
39
|
+
populateNamespaceSiblings: populateJavaPackageSiblings,
|
|
40
|
+
populateRangeBindings: populateJavaCrossFileReturnTypes,
|
|
73
41
|
};
|
|
74
42
|
export { javaScopeResolver };
|
|
43
|
+
function populateJavaCrossFileReturnTypes(parsedFiles, indexes) {
|
|
44
|
+
const moduleScopeByFile = new Map();
|
|
45
|
+
const classScopesByFile = new Map();
|
|
46
|
+
for (const parsed of parsedFiles) {
|
|
47
|
+
const ms = parsed.scopes.find((s) => s.kind === 'Module');
|
|
48
|
+
if (ms !== undefined)
|
|
49
|
+
moduleScopeByFile.set(parsed.filePath, ms);
|
|
50
|
+
const cs = parsed.scopes.filter((s) => s.kind === 'Class');
|
|
51
|
+
if (cs.length > 0)
|
|
52
|
+
classScopesByFile.set(parsed.filePath, cs);
|
|
53
|
+
}
|
|
54
|
+
for (const parsed of parsedFiles) {
|
|
55
|
+
const importerModule = moduleScopeByFile.get(parsed.filePath);
|
|
56
|
+
if (importerModule === undefined)
|
|
57
|
+
continue;
|
|
58
|
+
const ambiguousMirrors = new Set();
|
|
59
|
+
for (const name of namesAtScope(importerModule.id, indexes)) {
|
|
60
|
+
const refs = lookupBindingsAt(importerModule.id, name, indexes);
|
|
61
|
+
for (const ref of refs) {
|
|
62
|
+
if (ref.origin !== 'import' && ref.origin !== 'reexport')
|
|
63
|
+
continue;
|
|
64
|
+
if (!isClassLike(ref.def.type))
|
|
65
|
+
continue;
|
|
66
|
+
const sourceModule = moduleScopeByFile.get(ref.def.filePath);
|
|
67
|
+
if (sourceModule === undefined)
|
|
68
|
+
continue;
|
|
69
|
+
const tb = importerModule.typeBindings;
|
|
70
|
+
for (const [srcName, srcRef] of sourceModule.typeBindings) {
|
|
71
|
+
if (srcRef.source !== 'return-annotation')
|
|
72
|
+
continue;
|
|
73
|
+
if (ambiguousMirrors.has(srcName))
|
|
74
|
+
continue;
|
|
75
|
+
const existing = tb.get(srcName);
|
|
76
|
+
if (existing !== undefined && existing.rawName !== srcRef.rawName) {
|
|
77
|
+
ambiguousMirrors.add(srcName);
|
|
78
|
+
tb.delete(srcName);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (existing === undefined)
|
|
82
|
+
tb.set(srcName, srcRef);
|
|
83
|
+
}
|
|
84
|
+
for (const classScope of classScopesByFile.get(ref.def.filePath) ?? []) {
|
|
85
|
+
for (const [srcName, srcRef] of classScope.typeBindings) {
|
|
86
|
+
if (srcRef.source === 'self' || srcRef.source === 'parameter-annotation')
|
|
87
|
+
continue;
|
|
88
|
+
if (ambiguousMirrors.has(srcName))
|
|
89
|
+
continue;
|
|
90
|
+
const existing = tb.get(srcName);
|
|
91
|
+
if (existing !== undefined && existing.rawName !== srcRef.rawName) {
|
|
92
|
+
ambiguousMirrors.add(srcName);
|
|
93
|
+
tb.delete(srcName);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (existing === undefined)
|
|
97
|
+
tb.set(srcName, srcRef);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (const [name, ref] of importerModule.typeBindings) {
|
|
103
|
+
const resolved = followChainPostFinalize(ref, importerModule.id, indexes);
|
|
104
|
+
if (resolved !== ref) {
|
|
105
|
+
importerModule.typeBindings.set(name, resolved);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const parsed of parsedFiles) {
|
|
110
|
+
const moduleScopeId = moduleScopeByFile.get(parsed.filePath)?.id;
|
|
111
|
+
for (const scope of parsed.scopes) {
|
|
112
|
+
if (scope.id === moduleScopeId)
|
|
113
|
+
continue;
|
|
114
|
+
for (const [name, ref] of scope.typeBindings) {
|
|
115
|
+
const resolved = followChainPostFinalize(ref, scope.id, indexes);
|
|
116
|
+
if (resolved !== ref) {
|
|
117
|
+
scope.typeBindings.set(name, resolved);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function buildJavaMro(graph, parsedFiles, nodeLookup) {
|
|
124
|
+
const mro = buildMro(graph, parsedFiles, nodeLookup, defaultLinearize);
|
|
125
|
+
const defIdByGraphId = new Map();
|
|
126
|
+
for (const parsed of parsedFiles) {
|
|
127
|
+
for (const def of parsed.localDefs) {
|
|
128
|
+
if (!isClassLike(def.type))
|
|
129
|
+
continue;
|
|
130
|
+
const graphId = resolveDefGraphId(parsed.filePath, def, nodeLookup);
|
|
131
|
+
if (graphId !== undefined)
|
|
132
|
+
defIdByGraphId.set(graphId, def.nodeId);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const directImpls = new Map();
|
|
136
|
+
for (const rel of graph.iterRelationshipsByType('IMPLEMENTS')) {
|
|
137
|
+
const source = defIdByGraphId.get(rel.sourceId);
|
|
138
|
+
const target = defIdByGraphId.get(rel.targetId);
|
|
139
|
+
if (source === undefined || target === undefined)
|
|
140
|
+
continue;
|
|
141
|
+
let list = directImpls.get(source);
|
|
142
|
+
if (list === undefined) {
|
|
143
|
+
list = [];
|
|
144
|
+
directImpls.set(source, list);
|
|
145
|
+
}
|
|
146
|
+
if (!list.includes(target))
|
|
147
|
+
list.push(target);
|
|
148
|
+
}
|
|
149
|
+
for (const [classDefId, extendsMro] of mro) {
|
|
150
|
+
const ancestorChain = [classDefId, ...extendsMro];
|
|
151
|
+
const seeds = [];
|
|
152
|
+
for (const ancestorId of ancestorChain) {
|
|
153
|
+
for (const ifaceId of directImpls.get(ancestorId) ?? []) {
|
|
154
|
+
seeds.push(ifaceId);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (seeds.length === 0)
|
|
158
|
+
continue;
|
|
159
|
+
const interfaces = closeInterfaces(seeds, directImpls);
|
|
160
|
+
mro.set(classDefId, [...extendsMro, ...interfaces.filter((i) => !extendsMro.includes(i))]);
|
|
161
|
+
}
|
|
162
|
+
for (const [classDefId, ifaces] of directImpls) {
|
|
163
|
+
if (mro.has(classDefId))
|
|
164
|
+
continue;
|
|
165
|
+
mro.set(classDefId, closeInterfaces([...ifaces], directImpls));
|
|
166
|
+
}
|
|
167
|
+
return mro;
|
|
168
|
+
}
|
|
169
|
+
function closeInterfaces(seeds, directImpls) {
|
|
170
|
+
const out = [];
|
|
171
|
+
const seen = new Set();
|
|
172
|
+
const queue = [...seeds];
|
|
173
|
+
let head = 0;
|
|
174
|
+
while (head < queue.length) {
|
|
175
|
+
const cur = queue[head++];
|
|
176
|
+
if (seen.has(cur))
|
|
177
|
+
continue;
|
|
178
|
+
seen.add(cur);
|
|
179
|
+
out.push(cur);
|
|
180
|
+
for (const next of directImpls.get(cur) ?? []) {
|
|
181
|
+
if (!seen.has(next))
|
|
182
|
+
queue.push(next);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
@@ -22,6 +22,7 @@ import { processHeritage, processHeritageFromExtracted, extractExtractedHeritage
|
|
|
22
22
|
import { createResolutionContext } from '../model/resolution-context.js';
|
|
23
23
|
import { createASTCache } from '../ast-cache.js';
|
|
24
24
|
import { getLanguageFromFilename } from '../../../_shared/index.js';
|
|
25
|
+
import { isRegistryPrimary } from '../registry-primary-flag.js';
|
|
25
26
|
import { readFileContents } from '../filesystem-walker.js';
|
|
26
27
|
import { isLanguageAvailable } from '../../tree-sitter/parser-loader.js';
|
|
27
28
|
import { createWorkerPool, WorkerPoolInitializationError } from '../workers/worker-pool.js';
|
|
@@ -461,14 +462,37 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
461
462
|
if (chunkNeedsSynthesis[chunkIdx]) {
|
|
462
463
|
anyChunkNeedsWildcardSynth = true;
|
|
463
464
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
465
|
+
const skipFile = new Set();
|
|
466
|
+
const checkFile = new Set();
|
|
467
|
+
const shouldAccumulate = (filePath) => {
|
|
468
|
+
if (checkFile.has(filePath))
|
|
469
|
+
return true;
|
|
470
|
+
if (skipFile.has(filePath))
|
|
471
|
+
return false;
|
|
472
|
+
const lang = getLanguageFromFilename(filePath);
|
|
473
|
+
if (lang !== null && isRegistryPrimary(lang)) {
|
|
474
|
+
skipFile.add(filePath);
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
checkFile.add(filePath);
|
|
478
|
+
return true;
|
|
479
|
+
};
|
|
480
|
+
for (const item of chunkWorkerData.imports) {
|
|
481
|
+
if (shouldAccumulate(item.filePath))
|
|
482
|
+
deferredWorkerImports.push(item);
|
|
483
|
+
}
|
|
484
|
+
for (const item of chunkWorkerData.calls) {
|
|
485
|
+
if (shouldAccumulate(item.filePath))
|
|
486
|
+
deferredWorkerCalls.push(item);
|
|
487
|
+
}
|
|
488
|
+
for (const item of chunkWorkerData.heritage) {
|
|
489
|
+
if (shouldAccumulate(item.filePath))
|
|
490
|
+
deferredWorkerHeritage.push(item);
|
|
491
|
+
}
|
|
492
|
+
for (const item of chunkWorkerData.constructorBindings) {
|
|
493
|
+
if (shouldAccumulate(item.filePath))
|
|
494
|
+
deferredConstructorBindings.push(item);
|
|
495
|
+
}
|
|
472
496
|
// Aggregate worker-produced ParsedFile artifacts so scope-
|
|
473
497
|
// resolution can use them as a re-extraction cache (skips its
|
|
474
498
|
// own tree-sitter re-parse on warm runs).
|
|
@@ -477,8 +501,10 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
477
501
|
allParsedFiles.push(item);
|
|
478
502
|
}
|
|
479
503
|
if (chunkWorkerData.assignments?.length) {
|
|
480
|
-
for (const item of chunkWorkerData.assignments)
|
|
481
|
-
|
|
504
|
+
for (const item of chunkWorkerData.assignments) {
|
|
505
|
+
if (shouldAccumulate(item.filePath))
|
|
506
|
+
deferredAssignments.push(item);
|
|
507
|
+
}
|
|
482
508
|
}
|
|
483
509
|
if (chunkWorkerData.fileScopeBindings?.length) {
|
|
484
510
|
for (const { filePath, bindings } of chunkWorkerData.fileScopeBindings) {
|
|
@@ -41,7 +41,7 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
|
|
|
41
41
|
if (site.callForm === 'constructor') {
|
|
42
42
|
const classDef = findClassBindingInScope(site.inScope, site.name, scopes);
|
|
43
43
|
if (classDef !== undefined) {
|
|
44
|
-
fnDef = pickConstructorOrClass(classDef, workspaceIndex);
|
|
44
|
+
fnDef = pickConstructorOrClass(classDef, workspaceIndex, scopes);
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
// Implicit-this overload narrowing: an unqualified call inside
|
|
@@ -449,7 +449,7 @@ function logicalCallableKey(def) {
|
|
|
449
449
|
* Class def itself when no explicit Constructor exists. Matches
|
|
450
450
|
* legacy behavior — tests assert targetLabel === 'Class' for implicit
|
|
451
451
|
* ctors and targetLabel === 'Constructor' for explicit ones. */
|
|
452
|
-
function pickConstructorOrClass(classDef, workspaceIndex) {
|
|
452
|
+
function pickConstructorOrClass(classDef, workspaceIndex, scopes) {
|
|
453
453
|
const classScope = workspaceIndex.classScopeByDefId.get(classDef.nodeId);
|
|
454
454
|
if (classScope === undefined)
|
|
455
455
|
return classDef;
|
|
@@ -457,6 +457,17 @@ function pickConstructorOrClass(classDef, workspaceIndex) {
|
|
|
457
457
|
if (def.type === 'Constructor')
|
|
458
458
|
return def;
|
|
459
459
|
}
|
|
460
|
+
if (scopes !== undefined) {
|
|
461
|
+
for (const childId of scopes.scopeTree.getChildren(classScope.id)) {
|
|
462
|
+
const childScope = scopes.scopeTree.getScope(childId);
|
|
463
|
+
if (childScope === undefined || childScope.kind === 'Class')
|
|
464
|
+
continue;
|
|
465
|
+
for (const def of childScope.ownedDefs) {
|
|
466
|
+
if (def.type === 'Constructor')
|
|
467
|
+
return def;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
460
471
|
return classDef;
|
|
461
472
|
}
|
|
462
473
|
/** Walk up from the call-site scope to the enclosing class scope,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitnexus",
|
|
3
|
-
"version": "1.6.6-rc.
|
|
3
|
+
"version": "1.6.6-rc.52",
|
|
4
4
|
"description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
|
|
5
5
|
"author": "Abhigyan Patwari",
|
|
6
6
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
@@ -48,6 +48,8 @@
|
|
|
48
48
|
"test:integration": "vitest run test/integration",
|
|
49
49
|
"test:watch": "vitest",
|
|
50
50
|
"test:coverage": "vitest run --coverage",
|
|
51
|
+
"test:parity": "tsx scripts/run-parity.ts",
|
|
52
|
+
"test:cross-platform": "tsx scripts/run-cross-platform.ts",
|
|
51
53
|
"postinstall": "node scripts/materialize-vendor-grammars.cjs && node scripts/build-tree-sitter-dart.cjs && node scripts/build-tree-sitter-proto.cjs && node scripts/build-tree-sitter-swift.cjs",
|
|
52
54
|
"prepare": "node scripts/build.js",
|
|
53
55
|
"prepack": "node scripts/build.js"
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform test subset runner.
|
|
3
|
+
*
|
|
4
|
+
* Runs only the tests that exercise platform-sensitive behavior on
|
|
5
|
+
* Windows and macOS. The full suite runs on Ubuntu; this narrows the
|
|
6
|
+
* cross-platform matrix to tests that actually vary across OSes.
|
|
7
|
+
*
|
|
8
|
+
* Categories included:
|
|
9
|
+
* - Platform-specific logic (path.sep, process.platform guards)
|
|
10
|
+
* - Native addon loading (LadybugDB, tree-sitter)
|
|
11
|
+
* - Process spawning and shell behavior
|
|
12
|
+
* - Filesystem locking and temp-dir behavior
|
|
13
|
+
* - Worker threads (real, not mocked)
|
|
14
|
+
* - CLI end-to-end tests
|
|
15
|
+
*
|
|
16
|
+
* When adding a new test that uses platform-varying APIs (native addons,
|
|
17
|
+
* child_process with real spawning, filesystem locking, path.sep), add
|
|
18
|
+
* it to the appropriate section below.
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* npx vitest run $(npx tsx scripts/cross-platform-tests.ts)
|
|
22
|
+
* # or via the package script:
|
|
23
|
+
* npm run test:cross-platform
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
// Platform-specific logic tests — contain explicit process.platform guards
|
|
27
|
+
// or test behavior that differs across operating systems
|
|
28
|
+
const PLATFORM_LOGIC = [
|
|
29
|
+
'test/unit/setup.test.ts',
|
|
30
|
+
'test/unit/setup-jsonc.test.ts',
|
|
31
|
+
'test/unit/setup-codex.test.ts',
|
|
32
|
+
'test/unit/platform-capabilities.test.ts',
|
|
33
|
+
'test/unit/worker-pool-windows-quarantine.test.ts',
|
|
34
|
+
'test/unit/lbug-pool-win-fts-probe.test.ts',
|
|
35
|
+
'test/unit/repo-manager.test.ts',
|
|
36
|
+
'test/unit/repo-manager-finalize-invariant.test.ts',
|
|
37
|
+
'test/unit/hooks.test.ts',
|
|
38
|
+
'test/unit/cursor-hook.test.ts',
|
|
39
|
+
'test/unit/sidecar-recovery.test.ts',
|
|
40
|
+
'test/unit/pool-wal-recovery.test.ts',
|
|
41
|
+
'test/unit/detect-changes-worktree.test.ts',
|
|
42
|
+
'test/unit/eval-server-bind-restriction.test.ts',
|
|
43
|
+
'test/unit/ignore-service.test.ts',
|
|
44
|
+
'test/unit/group/bridge-db.test.ts',
|
|
45
|
+
'test/unit/group/bridge-db-edge.test.ts',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
// Native LadybugDB integration tests — exercise the @ladybugdb/core
|
|
49
|
+
// N-API addon which has known platform-specific behavior (Windows
|
|
50
|
+
// file-lock lag after close, macOS N-API destructor segfaults)
|
|
51
|
+
const LBUG_NATIVE = [
|
|
52
|
+
'test/integration/lbug-core-adapter.test.ts',
|
|
53
|
+
'test/integration/lbug-vector-extension.test.ts',
|
|
54
|
+
'test/integration/lbug-pool.test.ts',
|
|
55
|
+
'test/integration/lbug-pool-stability.test.ts',
|
|
56
|
+
'test/integration/lbug-lock-retry.test.ts',
|
|
57
|
+
'test/integration/lbug-open-retry.test.ts',
|
|
58
|
+
'test/integration/lbug-close-handle-release.test.ts',
|
|
59
|
+
'test/integration/lbug-orphan-sidecar-recovery.test.ts',
|
|
60
|
+
'test/integration/lbug-readonly-init.test.ts',
|
|
61
|
+
'test/integration/local-backend.test.ts',
|
|
62
|
+
'test/integration/local-backend-calltool.test.ts',
|
|
63
|
+
'test/integration/search-core.test.ts',
|
|
64
|
+
'test/integration/search-pool.test.ts',
|
|
65
|
+
'test/integration/staleness-and-stability.test.ts',
|
|
66
|
+
'test/integration/analyze-wal-checkpoint-failure.test.ts',
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
// Process spawning and CLI tests — exercise child_process with real
|
|
70
|
+
// process spawning, which behaves differently across platforms (shell
|
|
71
|
+
// quoting, path resolution, signal handling)
|
|
72
|
+
const SPAWN_CLI = [
|
|
73
|
+
'test/integration/cli-e2e.test.ts',
|
|
74
|
+
'test/integration/hooks-e2e.test.ts',
|
|
75
|
+
'test/integration/skills-e2e.test.ts',
|
|
76
|
+
'test/integration/server-http-startup.test.ts',
|
|
77
|
+
'test/integration/mcp/server-startup.test.ts',
|
|
78
|
+
'test/integration/analyze-heap-oom-e2e.test.ts',
|
|
79
|
+
'test/integration/group/group-cli.test.ts',
|
|
80
|
+
'test/integration/cli/tool-no-index-stderr.test.ts',
|
|
81
|
+
'test/integration/setup-skills.test.ts',
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// Worker threads tests — exercise real worker_threads which have
|
|
85
|
+
// platform-specific behavior (thread spawning, IPC, exit handling)
|
|
86
|
+
const WORKER_THREADS = [
|
|
87
|
+
'test/integration/worker-pool.test.ts',
|
|
88
|
+
'test/integration/parse-impl-quarantine-cache-skip.test.ts',
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
// Tree-sitter native addon smoke tests — verify that native grammars
|
|
92
|
+
// load correctly on each platform (binary compatibility, .node loading)
|
|
93
|
+
const NATIVE_ADDON_SMOKE = [
|
|
94
|
+
'test/integration/tree-sitter-languages.test.ts',
|
|
95
|
+
'test/integration/parsing.test.ts',
|
|
96
|
+
'test/integration/pipeline.test.ts',
|
|
97
|
+
'test/integration/pipeline-graph-golden.test.ts',
|
|
98
|
+
'test/unit/parser-loader.test.ts',
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
// Filesystem behavior tests — exercise operations that vary across
|
|
102
|
+
// platforms (CRLF, symlinks, permissions, temp dirs)
|
|
103
|
+
const FILESYSTEM = [
|
|
104
|
+
'test/integration/filesystem-walker.test.ts',
|
|
105
|
+
'test/integration/markdown-processor-crlf.test.ts',
|
|
106
|
+
'test/integration/ignore-and-skip-e2e.test.ts',
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
const ALL_CROSS_PLATFORM = [
|
|
110
|
+
...PLATFORM_LOGIC,
|
|
111
|
+
...LBUG_NATIVE,
|
|
112
|
+
...SPAWN_CLI,
|
|
113
|
+
...WORKER_THREADS,
|
|
114
|
+
...NATIVE_ADDON_SMOKE,
|
|
115
|
+
...FILESYSTEM,
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
// When invoked directly, print the file list for vitest consumption
|
|
119
|
+
if (process.argv[1]?.endsWith('cross-platform-tests.ts')) {
|
|
120
|
+
console.log(ALL_CROSS_PLATFORM.join('\n'));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export {
|
|
124
|
+
ALL_CROSS_PLATFORM,
|
|
125
|
+
PLATFORM_LOGIC,
|
|
126
|
+
LBUG_NATIVE,
|
|
127
|
+
SPAWN_CLI,
|
|
128
|
+
WORKER_THREADS,
|
|
129
|
+
NATIVE_ADDON_SMOKE,
|
|
130
|
+
FILESYSTEM,
|
|
131
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform test runner.
|
|
3
|
+
*
|
|
4
|
+
* Runs the platform-sensitive test subset defined in cross-platform-tests.ts
|
|
5
|
+
* via vitest. Used by `npm run test:cross-platform` and by the CI cross-
|
|
6
|
+
* platform matrix (ci-tests.yml).
|
|
7
|
+
*
|
|
8
|
+
* The main vitest.config.ts is used, so lbug-db project files get
|
|
9
|
+
* sequential execution and other safety constraints are preserved.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { execFileSync } from 'child_process';
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
import { ALL_CROSS_PLATFORM } from './cross-platform-tests.js';
|
|
17
|
+
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
20
|
+
|
|
21
|
+
// Verify all files exist
|
|
22
|
+
const missing = ALL_CROSS_PLATFORM.filter((f) => !fs.existsSync(path.resolve(ROOT, f)));
|
|
23
|
+
if (missing.length > 0) {
|
|
24
|
+
console.error(`Cross-platform test files not found (${missing.length}):`);
|
|
25
|
+
for (const f of missing) console.error(` ${f}`);
|
|
26
|
+
console.error('\nUpdate scripts/cross-platform-tests.ts if files were moved or removed.');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log(`Running ${ALL_CROSS_PLATFORM.length} platform-sensitive tests...\n`);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
execFileSync('npx', ['vitest', 'run', ...ALL_CROSS_PLATFORM], {
|
|
34
|
+
cwd: ROOT,
|
|
35
|
+
stdio: 'inherit',
|
|
36
|
+
timeout: 15 * 60 * 1000,
|
|
37
|
+
shell: true,
|
|
38
|
+
});
|
|
39
|
+
} catch (err: any) {
|
|
40
|
+
if (err.killed || err.signal) {
|
|
41
|
+
console.error('vitest timed out after 15 minutes');
|
|
42
|
+
}
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consolidated scope-resolution parity runner.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the per-language matrix in ci-scope-parity.yml with a single
|
|
5
|
+
* job that runs all migrated languages sequentially in one process. This
|
|
6
|
+
* eliminates 8× redundant checkout + npm ci + build cycles (the old
|
|
7
|
+
* workflow created a separate GitHub Actions job per language).
|
|
8
|
+
*
|
|
9
|
+
* For each language in MIGRATED_LANGUAGES:
|
|
10
|
+
* 1. Run its resolver test with REGISTRY_PRIMARY_<LANG>=0 (legacy DAG)
|
|
11
|
+
* 2. Run its resolver test with REGISTRY_PRIMARY_<LANG>=1 (registry-primary)
|
|
12
|
+
*
|
|
13
|
+
* Both modes must pass. Failures are collected and reported at the end
|
|
14
|
+
* so all regressions are visible in a single CI run (equivalent to the
|
|
15
|
+
* old workflow's fail-fast: false behavior).
|
|
16
|
+
*
|
|
17
|
+
* Vitest output streams to the console in real time (stdio: 'inherit')
|
|
18
|
+
* so CI logs show the actual test output directly. No per-invocation
|
|
19
|
+
* timeout — the CI job-level timeout (30 min) is the outer guard.
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* npx tsx scripts/run-parity.ts
|
|
23
|
+
* npx tsx scripts/run-parity.ts --language python # single language
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { execFileSync } from 'child_process';
|
|
27
|
+
import fs from 'fs';
|
|
28
|
+
import path from 'path';
|
|
29
|
+
import { fileURLToPath } from 'url';
|
|
30
|
+
import { MIGRATED_LANGUAGES } from '../src/core/ingestion/registry-primary-flag.js';
|
|
31
|
+
|
|
32
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
34
|
+
|
|
35
|
+
interface ParityFailure {
|
|
36
|
+
lang: string;
|
|
37
|
+
mode: 'legacy' | 'registry-primary';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function envVarName(slug: string): string {
|
|
41
|
+
return `REGISTRY_PRIMARY_${slug.toUpperCase().replace(/-/g, '_')}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function testFilePath(slug: string): string {
|
|
45
|
+
return `test/integration/resolvers/${slug}.test.ts`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function runVitest(testFile: string, env: Record<string, string>): boolean {
|
|
49
|
+
try {
|
|
50
|
+
execFileSync('npx', ['vitest', 'run', testFile], {
|
|
51
|
+
cwd: ROOT,
|
|
52
|
+
env: { ...process.env, ...env },
|
|
53
|
+
stdio: 'inherit',
|
|
54
|
+
shell: true,
|
|
55
|
+
});
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Parse CLI args
|
|
63
|
+
const args = process.argv.slice(2);
|
|
64
|
+
const langFlag = args.indexOf('--language');
|
|
65
|
+
const singleLang = langFlag >= 0 ? args[langFlag + 1] : undefined;
|
|
66
|
+
|
|
67
|
+
if (langFlag >= 0 && singleLang === undefined) {
|
|
68
|
+
console.error('--language requires a value');
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const languages = singleLang ? [singleLang] : [...MIGRATED_LANGUAGES].map(String);
|
|
73
|
+
|
|
74
|
+
// Verify test files exist before running
|
|
75
|
+
const missingFiles: string[] = [];
|
|
76
|
+
for (const lang of languages) {
|
|
77
|
+
const file = path.resolve(ROOT, testFilePath(lang));
|
|
78
|
+
try {
|
|
79
|
+
fs.accessSync(file);
|
|
80
|
+
} catch {
|
|
81
|
+
missingFiles.push(`${testFilePath(lang)} (${lang})`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (missingFiles.length > 0) {
|
|
86
|
+
console.error('Missing resolver test files:');
|
|
87
|
+
for (const f of missingFiles) console.error(` ${f}`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log(`Scope-resolution parity: ${languages.length} language(s)`);
|
|
92
|
+
console.log(`Languages: ${languages.join(', ')}\n`);
|
|
93
|
+
|
|
94
|
+
const failures: ParityFailure[] = [];
|
|
95
|
+
|
|
96
|
+
for (const lang of languages) {
|
|
97
|
+
const file = testFilePath(lang);
|
|
98
|
+
const envVar = envVarName(lang);
|
|
99
|
+
|
|
100
|
+
console.log(`\n── ${lang} — legacy DAG (${envVar}=0) ──`);
|
|
101
|
+
if (!runVitest(file, { [envVar]: '0' })) {
|
|
102
|
+
failures.push({ lang, mode: 'legacy' });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log(`\n── ${lang} — registry-primary (${envVar}=1) ──`);
|
|
106
|
+
if (!runVitest(file, { [envVar]: '1' })) {
|
|
107
|
+
failures.push({ lang, mode: 'registry-primary' });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Summary
|
|
112
|
+
const total = languages.length * 2;
|
|
113
|
+
const passed = total - failures.length;
|
|
114
|
+
|
|
115
|
+
console.log('\n═══════════════════════════════════════');
|
|
116
|
+
console.log('PARITY SUMMARY');
|
|
117
|
+
console.log('═══════════════════════════════════════');
|
|
118
|
+
console.log(`Passed: ${passed}/${total}`);
|
|
119
|
+
|
|
120
|
+
if (failures.length > 0) {
|
|
121
|
+
console.log(`\nFAILURES (${failures.length}):`);
|
|
122
|
+
for (const f of failures) {
|
|
123
|
+
console.log(` ✗ ${f.lang} [${f.mode}]`);
|
|
124
|
+
}
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log('\nAll parity checks passed.');
|