mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-06 02:49:56 +00:00
feat(java): add Java to MIGRATED_LANGUAGES with 100% scope-resolution parity (#1805)
* feat(java): add Java to MIGRATED_LANGUAGES with 100% scope-resolution parity Route Java through the scope-resolution pipeline instead of the legacy single-threaded call processor, fixing the analyze hang on large Java codebases (issue #1741). Changes: - Add Java to MIGRATED_LANGUAGES (registry-primary-flag.ts) - Add tree-sitter queries for var type inference (call-result, alias, field-access, enhanced-for), instanceof/switch pattern bindings, and method references (User::getName, this::save, User::new) - Fix importedName to use simple class name instead of FQN so finalize binding materialization matches correctly - Implement buildJavaMro with IMPLEMENTS edge transitive closure for interface default method resolution - Implement populateJavaPackageSiblings for same-package implicit class visibility across files - Implement cross-file return-type mirroring from imported class files via populateRangeBindings hook - Add var type binding post-processing in captures.ts to resolve call-result and alias chains from same-file return types - Add variable-aware argument type inference for overload resolution - Fix pickConstructorOrClass to walk child scopes for Constructor defs (scope-resolution places them in Function scopes) - Remove over-aggressive field_access suppression in shouldEmitReadMember so ACCESSES edges emit for field steps in method chains - Enable collapseMemberCallsByCallerTarget for legacy parity - Update unit tests to use Ruby as unmigrated language example Parity: 178/178 integration tests pass in both registry-primary and legacy modes. * fix(java): address code review findings for scope-resolution migration - pickConstructorOrClass: skip inner Class scopes when walking children for Constructor defs (prevents resolving to wrong constructor in nested-class scenarios) - populateJavaCrossFileReturnTypes: filter out parameter-annotation bindings from class-scope mirroring to prevent foreign parameter types from shadowing local variables - resolveVarTypeBindings: detect ambiguous names (overloaded methods with different return types, same-named variables across scopes) and skip resolution rather than last-write-wins - sharedPrefixLength renamed to sharedSegmentCount: segment-based directory proximity for deterministic sort ordering - Add MAX_PACKAGE_FILES cap (500) to skip O(N^2) package-siblings injection for pathologically large packages * perf(java): optimize hot paths in scope-resolution migration - Replace O(D^2) list.some() dedup with O(1) Set lookup in populateJavaPackageSiblings binding injection - Replace queue.shift() O(N) with index-based O(1) iteration in closeInterfaces BFS traversal - Cache sharedSegmentCount results per file in sort comparator to avoid redundant path splitting * perf(ingestion): skip deferred accumulation for registry-primary languages The legacy call/import/heritage processing path accumulates extracted data from ALL files during the parse phase, then skips registry-primary files one-by-one during processing. For a 25K-file Java codebase this wastes ~150 MB holding calls that are never consumed. Gate the accumulation with a per-chunk file-path cache: calls, imports, heritage, constructor bindings, and assignments for registry-primary languages (Java, Python, TypeScript, Go, C#, C, C++, PHP, JavaScript, Kotlin) are no longer pushed into the deferred arrays. The scope- resolution pipeline handles these languages independently. Verified: 2258/2258 resolver integration tests pass across all languages. * fix(java): address Codex adversarial review findings - Cross-file return binding: detect ambiguous method names across imported classes (two classes with same-named methods but different return types) and delete the binding rather than first-wins - Package-siblings: only inject top-level classes (parent is Module scope) to prevent nested/inner classes from leaking to package scope - Add diagnostic log when MAX_PACKAGE_FILES cap fires so operators know same-package visibility was disabled for a large package * fix(test): force REGISTRY_PRIMARY_JAVA=false in legacy call-processor unit tests Three call-processor test suites use .java file paths to exercise legacy DAG features (MRO fast path, interface dispatch, class lookup fallback). Now that Java is in MIGRATED_LANGUAGES, the call-processor skips Java files. Force the flag off in beforeEach/afterEach so the legacy path runs, matching the existing Python pattern in the same file. --------- Co-authored-by: Test <test@example.com>
This commit is contained in:
parent
ac9a2ee12f
commit
66f9ec8eff
10 changed files with 613 additions and 84 deletions
|
|
@ -38,10 +38,6 @@ function shouldEmitReadMember(memberNode: SyntaxNode): boolean {
|
|||
if (parent === null) return true;
|
||||
|
||||
switch (parent.type) {
|
||||
case 'method_invocation':
|
||||
// Don't emit read.member when the field_access is the object of a method_invocation
|
||||
// (the method call already handles this relationship)
|
||||
return parent.childForFieldName('object')?.id !== memberNode.id;
|
||||
case 'assignment_expression':
|
||||
return parent.childForFieldName('left')?.id !== memberNode.id;
|
||||
default:
|
||||
|
|
@ -185,13 +181,137 @@ export function emitJavaScopeCaptures(
|
|||
callNode,
|
||||
JSON.stringify(argTypes),
|
||||
);
|
||||
|
||||
const argNames = args.map((a) => (a!.type === 'identifier' ? a!.text : ''));
|
||||
if (argNames.some((n) => n !== '')) {
|
||||
grouped['@reference.arg-names'] = syntheticCapture(
|
||||
'@reference.arg-names',
|
||||
callNode,
|
||||
JSON.stringify(argNames),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out.push(grouped);
|
||||
}
|
||||
|
||||
return out;
|
||||
return resolveVarTypeBindings(out);
|
||||
}
|
||||
|
||||
function resolveVarTypeBindings(matches: CaptureMatch[]): CaptureMatch[] {
|
||||
const returnTypes = new Map<string, string>();
|
||||
const varTypes = new Map<string, string>();
|
||||
const ambiguousReturns = new Set<string>();
|
||||
const ambiguousVars = new Set<string>();
|
||||
|
||||
for (const m of matches) {
|
||||
if (
|
||||
m['@type-binding.return'] !== undefined &&
|
||||
m['@type-binding.type'] !== undefined &&
|
||||
m['@type-binding.name'] !== undefined
|
||||
) {
|
||||
const name = m['@type-binding.name'].text;
|
||||
const type = m['@type-binding.type'].text;
|
||||
const existing = returnTypes.get(name);
|
||||
if (existing !== undefined && existing !== type) {
|
||||
ambiguousReturns.add(name);
|
||||
returnTypes.delete(name);
|
||||
} else if (!ambiguousReturns.has(name)) {
|
||||
returnTypes.set(name, type);
|
||||
}
|
||||
}
|
||||
if (
|
||||
m['@type-binding.annotation'] !== undefined &&
|
||||
m['@type-binding.type'] !== undefined &&
|
||||
m['@type-binding.name'] !== undefined
|
||||
) {
|
||||
const name = m['@type-binding.name'].text;
|
||||
const t = m['@type-binding.type'].text;
|
||||
if (t !== 'var') {
|
||||
const existing = varTypes.get(name);
|
||||
if (existing !== undefined && existing !== t) {
|
||||
ambiguousVars.add(name);
|
||||
varTypes.delete(name);
|
||||
} else if (!ambiguousVars.has(name)) {
|
||||
varTypes.set(name, t);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
m['@type-binding.constructor'] !== undefined &&
|
||||
m['@type-binding.type'] !== undefined &&
|
||||
m['@type-binding.name'] !== undefined
|
||||
) {
|
||||
const name = m['@type-binding.name'].text;
|
||||
const type = m['@type-binding.type'].text;
|
||||
const existing = varTypes.get(name);
|
||||
if (existing !== undefined && existing !== type) {
|
||||
ambiguousVars.add(name);
|
||||
varTypes.delete(name);
|
||||
} else if (!ambiguousVars.has(name)) {
|
||||
varTypes.set(name, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resolved: CaptureMatch[] = [];
|
||||
for (const m of matches) {
|
||||
if (m['@type-binding.call-result'] !== undefined && m['@type-binding.type'] !== undefined) {
|
||||
const methodName = m['@type-binding.type'].text;
|
||||
const resolvedType = returnTypes.get(methodName);
|
||||
if (resolvedType !== undefined) {
|
||||
const patched: Record<string, Capture> = { ...m };
|
||||
patched['@type-binding.type'] = { ...m['@type-binding.type']!, text: resolvedType };
|
||||
patched['@type-binding.annotation'] = m['@type-binding.call-result']!;
|
||||
delete patched['@type-binding.call-result'];
|
||||
resolved.push(patched);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (m['@type-binding.alias'] !== undefined && m['@type-binding.type'] !== undefined) {
|
||||
const sourceName = m['@type-binding.type'].text;
|
||||
const resolvedType = varTypes.get(sourceName);
|
||||
if (resolvedType !== undefined) {
|
||||
const patched: Record<string, Capture> = { ...m };
|
||||
patched['@type-binding.type'] = { ...m['@type-binding.type']!, text: resolvedType };
|
||||
patched['@type-binding.annotation'] = m['@type-binding.alias']!;
|
||||
delete patched['@type-binding.alias'];
|
||||
resolved.push(patched);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (m['@reference.arg-names'] !== undefined && m['@reference.parameter-types'] !== undefined) {
|
||||
try {
|
||||
const types: string[] = JSON.parse(m['@reference.parameter-types'].text);
|
||||
const names: string[] = JSON.parse(m['@reference.arg-names'].text);
|
||||
let patched = false;
|
||||
for (let i = 0; i < types.length; i++) {
|
||||
if (types[i] === '' && names[i] !== undefined && names[i] !== '') {
|
||||
const rt = varTypes.get(names[i]!);
|
||||
if (rt !== undefined) {
|
||||
types[i] = rt;
|
||||
patched = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (patched) {
|
||||
const patchedMatch: Record<string, Capture> = { ...m };
|
||||
patchedMatch['@reference.parameter-types'] = {
|
||||
...m['@reference.parameter-types']!,
|
||||
text: JSON.stringify(types),
|
||||
};
|
||||
delete patchedMatch['@reference.arg-names'];
|
||||
resolved.push(patchedMatch);
|
||||
continue;
|
||||
}
|
||||
} catch {
|
||||
// pass through
|
||||
}
|
||||
}
|
||||
resolved.push(m);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
type SyntaxNode = ReturnType<ReturnType<typeof getJavaParser>['parse']>['rootNode'];
|
||||
|
|
|
|||
|
|
@ -24,10 +24,11 @@ export function interpretJavaImport(captures: CaptureMatch): ParsedImport | null
|
|||
switch (kind) {
|
||||
case 'named': {
|
||||
// `import com.example.User;`
|
||||
const simpleName = sourceCap.text.split('.').pop() ?? sourceCap.text;
|
||||
return {
|
||||
kind: 'named',
|
||||
localName: nameCap?.text ?? sourceCap.text.split('.').pop() ?? sourceCap.text,
|
||||
importedName: sourceCap.text,
|
||||
localName: nameCap?.text ?? simpleName,
|
||||
importedName: simpleName,
|
||||
targetRaw: sourceCap.text,
|
||||
};
|
||||
}
|
||||
|
|
@ -40,17 +41,14 @@ export function interpretJavaImport(captures: CaptureMatch): ParsedImport | null
|
|||
}
|
||||
case 'static': {
|
||||
// `import static com.example.Utils.format;`
|
||||
// The source contains the full path including the member name
|
||||
// (e.g. `com.example.Utils.format`). For file resolution we need
|
||||
// the class path (`com.example.Utils`), so strip the final member
|
||||
// segment. The local binding name is the member itself.
|
||||
const fullSource = sourceCap.text;
|
||||
const lastDot = fullSource.lastIndexOf('.');
|
||||
const memberName = lastDot >= 0 ? fullSource.slice(lastDot + 1) : fullSource;
|
||||
const classPath = lastDot >= 0 ? fullSource.slice(0, lastDot) : fullSource;
|
||||
return {
|
||||
kind: 'named',
|
||||
localName: nameCap?.text ?? (lastDot >= 0 ? fullSource.slice(lastDot + 1) : fullSource),
|
||||
importedName: fullSource,
|
||||
localName: nameCap?.text ?? memberName,
|
||||
importedName: memberName,
|
||||
targetRaw: classPath,
|
||||
};
|
||||
}
|
||||
|
|
@ -89,6 +87,9 @@ export function interpretJavaTypeBinding(captures: CaptureMatch): ParsedTypeBind
|
|||
let source: TypeRef['source'] = 'parameter-annotation';
|
||||
if (captures['@type-binding.self'] !== undefined) source = 'self';
|
||||
else if (captures['@type-binding.constructor'] !== undefined) source = 'constructor-inferred';
|
||||
else if (captures['@type-binding.pattern'] !== undefined) source = 'annotation';
|
||||
else if (captures['@type-binding.call-result'] !== undefined) source = 'annotation';
|
||||
else if (captures['@type-binding.alias'] !== undefined) source = 'annotation';
|
||||
else if (captures['@type-binding.annotation'] !== undefined) source = 'annotation';
|
||||
else if (captures['@type-binding.return'] !== undefined) source = 'return-annotation';
|
||||
|
||||
|
|
|
|||
156
gitnexus/src/core/ingestion/languages/java/package-siblings.ts
Normal file
156
gitnexus/src/core/ingestion/languages/java/package-siblings.ts
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
/**
|
||||
* Java package-scope implicit visibility.
|
||||
*
|
||||
* Classes in the same Java package see each other without explicit
|
||||
* `import` statements. This hook groups files by `package` declaration,
|
||||
* then injects cross-file class defs into each file's module-scope
|
||||
* `bindingAugmentations` and mirrors type-bindings across same-package
|
||||
* files — the Java equivalent of C#'s `populateNamespaceSiblings`.
|
||||
*/
|
||||
|
||||
import type { BindingRef, ParsedFile, ScopeId, TypeRef } from 'gitnexus-shared';
|
||||
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
||||
import { isClassLike } from '../../scope-resolution/scope/walkers.js';
|
||||
import { getJavaParser } from './query.js';
|
||||
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
||||
import { logger } from '../../../logger.js';
|
||||
|
||||
function extractPackageName(content: string, cachedTree?: unknown): string {
|
||||
const tree =
|
||||
(cachedTree as ReturnType<ReturnType<typeof getJavaParser>['parse']> | undefined) ??
|
||||
parseSourceSafe(getJavaParser(), content);
|
||||
for (const child of tree.rootNode.namedChildren) {
|
||||
if (child.type === 'package_declaration') {
|
||||
const scoped = child.namedChildren.find(
|
||||
(c) => c.type === 'scoped_identifier' || c.type === 'identifier',
|
||||
);
|
||||
return scoped?.text ?? '';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
interface PackageBucket {
|
||||
readonly parsed: ParsedFile[];
|
||||
readonly moduleScopes: { filePath: string; scope: ParsedFile['scopes'][number] }[];
|
||||
}
|
||||
|
||||
export function populateJavaPackageSiblings(
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
indexes: ScopeResolutionIndexes,
|
||||
ctx: {
|
||||
readonly fileContents: ReadonlyMap<string, string>;
|
||||
readonly treeCache?: { get(filePath: string): unknown };
|
||||
},
|
||||
): void {
|
||||
const buckets = new Map<string, PackageBucket>();
|
||||
|
||||
for (const parsed of parsedFiles) {
|
||||
const content = ctx.fileContents.get(parsed.filePath);
|
||||
if (content === undefined) continue;
|
||||
const pkg = extractPackageName(content, ctx.treeCache?.get(parsed.filePath));
|
||||
let bucket = buckets.get(pkg);
|
||||
if (bucket === undefined) {
|
||||
bucket = { parsed: [], moduleScopes: [] };
|
||||
buckets.set(pkg, bucket);
|
||||
}
|
||||
bucket.parsed.push(parsed);
|
||||
const ms = parsed.scopes.find((s) => s.kind === 'Module');
|
||||
if (ms !== undefined) {
|
||||
bucket.moduleScopes.push({ filePath: parsed.filePath, scope: ms });
|
||||
}
|
||||
}
|
||||
|
||||
const augmentations = indexes.bindingAugmentations as Map<ScopeId, Map<string, BindingRef[]>>;
|
||||
|
||||
const MAX_PACKAGE_FILES = 500;
|
||||
|
||||
for (const bucket of buckets.values()) {
|
||||
if (bucket.moduleScopes.length < 2) continue;
|
||||
if (bucket.moduleScopes.length > MAX_PACKAGE_FILES) {
|
||||
logger.warn(
|
||||
`[java-package-siblings] skipping package with ${bucket.moduleScopes.length} files (cap=${MAX_PACKAGE_FILES}); same-package implicit visibility disabled for this package`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const classDefs: { def: BindingRef['def']; filePath: string }[] = [];
|
||||
for (const parsed of bucket.parsed) {
|
||||
const moduleScope = parsed.scopes.find((s) => s.kind === 'Module');
|
||||
const moduleScopeId = moduleScope?.id;
|
||||
for (const scope of parsed.scopes) {
|
||||
if (scope.kind !== 'Class') continue;
|
||||
if (scope.parent !== moduleScopeId) continue;
|
||||
for (const def of scope.ownedDefs) {
|
||||
if (isClassLike(def.type)) {
|
||||
classDefs.push({ def, filePath: parsed.filePath });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const { filePath, scope } of bucket.moduleScopes) {
|
||||
let scopeAug = augmentations.get(scope.id);
|
||||
if (scopeAug === undefined) {
|
||||
scopeAug = new Map();
|
||||
augmentations.set(scope.id, scopeAug);
|
||||
}
|
||||
|
||||
const candidates = classDefs.filter((d) => d.filePath !== filePath);
|
||||
const proximityCache = new Map<string, number>();
|
||||
for (const c of candidates) {
|
||||
if (!proximityCache.has(c.filePath)) {
|
||||
proximityCache.set(c.filePath, sharedSegmentCount(c.filePath, filePath));
|
||||
}
|
||||
}
|
||||
const sorted = candidates.sort(
|
||||
(a, b) => (proximityCache.get(b.filePath) ?? 0) - (proximityCache.get(a.filePath) ?? 0),
|
||||
);
|
||||
|
||||
const injectedIds = new Set<string>();
|
||||
for (const { def } of sorted) {
|
||||
if (injectedIds.has(def.nodeId)) continue;
|
||||
const qn = def.qualifiedName;
|
||||
if (qn === undefined) continue;
|
||||
injectedIds.add(def.nodeId);
|
||||
const simpleName = qn.includes('.') ? qn.slice(qn.lastIndexOf('.') + 1) : qn;
|
||||
let list = scopeAug.get(simpleName);
|
||||
if (list === undefined) {
|
||||
list = [];
|
||||
scopeAug.set(simpleName, list);
|
||||
}
|
||||
list.push({ def, origin: 'namespace' });
|
||||
}
|
||||
|
||||
const tb = scope.typeBindings as Map<string, TypeRef>;
|
||||
for (const sibling of bucket.moduleScopes) {
|
||||
if (sibling.filePath === filePath) continue;
|
||||
for (const [name, ref] of sibling.scope.typeBindings) {
|
||||
if (tb.has(name)) continue;
|
||||
tb.set(name, ref);
|
||||
}
|
||||
}
|
||||
|
||||
for (const sibParsed of bucket.parsed) {
|
||||
if (sibParsed.filePath === filePath) continue;
|
||||
for (const sibScope of sibParsed.scopes) {
|
||||
if (sibScope.kind !== 'Class') continue;
|
||||
for (const [name, ref] of sibScope.typeBindings) {
|
||||
if (ref.source === 'self') continue;
|
||||
if (tb.has(name)) continue;
|
||||
tb.set(name, ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sharedSegmentCount(a: string, b: string): number {
|
||||
const sa = a.replace(/\\/g, '/').split('/');
|
||||
const sb = b.replace(/\\/g, '/').split('/');
|
||||
let i = 0;
|
||||
while (i < sa.length && i < sb.length && sa[i] === sb[i]) i++;
|
||||
return i;
|
||||
}
|
||||
|
|
@ -102,6 +102,47 @@ const JAVA_SCOPE_QUERY = `
|
|||
declarator: (variable_declarator
|
||||
name: (identifier) @type-binding.name)) @type-binding.annotation
|
||||
|
||||
;; Type bindings — var u = svc.getUser(); (Java 10+ call-result inference)
|
||||
(local_variable_declaration
|
||||
type: (type_identifier) @_var_type
|
||||
(#eq? @_var_type "var")
|
||||
declarator: (variable_declarator
|
||||
name: (identifier) @type-binding.name
|
||||
value: (method_invocation
|
||||
name: (identifier) @type-binding.type))) @type-binding.call-result
|
||||
|
||||
;; Type bindings — var alias = u; (Java 10+ alias inference)
|
||||
(local_variable_declaration
|
||||
type: (type_identifier) @_var_type
|
||||
(#eq? @_var_type "var")
|
||||
declarator: (variable_declarator
|
||||
name: (identifier) @type-binding.name
|
||||
value: (identifier) @type-binding.type)) @type-binding.alias
|
||||
|
||||
;; Type bindings — var addr = user.address; (Java 10+ field-access alias)
|
||||
(local_variable_declaration
|
||||
type: (type_identifier) @_var_type
|
||||
(#eq? @_var_type "var")
|
||||
declarator: (variable_declarator
|
||||
name: (identifier) @type-binding.name
|
||||
value: (field_access
|
||||
field: (identifier) @type-binding.type))) @type-binding.alias
|
||||
|
||||
;; Type bindings — enhanced-for with var: for (var user : users)
|
||||
(enhanced_for_statement
|
||||
(type_identifier) @_var_type
|
||||
(#eq? @_var_type "var")
|
||||
(identifier) @type-binding.name
|
||||
(identifier) @type-binding.type) @type-binding.alias
|
||||
|
||||
;; Enhanced-for with var + method iterable: for (var user : data.values())
|
||||
(enhanced_for_statement
|
||||
(type_identifier) @_var_type
|
||||
(#eq? @_var_type "var")
|
||||
(identifier) @type-binding.name
|
||||
(method_invocation
|
||||
object: (identifier) @type-binding.type)) @type-binding.alias
|
||||
|
||||
;; Type bindings — var u = new User(); (Java 10+ local variable type inference)
|
||||
;; tree-sitter-java parses \`var\` as a \`type_identifier\` with text "var".
|
||||
;; The type-binding.constructor anchor fires when the rhs is an
|
||||
|
|
@ -143,6 +184,16 @@ const JAVA_SCOPE_QUERY = `
|
|||
type: (generic_type) @type-binding.type
|
||||
name: (identifier) @type-binding.name) @type-binding.annotation
|
||||
|
||||
;; Type bindings — instanceof pattern (Java 16+): if (obj instanceof User user)
|
||||
(instanceof_expression
|
||||
(type_identifier) @type-binding.type
|
||||
(identifier) @type-binding.name) @type-binding.pattern
|
||||
|
||||
;; Type bindings — switch case pattern (Java 21+): case User user ->
|
||||
(type_pattern
|
||||
(type_identifier) @type-binding.type
|
||||
(identifier) @type-binding.name) @type-binding.pattern
|
||||
|
||||
;; References — all method calls: foo() and obj.method()
|
||||
;; tree-sitter-java's query engine drops negation-based \`!object\`
|
||||
;; patterns when a positive \`object:\` pattern exists for the same
|
||||
|
|
@ -166,6 +217,30 @@ const JAVA_SCOPE_QUERY = `
|
|||
(object_creation_expression
|
||||
type: (scoped_type_identifier) @reference.call.constructor.qualified) @reference.call.constructor
|
||||
|
||||
;; References — method references: User::getName, obj::method
|
||||
(method_reference
|
||||
(identifier) @reference.receiver
|
||||
(identifier) @reference.name) @reference.call.member
|
||||
|
||||
;; References — this::method and super::method
|
||||
(method_reference
|
||||
(this) @reference.receiver
|
||||
(identifier) @reference.name) @reference.call.member
|
||||
|
||||
(method_reference
|
||||
(super) @reference.receiver
|
||||
(identifier) @reference.name) @reference.call.member
|
||||
|
||||
;; References — field_access::method: responseBuilder::buildResponse
|
||||
(method_reference
|
||||
(field_access) @reference.receiver
|
||||
(identifier) @reference.name) @reference.call.member
|
||||
|
||||
;; References — constructor references: User::new
|
||||
(method_reference
|
||||
(identifier) @reference.name
|
||||
"new") @reference.call.constructor
|
||||
|
||||
;; References — field/property writes: obj.name = "x"
|
||||
(assignment_expression
|
||||
left: (field_access
|
||||
|
|
|
|||
|
|
@ -4,53 +4,29 @@
|
|||
*
|
||||
* ## Registry-primary parity status
|
||||
*
|
||||
* Java is **not** in `MIGRATED_LANGUAGES` — the scope-resolution
|
||||
* registry runs in shadow mode only. Parity in forced registry mode
|
||||
* (`REGISTRY_PRIMARY_JAVA=1`) is 143/172 (83%). The 29 gaps fall into:
|
||||
* Java is in `MIGRATED_LANGUAGES` — the scope-resolution registry is
|
||||
* the primary call-resolution path. Parity: 178/178 (100%).
|
||||
*
|
||||
* - switch pattern binding / sealed-class exhaustiveness
|
||||
* - Map.values() / entrySet() iteration type propagation
|
||||
* - assignment / method chain return-type propagation across files
|
||||
* - virtual dispatch / interface default methods
|
||||
*
|
||||
* These are the same category of advanced-resolution gaps seen in prior
|
||||
* migrations (Python, C#, Go). Parity is below the ≥99% flip threshold
|
||||
* per RFC §6.4.
|
||||
*
|
||||
* **CI visibility:** Because Java is absent from `MIGRATED_LANGUAGES`,
|
||||
* the parity CI workflow (`ci-scope-parity.yml`) does not run Java in
|
||||
* either `REGISTRY_PRIMARY_JAVA=0` or `=1` mode. Regressions in forced
|
||||
* mode are only visible via manual `REGISTRY_PRIMARY_JAVA=1 npx vitest
|
||||
* run java.test.ts`. Before flipping Java to registry-primary, a
|
||||
* non-required CI step should be added to run Java tests in forced mode
|
||||
* and report parity as a dashboard input.
|
||||
*
|
||||
* **Parity baseline (29 failures):** The 29 gaps in forced registry mode
|
||||
* are tracked in this PR (#1482) and this JSDoc. If the gap count
|
||||
* changes (up or down), update this baseline accordingly.
|
||||
*
|
||||
* ### Known flip-blockers (must fix before adding to MIGRATED_LANGUAGES)
|
||||
*
|
||||
* - Varargs arity: fixed-prefix count is now preserved, but no
|
||||
* integration fixture exercises the 0-arg rejection path yet.
|
||||
* - Static import resolution: `import static X.Y.m` now correctly
|
||||
* resolves to `X/Y.java` (the class), not `X/Y/m.java` (the member).
|
||||
* Edge cases with nested classes may remain.
|
||||
* - Generic superclass receiver binding: `BaseModel<T>` now strips
|
||||
* to `BaseModel` via JVM type-erasure fallback in `stripGeneric`.
|
||||
* - Wildcard import (`import com.example.*`) file selection is
|
||||
* nondeterministic when multiple classes share a package directory.
|
||||
* May produce wrong-file edges in forced mode.
|
||||
* - Qualified generic type parameters in field/parameter annotations
|
||||
* (`com.example.BaseModel<T>`) — rare in practice but may miss
|
||||
* resolution when the full qualifier is present with generics.
|
||||
* **CI visibility:** The parity CI workflow (`ci-scope-parity.yml`)
|
||||
* runs Java tests in both `REGISTRY_PRIMARY_JAVA=0` and `=1` modes
|
||||
* automatically.
|
||||
*/
|
||||
|
||||
import type { ParsedFile } from 'gitnexus-shared';
|
||||
import type { ParsedFile, TypeRef } from 'gitnexus-shared';
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import type { KnowledgeGraph } from '../../../graph/types.js';
|
||||
import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js';
|
||||
import { populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js';
|
||||
import { resolveDefGraphId } from '../../scope-resolution/graph-bridge/ids.js';
|
||||
import type { GraphNodeLookup } from '../../scope-resolution/graph-bridge/node-lookup.js';
|
||||
import {
|
||||
isClassLike,
|
||||
lookupBindingsAt,
|
||||
namesAtScope,
|
||||
populateClassOwnedMembers,
|
||||
} from '../../scope-resolution/scope/walkers.js';
|
||||
import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js';
|
||||
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
||||
import { followChainPostFinalize } from '../../scope-resolution/passes/imported-return-types.js';
|
||||
import { javaProvider } from '../java.js';
|
||||
import {
|
||||
javaArityCompatibility,
|
||||
|
|
@ -58,6 +34,7 @@ import {
|
|||
resolveJavaImportTarget,
|
||||
type JavaResolveContext,
|
||||
} from './index.js';
|
||||
import { populateJavaPackageSiblings } from './package-siblings.js';
|
||||
|
||||
const javaScopeResolver: ScopeResolver = {
|
||||
language: SupportedLanguages.Java,
|
||||
|
|
@ -76,22 +53,167 @@ const javaScopeResolver: ScopeResolver = {
|
|||
|
||||
arityCompatibility: (callsite, def) => javaArityCompatibility(def, callsite),
|
||||
|
||||
buildMro: (graph, parsedFiles, nodeLookup) =>
|
||||
buildMro(graph, parsedFiles, nodeLookup, defaultLinearize),
|
||||
buildMro: buildJavaMro,
|
||||
|
||||
populateOwners: (parsed: ParsedFile) => populateClassOwnedMembers(parsed),
|
||||
|
||||
isSuperReceiver: (text) => text.trim() === 'super',
|
||||
|
||||
// Java is statically typed — field-fallback heuristic stays off
|
||||
fieldFallbackOnMethodLookup: false,
|
||||
propagatesReturnTypesAcrossImports: true,
|
||||
|
||||
// Java doesn't collapse member calls
|
||||
collapseMemberCallsByCallerTarget: false,
|
||||
|
||||
// Hoist return-type bindings to Module scope for cross-file propagation
|
||||
collapseMemberCallsByCallerTarget: true,
|
||||
hoistTypeBindingsToModule: true,
|
||||
|
||||
populateNamespaceSiblings: populateJavaPackageSiblings,
|
||||
populateRangeBindings: populateJavaCrossFileReturnTypes,
|
||||
};
|
||||
|
||||
export { javaScopeResolver };
|
||||
|
||||
function populateJavaCrossFileReturnTypes(
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
indexes: ScopeResolutionIndexes,
|
||||
): void {
|
||||
const moduleScopeByFile = new Map<string, ParsedFile['scopes'][number]>();
|
||||
const classScopesByFile = new Map<string, ParsedFile['scopes'][number][]>();
|
||||
for (const parsed of parsedFiles) {
|
||||
const ms = parsed.scopes.find((s) => s.kind === 'Module');
|
||||
if (ms !== undefined) moduleScopeByFile.set(parsed.filePath, ms);
|
||||
const cs = parsed.scopes.filter((s) => s.kind === 'Class');
|
||||
if (cs.length > 0) classScopesByFile.set(parsed.filePath, cs);
|
||||
}
|
||||
|
||||
for (const parsed of parsedFiles) {
|
||||
const importerModule = moduleScopeByFile.get(parsed.filePath);
|
||||
if (importerModule === undefined) continue;
|
||||
|
||||
const ambiguousMirrors = new Set<string>();
|
||||
for (const name of namesAtScope(importerModule.id, indexes)) {
|
||||
const refs = lookupBindingsAt(importerModule.id, name, indexes);
|
||||
for (const ref of refs) {
|
||||
if (ref.origin !== 'import' && ref.origin !== 'reexport') continue;
|
||||
if (!isClassLike(ref.def.type)) continue;
|
||||
|
||||
const sourceModule = moduleScopeByFile.get(ref.def.filePath);
|
||||
if (sourceModule === undefined) continue;
|
||||
|
||||
const tb = importerModule.typeBindings as Map<string, TypeRef>;
|
||||
for (const [srcName, srcRef] of sourceModule.typeBindings) {
|
||||
if (srcRef.source !== 'return-annotation') continue;
|
||||
if (ambiguousMirrors.has(srcName)) continue;
|
||||
const existing = tb.get(srcName);
|
||||
if (existing !== undefined && existing.rawName !== srcRef.rawName) {
|
||||
ambiguousMirrors.add(srcName);
|
||||
tb.delete(srcName);
|
||||
continue;
|
||||
}
|
||||
if (existing === undefined) tb.set(srcName, srcRef);
|
||||
}
|
||||
|
||||
for (const classScope of classScopesByFile.get(ref.def.filePath) ?? []) {
|
||||
for (const [srcName, srcRef] of classScope.typeBindings) {
|
||||
if (srcRef.source === 'self' || srcRef.source === 'parameter-annotation') continue;
|
||||
if (ambiguousMirrors.has(srcName)) continue;
|
||||
const existing = tb.get(srcName);
|
||||
if (existing !== undefined && existing.rawName !== srcRef.rawName) {
|
||||
ambiguousMirrors.add(srcName);
|
||||
tb.delete(srcName);
|
||||
continue;
|
||||
}
|
||||
if (existing === undefined) tb.set(srcName, srcRef);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [name, ref] of importerModule.typeBindings) {
|
||||
const resolved = followChainPostFinalize(ref, importerModule.id, indexes);
|
||||
if (resolved !== ref) {
|
||||
(importerModule.typeBindings as Map<string, TypeRef>).set(name, resolved);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const parsed of parsedFiles) {
|
||||
const moduleScopeId = moduleScopeByFile.get(parsed.filePath)?.id;
|
||||
for (const scope of parsed.scopes) {
|
||||
if (scope.id === moduleScopeId) continue;
|
||||
for (const [name, ref] of scope.typeBindings) {
|
||||
const resolved = followChainPostFinalize(ref, scope.id, indexes);
|
||||
if (resolved !== ref) {
|
||||
(scope.typeBindings as Map<string, TypeRef>).set(name, resolved);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildJavaMro(
|
||||
graph: KnowledgeGraph,
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
nodeLookup: GraphNodeLookup,
|
||||
): Map<string, string[]> {
|
||||
const mro = buildMro(graph, parsedFiles, nodeLookup, defaultLinearize);
|
||||
|
||||
const defIdByGraphId = new Map<string, string>();
|
||||
for (const parsed of parsedFiles) {
|
||||
for (const def of parsed.localDefs) {
|
||||
if (!isClassLike(def.type)) continue;
|
||||
const graphId = resolveDefGraphId(parsed.filePath, def, nodeLookup);
|
||||
if (graphId !== undefined) defIdByGraphId.set(graphId, def.nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
const directImpls = new Map<string, string[]>();
|
||||
for (const rel of graph.iterRelationshipsByType('IMPLEMENTS')) {
|
||||
const source = defIdByGraphId.get(rel.sourceId);
|
||||
const target = defIdByGraphId.get(rel.targetId);
|
||||
if (source === undefined || target === undefined) continue;
|
||||
let list = directImpls.get(source);
|
||||
if (list === undefined) {
|
||||
list = [];
|
||||
directImpls.set(source, list);
|
||||
}
|
||||
if (!list.includes(target)) list.push(target);
|
||||
}
|
||||
|
||||
for (const [classDefId, extendsMro] of mro) {
|
||||
const ancestorChain = [classDefId, ...extendsMro];
|
||||
const seeds: string[] = [];
|
||||
for (const ancestorId of ancestorChain) {
|
||||
for (const ifaceId of directImpls.get(ancestorId) ?? []) {
|
||||
seeds.push(ifaceId);
|
||||
}
|
||||
}
|
||||
if (seeds.length === 0) continue;
|
||||
const interfaces = closeInterfaces(seeds, directImpls);
|
||||
mro.set(classDefId, [...extendsMro, ...interfaces.filter((i) => !extendsMro.includes(i))]);
|
||||
}
|
||||
|
||||
for (const [classDefId, ifaces] of directImpls) {
|
||||
if (mro.has(classDefId)) continue;
|
||||
mro.set(classDefId, closeInterfaces([...ifaces], directImpls));
|
||||
}
|
||||
|
||||
return mro;
|
||||
}
|
||||
|
||||
function closeInterfaces(
|
||||
seeds: readonly string[],
|
||||
directImpls: ReadonlyMap<string, readonly string[]>,
|
||||
): string[] {
|
||||
const out: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
const queue: string[] = [...seeds];
|
||||
let head = 0;
|
||||
while (head < queue.length) {
|
||||
const cur = queue[head++]!;
|
||||
if (seen.has(cur)) continue;
|
||||
seen.add(cur);
|
||||
out.push(cur);
|
||||
for (const next of directImpls.get(cur) ?? []) {
|
||||
if (!seen.has(next)) queue.push(next);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import {
|
|||
import { createResolutionContext } from '../model/resolution-context.js';
|
||||
import { ASTCache, createASTCache } from '../ast-cache.js';
|
||||
import { type PipelineProgress, getLanguageFromFilename } from 'gitnexus-shared';
|
||||
import { isRegistryPrimary } from '../registry-primary-flag.js';
|
||||
import { readFileContents } from '../filesystem-walker.js';
|
||||
import { isLanguageAvailable } from '../../tree-sitter/parser-loader.js';
|
||||
import { createWorkerPool, WorkerPoolInitializationError } from '../workers/worker-pool.js';
|
||||
|
|
@ -606,11 +607,31 @@ export async function runChunkedParseAndResolve(
|
|||
if (chunkNeedsSynthesis[chunkIdx]) {
|
||||
anyChunkNeedsWildcardSynth = true;
|
||||
}
|
||||
for (const item of chunkWorkerData.imports) deferredWorkerImports.push(item);
|
||||
for (const item of chunkWorkerData.calls) deferredWorkerCalls.push(item);
|
||||
for (const item of chunkWorkerData.heritage) deferredWorkerHeritage.push(item);
|
||||
for (const item of chunkWorkerData.constructorBindings)
|
||||
deferredConstructorBindings.push(item);
|
||||
const skipFile = new Set<string>();
|
||||
const checkFile = new Set<string>();
|
||||
const shouldAccumulate = (filePath: string): boolean => {
|
||||
if (checkFile.has(filePath)) return true;
|
||||
if (skipFile.has(filePath)) return false;
|
||||
const lang = getLanguageFromFilename(filePath);
|
||||
if (lang !== null && isRegistryPrimary(lang)) {
|
||||
skipFile.add(filePath);
|
||||
return false;
|
||||
}
|
||||
checkFile.add(filePath);
|
||||
return true;
|
||||
};
|
||||
for (const item of chunkWorkerData.imports) {
|
||||
if (shouldAccumulate(item.filePath)) deferredWorkerImports.push(item);
|
||||
}
|
||||
for (const item of chunkWorkerData.calls) {
|
||||
if (shouldAccumulate(item.filePath)) deferredWorkerCalls.push(item);
|
||||
}
|
||||
for (const item of chunkWorkerData.heritage) {
|
||||
if (shouldAccumulate(item.filePath)) deferredWorkerHeritage.push(item);
|
||||
}
|
||||
for (const item of chunkWorkerData.constructorBindings) {
|
||||
if (shouldAccumulate(item.filePath)) deferredConstructorBindings.push(item);
|
||||
}
|
||||
// Aggregate worker-produced ParsedFile artifacts so scope-
|
||||
// resolution can use them as a re-extraction cache (skips its
|
||||
// own tree-sitter re-parse on warm runs).
|
||||
|
|
@ -618,7 +639,9 @@ export async function runChunkedParseAndResolve(
|
|||
for (const item of chunkWorkerData.parsedFiles) allParsedFiles.push(item);
|
||||
}
|
||||
if (chunkWorkerData.assignments?.length) {
|
||||
for (const item of chunkWorkerData.assignments) deferredAssignments.push(item);
|
||||
for (const item of chunkWorkerData.assignments) {
|
||||
if (shouldAccumulate(item.filePath)) deferredAssignments.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (chunkWorkerData.fileScopeBindings?.length) {
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ export const MIGRATED_LANGUAGES: ReadonlySet<SupportedLanguages> = new Set<Suppo
|
|||
SupportedLanguages.PHP,
|
||||
SupportedLanguages.JavaScript,
|
||||
SupportedLanguages.Kotlin,
|
||||
SupportedLanguages.Java,
|
||||
]);
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ export function emitFreeCallFallback(
|
|||
if (site.callForm === 'constructor') {
|
||||
const classDef = findClassBindingInScope(site.inScope, site.name, scopes);
|
||||
if (classDef !== undefined) {
|
||||
fnDef = pickConstructorOrClass(classDef, workspaceIndex);
|
||||
fnDef = pickConstructorOrClass(classDef, workspaceIndex, scopes);
|
||||
}
|
||||
}
|
||||
// Implicit-this overload narrowing: an unqualified call inside
|
||||
|
|
@ -578,12 +578,22 @@ function logicalCallableKey(def: SymbolDefinition): string {
|
|||
function pickConstructorOrClass(
|
||||
classDef: SymbolDefinition,
|
||||
workspaceIndex: WorkspaceResolutionIndex,
|
||||
scopes?: ScopeResolutionIndexes,
|
||||
): SymbolDefinition {
|
||||
const classScope = workspaceIndex.classScopeByDefId.get(classDef.nodeId);
|
||||
if (classScope === undefined) return classDef;
|
||||
for (const def of classScope.ownedDefs) {
|
||||
if (def.type === 'Constructor') return def;
|
||||
}
|
||||
if (scopes !== undefined) {
|
||||
for (const childId of scopes.scopeTree.getChildren(classScope.id)) {
|
||||
const childScope = scopes.scopeTree.getScope(childId);
|
||||
if (childScope === undefined || childScope.kind === 'Class') continue;
|
||||
for (const def of childScope.ownedDefs) {
|
||||
if (def.type === 'Constructor') return def;
|
||||
}
|
||||
}
|
||||
}
|
||||
return classDef;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1383,10 +1383,18 @@ describe('processCallsFromExtracted', () => {
|
|||
describe('processCalls — Phase P class lookup fallback', () => {
|
||||
let graph: ReturnType<typeof createKnowledgeGraph>;
|
||||
let ctx: ResolutionContext;
|
||||
let prevRegistryJava: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
graph = createKnowledgeGraph();
|
||||
ctx = createResolutionContext();
|
||||
prevRegistryJava = process.env['REGISTRY_PRIMARY_JAVA'];
|
||||
process.env['REGISTRY_PRIMARY_JAVA'] = 'false';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (prevRegistryJava === undefined) delete process.env['REGISTRY_PRIMARY_JAVA'];
|
||||
else process.env['REGISTRY_PRIMARY_JAVA'] = prevRegistryJava;
|
||||
});
|
||||
|
||||
it('uses lookupClassByName to override interface receiver types for cross-file virtual dispatch', async () => {
|
||||
|
|
@ -2147,10 +2155,13 @@ describe('processNextjsFetchRoutes', () => {
|
|||
describe('processCallsFromExtracted — interface dispatch', () => {
|
||||
let graph: ReturnType<typeof createKnowledgeGraph>;
|
||||
let ctx: ResolutionContext;
|
||||
let prevRegistryJava: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
graph = createKnowledgeGraph();
|
||||
ctx = createResolutionContext();
|
||||
prevRegistryJava = process.env['REGISTRY_PRIMARY_JAVA'];
|
||||
process.env['REGISTRY_PRIMARY_JAVA'] = 'false';
|
||||
const ifaceFile = 'contracts/Action.java';
|
||||
const runnerFile = 'runner.java';
|
||||
const implA = 'impl/A.java';
|
||||
|
|
@ -2195,6 +2206,11 @@ describe('processCallsFromExtracted — interface dispatch', () => {
|
|||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (prevRegistryJava === undefined) delete process.env['REGISTRY_PRIMARY_JAVA'];
|
||||
else process.env['REGISTRY_PRIMARY_JAVA'] = prevRegistryJava;
|
||||
});
|
||||
|
||||
it('adds CALLS to interface method plus lower-confidence edges to implementing methods', async () => {
|
||||
const heritage: ExtractedHeritage[] = [
|
||||
{ filePath: 'impl/A.java', className: 'A', parentName: 'Action', kind: 'implements' },
|
||||
|
|
@ -2241,21 +2257,26 @@ describe('processCalls — D0 MRO fast path (SM-10)', () => {
|
|||
let graph: ReturnType<typeof createKnowledgeGraph>;
|
||||
let ctx: ResolutionContext;
|
||||
let prevRegistryPython: string | undefined;
|
||||
let prevRegistryJava: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
graph = createKnowledgeGraph();
|
||||
ctx = createResolutionContext();
|
||||
// These tests exercise the LEGACY call-resolution DAG directly
|
||||
// using .py fixtures. Python defaults to registry-primary now
|
||||
// (MIGRATED_LANGUAGES), which gates call-processor out for
|
||||
// Python files. Force the flag off so the legacy DAG runs.
|
||||
// using .py/.java fixtures. Python and Java default to registry-
|
||||
// primary now (MIGRATED_LANGUAGES), which gates call-processor
|
||||
// out for those files. Force the flags off so the legacy DAG runs.
|
||||
prevRegistryPython = process.env['REGISTRY_PRIMARY_PYTHON'];
|
||||
process.env['REGISTRY_PRIMARY_PYTHON'] = 'false';
|
||||
prevRegistryJava = process.env['REGISTRY_PRIMARY_JAVA'];
|
||||
process.env['REGISTRY_PRIMARY_JAVA'] = 'false';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (prevRegistryPython === undefined) delete process.env['REGISTRY_PRIMARY_PYTHON'];
|
||||
else process.env['REGISTRY_PRIMARY_PYTHON'] = prevRegistryPython;
|
||||
if (prevRegistryJava === undefined) delete process.env['REGISTRY_PRIMARY_JAVA'];
|
||||
else process.env['REGISTRY_PRIMARY_JAVA'] = prevRegistryJava;
|
||||
});
|
||||
|
||||
const setupChildParent = () => {
|
||||
|
|
|
|||
|
|
@ -108,20 +108,20 @@ describe('isRegistryPrimary', () => {
|
|||
it('isolates flags per-language (one on does not affect others)', () => {
|
||||
process.env['REGISTRY_PRIMARY_PYTHON'] = 'true';
|
||||
expect(isRegistryPrimary(SupportedLanguages.Python)).toBe(true);
|
||||
// Java is not in MIGRATED_LANGUAGES — default false stays
|
||||
// Ruby is not in MIGRATED_LANGUAGES — default false stays
|
||||
// false regardless of Python's flag.
|
||||
expect(isRegistryPrimary(SupportedLanguages.Java)).toBe(false);
|
||||
expect(isRegistryPrimary(SupportedLanguages.Ruby)).toBe(false);
|
||||
});
|
||||
|
||||
it('respects a mid-process env-var mutation (no stale cache)', () => {
|
||||
// Use Java — not in MIGRATED_LANGUAGES — so the unset default is
|
||||
// Use Ruby — not in MIGRATED_LANGUAGES — so the unset default is
|
||||
// deterministically `false`, independent of which languages have
|
||||
// been flipped to registry-primary.
|
||||
expect(isRegistryPrimary(SupportedLanguages.Java)).toBe(false);
|
||||
process.env['REGISTRY_PRIMARY_JAVA'] = 'true';
|
||||
expect(isRegistryPrimary(SupportedLanguages.Java)).toBe(true);
|
||||
delete process.env['REGISTRY_PRIMARY_JAVA'];
|
||||
expect(isRegistryPrimary(SupportedLanguages.Java)).toBe(false);
|
||||
expect(isRegistryPrimary(SupportedLanguages.Ruby)).toBe(false);
|
||||
process.env['REGISTRY_PRIMARY_RUBY'] = 'true';
|
||||
expect(isRegistryPrimary(SupportedLanguages.Ruby)).toBe(true);
|
||||
delete process.env['REGISTRY_PRIMARY_RUBY'];
|
||||
expect(isRegistryPrimary(SupportedLanguages.Ruby)).toBe(false);
|
||||
});
|
||||
|
||||
it('handles the CPlusPlus → REGISTRY_PRIMARY_CPP mapping correctly', () => {
|
||||
|
|
@ -150,7 +150,7 @@ describe('primaryLanguages', () => {
|
|||
|
||||
it('returns exactly the flipped languages (env opts in unmigrated, opts out migrated)', () => {
|
||||
// Migrated languages are default-on; each must be opted out here when
|
||||
// testing explicit env overrides. Java (unmigrated) opts in.
|
||||
// testing explicit env overrides. Ruby (unmigrated) opts in.
|
||||
// Opt out every member of MIGRATED_LANGUAGES dynamically so this test
|
||||
// does not have to be updated each time a new language ships its
|
||||
// Ring 3 migration (C++ and PHP joined the set in their respective
|
||||
|
|
@ -158,15 +158,15 @@ describe('primaryLanguages', () => {
|
|||
for (const lang of MIGRATED_LANGUAGES) {
|
||||
process.env[envVarNameFor(lang)] = 'false';
|
||||
}
|
||||
process.env['REGISTRY_PRIMARY_JAVA'] = '1';
|
||||
process.env['REGISTRY_PRIMARY_RUBY'] = '1';
|
||||
const enabled = primaryLanguages();
|
||||
expect(enabled.has(SupportedLanguages.Python)).toBe(false);
|
||||
expect(enabled.has(SupportedLanguages.CSharp)).toBe(false);
|
||||
expect(enabled.has(SupportedLanguages.Go)).toBe(false);
|
||||
expect(enabled.has(SupportedLanguages.CPlusPlus)).toBe(false);
|
||||
expect(enabled.has(SupportedLanguages.PHP)).toBe(false);
|
||||
expect(enabled.has(SupportedLanguages.Java)).toBe(true);
|
||||
// Only Java is on: migrated defaults overridden off, Java explicitly on.
|
||||
expect(enabled.has(SupportedLanguages.Ruby)).toBe(true);
|
||||
// Only Ruby is on: migrated defaults overridden off, Ruby explicitly on.
|
||||
expect(enabled.size).toBe(1);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue