gitnexus 1.6.6-rc.51 → 1.6.6-rc.53
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/finalize-orchestrator.js +1 -0
- 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/languages/php/namespace-siblings.d.ts +25 -0
- package/dist/core/ingestion/languages/php/namespace-siblings.js +94 -38
- package/dist/core/ingestion/model/scope-resolution-indexes.d.ts +6 -0
- 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/dist/core/ingestion/scope-resolution/pipeline/phase.js +9 -0
- package/dist/core/ingestion/scope-resolution/scope/walkers.js +26 -10
- package/package.json +1 -1
|
@@ -94,6 +94,7 @@ export function finalizeScopeModel(parsedFiles, options = {}) {
|
|
|
94
94
|
// AFTER `finalizeScopeModel` returns, before `resolveReferenceSites`
|
|
95
95
|
// consumes the bundle. Most languages leave it empty.
|
|
96
96
|
bindingAugmentations: new Map(),
|
|
97
|
+
workspaceFqnBindings: new Map(),
|
|
97
98
|
referenceSites: Object.freeze([...allReferenceSites]),
|
|
98
99
|
sccs: finalizeOut.sccs,
|
|
99
100
|
stats: finalizeOut.stats,
|
|
@@ -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
|
+
}
|
|
@@ -26,6 +26,30 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import type { ParsedFile } from '../../../../_shared/index.js';
|
|
28
28
|
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
|
29
|
+
interface PhpFileStructure {
|
|
30
|
+
/** The declared namespace (backslash-separated), or '' for global namespace. */
|
|
31
|
+
readonly namespace: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Extract a PHP namespace declaration from raw source without tree-sitter.
|
|
35
|
+
*
|
|
36
|
+
* Single-pass line scanner that skips heredoc/nowdoc bodies, block
|
|
37
|
+
* comments, and single-line comments before matching. This avoids the
|
|
38
|
+
* false positives that a multiline regex produces when `namespace` appears
|
|
39
|
+
* inside a heredoc, nowdoc, string, or comment.
|
|
40
|
+
*/
|
|
41
|
+
export declare function extractNamespaceViaScanner(content: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Extract the declared namespace from a PHP file's source.
|
|
44
|
+
* Uses the cached AST tree when available to avoid re-parsing.
|
|
45
|
+
*
|
|
46
|
+
* When no cached tree is available (worker-parsed files can't transfer
|
|
47
|
+
* native Tree objects across MessageChannels), uses a line scanner
|
|
48
|
+
* instead of re-parsing every file with tree-sitter. For 16K+ PHP files
|
|
49
|
+
* this eliminates ~16K tree-sitter re-parses during the namespace-siblings
|
|
50
|
+
* pass. See: https://github.com/abhigyanpatwari/GitNexus/issues/1741
|
|
51
|
+
*/
|
|
52
|
+
export declare function extractPhpFileStructure(content: string, cachedTree: unknown): PhpFileStructure;
|
|
29
53
|
export interface PhpSiblingInputs {
|
|
30
54
|
readonly fileContents: ReadonlyMap<string, string>;
|
|
31
55
|
readonly treeCache?: {
|
|
@@ -49,3 +73,4 @@ export declare function getPhpNamespaceForFile(filePath: string): string;
|
|
|
49
73
|
* explicit `use` imports (`origin: 'import'`) and local declarations.
|
|
50
74
|
*/
|
|
51
75
|
export declare function populatePhpNamespaceSiblings(parsedFiles: readonly ParsedFile[], indexes: ScopeResolutionIndexes, inputs: PhpSiblingInputs): void;
|
|
76
|
+
export {};
|
|
@@ -24,22 +24,85 @@
|
|
|
24
24
|
* to extract namespace declarations — same AST that `extractParsedFile`
|
|
25
25
|
* already parsed, reused via `treeCache` to avoid double-parsing.
|
|
26
26
|
*/
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
const NAMESPACE_RE = /^\s*namespace\s+([\w\\]+)\s*[;{]/i;
|
|
28
|
+
const HEREDOC_START_RE = /<<<\s*['"]?(\w+)['"]?\s*$/;
|
|
29
|
+
/**
|
|
30
|
+
* Extract a PHP namespace declaration from raw source without tree-sitter.
|
|
31
|
+
*
|
|
32
|
+
* Single-pass line scanner that skips heredoc/nowdoc bodies, block
|
|
33
|
+
* comments, and single-line comments before matching. This avoids the
|
|
34
|
+
* false positives that a multiline regex produces when `namespace` appears
|
|
35
|
+
* inside a heredoc, nowdoc, string, or comment.
|
|
36
|
+
*/
|
|
37
|
+
export function extractNamespaceViaScanner(content) {
|
|
38
|
+
const lines = content.split('\n');
|
|
39
|
+
let inBlockComment = false;
|
|
40
|
+
let heredocDelimiter = null;
|
|
41
|
+
for (const raw of lines) {
|
|
42
|
+
if (heredocDelimiter !== null) {
|
|
43
|
+
const trimmed = raw.trim();
|
|
44
|
+
if (trimmed === heredocDelimiter + ';' || trimmed === heredocDelimiter) {
|
|
45
|
+
heredocDelimiter = null;
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (inBlockComment) {
|
|
50
|
+
if (raw.includes('*/')) {
|
|
51
|
+
inBlockComment = false;
|
|
52
|
+
}
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
let line = raw;
|
|
56
|
+
const blockStart = line.indexOf('/*');
|
|
57
|
+
if (blockStart >= 0) {
|
|
58
|
+
const blockEnd = line.indexOf('*/', blockStart + 2);
|
|
59
|
+
if (blockEnd >= 0) {
|
|
60
|
+
line = line.slice(0, blockStart) + line.slice(blockEnd + 2);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
line = line.slice(0, blockStart);
|
|
64
|
+
inBlockComment = true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const slashIdx = line.indexOf('//');
|
|
68
|
+
const hashIdx = line.indexOf('#');
|
|
69
|
+
if (slashIdx >= 0 && (hashIdx < 0 || slashIdx < hashIdx)) {
|
|
70
|
+
line = line.slice(0, slashIdx);
|
|
71
|
+
}
|
|
72
|
+
else if (hashIdx >= 0) {
|
|
73
|
+
line = line.slice(0, hashIdx);
|
|
74
|
+
}
|
|
75
|
+
const heredocMatch = raw.match(HEREDOC_START_RE);
|
|
76
|
+
if (heredocMatch) {
|
|
77
|
+
heredocDelimiter = heredocMatch[1];
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const stripped = line.replace(/<\?php/gi, '').replace(/declare\s*\([^)]*\)\s*;?/gi, '');
|
|
81
|
+
const nsMatch = stripped.match(NAMESPACE_RE);
|
|
82
|
+
if (nsMatch) {
|
|
83
|
+
return nsMatch[1];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return '';
|
|
87
|
+
}
|
|
30
88
|
/**
|
|
31
89
|
* Extract the declared namespace from a PHP file's source.
|
|
32
90
|
* Uses the cached AST tree when available to avoid re-parsing.
|
|
91
|
+
*
|
|
92
|
+
* When no cached tree is available (worker-parsed files can't transfer
|
|
93
|
+
* native Tree objects across MessageChannels), uses a line scanner
|
|
94
|
+
* instead of re-parsing every file with tree-sitter. For 16K+ PHP files
|
|
95
|
+
* this eliminates ~16K tree-sitter re-parses during the namespace-siblings
|
|
96
|
+
* pass. See: https://github.com/abhigyanpatwari/GitNexus/issues/1741
|
|
33
97
|
*/
|
|
34
|
-
function extractPhpFileStructure(content, cachedTree) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
});
|
|
98
|
+
export function extractPhpFileStructure(content, cachedTree) {
|
|
99
|
+
if (!cachedTree) {
|
|
100
|
+
return { namespace: extractNamespaceViaScanner(content) };
|
|
101
|
+
}
|
|
39
102
|
// Walk top-level nodes looking for namespace_definition.
|
|
40
103
|
// PHP files have at most one namespace declaration (PSR-4 convention).
|
|
41
104
|
// `namespace_definition` has a `name:` field of type `namespace_name`.
|
|
42
|
-
const root =
|
|
105
|
+
const root = cachedTree.rootNode;
|
|
43
106
|
for (let i = 0; i < root.namedChildCount; i++) {
|
|
44
107
|
const child = root.namedChild(i);
|
|
45
108
|
if (child === null)
|
|
@@ -194,38 +257,31 @@ export function populatePhpNamespaceSiblings(parsedFiles, indexes, inputs) {
|
|
|
194
257
|
}
|
|
195
258
|
}
|
|
196
259
|
}
|
|
197
|
-
// Step 3b:
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
// regardless of which simple-name `User` the caller's `use` imports
|
|
202
|
-
// shadowed. The shared `findClassBindingInScope` scope-chain walk
|
|
203
|
-
// consumes these augmentations via `lookupBindingsAt`, so adding the
|
|
204
|
-
// qualified key on every file's module scope routes FQN-receivers to
|
|
205
|
-
// the right def. Codex PR #1497 review, finding 1.
|
|
260
|
+
// Step 3b: Register FQN bindings in a workspace-level map instead of
|
|
261
|
+
// per-scope augmentations. PHP `\App\Models\User` and `App\Models\User`
|
|
262
|
+
// must resolve regardless of which file the lookup originates from.
|
|
263
|
+
// `lookupBindingsAt` consults `workspaceFqnBindings` as a third source.
|
|
206
264
|
//
|
|
207
|
-
// Cost: O(
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
for (const
|
|
211
|
-
|
|
212
|
-
if (moduleScope === undefined)
|
|
265
|
+
// Cost: O(class-like defs) entries — NOT O(files × classDefs). For 16K
|
|
266
|
+
// PHP files with 5K classes, this is 5K entries instead of 80M.
|
|
267
|
+
const fqnMap = indexes.workspaceFqnBindings;
|
|
268
|
+
for (const [ns, bucket] of buckets) {
|
|
269
|
+
if (ns === '')
|
|
213
270
|
continue;
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const arr = getAugmentationBucket(augmentations, moduleScopeId, fqn);
|
|
225
|
-
if (arr.some((b) => b.def.nodeId === def.nodeId))
|
|
226
|
-
continue;
|
|
227
|
-
arr.push({ def, origin: 'namespace' });
|
|
271
|
+
for (const def of bucket.classDefs) {
|
|
272
|
+
const q = def.qualifiedName ?? '';
|
|
273
|
+
const simpleName = q.includes('\\') ? q.slice(q.lastIndexOf('\\') + 1) : q;
|
|
274
|
+
if (simpleName === '')
|
|
275
|
+
continue;
|
|
276
|
+
const fqn = `${ns}\\${simpleName}`;
|
|
277
|
+
let arr = fqnMap.get(fqn);
|
|
278
|
+
if (arr === undefined) {
|
|
279
|
+
arr = [];
|
|
280
|
+
fqnMap.set(fqn, arr);
|
|
228
281
|
}
|
|
282
|
+
if (arr.some((b) => b.def.nodeId === def.nodeId))
|
|
283
|
+
continue;
|
|
284
|
+
arr.push({ def, origin: 'namespace' });
|
|
229
285
|
}
|
|
230
286
|
}
|
|
231
287
|
// Step 4: Mirror return-type bindings from same-namespace sibling files.
|
|
@@ -63,6 +63,12 @@ export interface ScopeResolutionIndexes {
|
|
|
63
63
|
* are returned first and win duplicate `def.nodeId` metadata, with
|
|
64
64
|
* unique augmentations appended after. See I8. */
|
|
65
65
|
readonly bindingAugmentations: ReadonlyMap<ScopeId, ReadonlyMap<string, readonly BindingRef[]>>;
|
|
66
|
+
/** Workspace-level FQN binding lookup. Populated by PHP namespace-
|
|
67
|
+
* siblings Step 3b as a shared map instead of per-scope duplication.
|
|
68
|
+
* Consulted by `lookupBindingsAt` as a third source after finalized
|
|
69
|
+
* and per-scope augmented bindings. Keys are backslash-separated FQNs
|
|
70
|
+
* (e.g. `App\Models\User`). */
|
|
71
|
+
readonly workspaceFqnBindings: ReadonlyMap<string, readonly BindingRef[]>;
|
|
66
72
|
/** Pre-resolution usage facts; consumed by the resolution phase. */
|
|
67
73
|
readonly referenceSites: readonly ReferenceSite[];
|
|
68
74
|
/** SCC condensation of the file-level import graph — callers that want
|
|
@@ -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,
|
|
@@ -124,6 +124,15 @@ export const scopeResolutionPhase = {
|
|
|
124
124
|
}
|
|
125
125
|
},
|
|
126
126
|
}, provider);
|
|
127
|
+
// Release file contents and pre-extracted entries after each language
|
|
128
|
+
// to reduce memory pressure. For large codebases (16K+ PHP files),
|
|
129
|
+
// holding all source code simultaneously with scope trees causes OOM.
|
|
130
|
+
// See: https://github.com/abhigyanpatwari/GitNexus/issues/1741
|
|
131
|
+
files.length = 0;
|
|
132
|
+
contents.clear();
|
|
133
|
+
for (const fp of filePaths) {
|
|
134
|
+
preExtractedByPath.delete(fp);
|
|
135
|
+
}
|
|
127
136
|
anyRan = true;
|
|
128
137
|
totalFiles += stats.filesProcessed;
|
|
129
138
|
totalImports += stats.importsEmitted;
|
|
@@ -44,24 +44,40 @@ const EMPTY_BINDINGS = Object.freeze([]);
|
|
|
44
44
|
export function lookupBindingsAt(scopeId, name, scopes) {
|
|
45
45
|
const finalized = scopes.bindings.get(scopeId)?.get(name);
|
|
46
46
|
const augmented = scopes.bindingAugmentations.get(scopeId)?.get(name);
|
|
47
|
+
const workspace = scopes.workspaceFqnBindings?.get(name);
|
|
47
48
|
const fLen = finalized?.length ?? 0;
|
|
48
49
|
const aLen = augmented?.length ?? 0;
|
|
49
|
-
|
|
50
|
+
const wLen = workspace?.length ?? 0;
|
|
51
|
+
if (fLen === 0 && aLen === 0 && wLen === 0)
|
|
50
52
|
return EMPTY_BINDINGS;
|
|
51
|
-
if (aLen === 0)
|
|
53
|
+
if (aLen === 0 && wLen === 0)
|
|
52
54
|
return finalized;
|
|
53
|
-
if (fLen === 0)
|
|
55
|
+
if (fLen === 0 && wLen === 0)
|
|
54
56
|
return augmented;
|
|
57
|
+
if (fLen === 0 && aLen === 0)
|
|
58
|
+
return workspace;
|
|
55
59
|
const seen = new Set();
|
|
56
60
|
const out = [];
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
if (fLen > 0) {
|
|
62
|
+
for (const r of finalized) {
|
|
63
|
+
seen.add(r.def.nodeId);
|
|
64
|
+
out.push(r);
|
|
65
|
+
}
|
|
60
66
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
if (aLen > 0) {
|
|
68
|
+
for (const r of augmented) {
|
|
69
|
+
if (seen.has(r.def.nodeId))
|
|
70
|
+
continue;
|
|
71
|
+
seen.add(r.def.nodeId);
|
|
72
|
+
out.push(r);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (wLen > 0) {
|
|
76
|
+
for (const r of workspace) {
|
|
77
|
+
if (seen.has(r.def.nodeId))
|
|
78
|
+
continue;
|
|
79
|
+
out.push(r);
|
|
80
|
+
}
|
|
65
81
|
}
|
|
66
82
|
return out;
|
|
67
83
|
}
|
package/package.json
CHANGED