mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-07 08:26:11 +00:00
fix(group): preserve full paths for nested Kotlin constants (#3059)
Key nested objects and companions by their full enclosing type path so same-file, imported, and bare nested references resolve consistently. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
97a84eeaae
commit
face59ae1c
4 changed files with 78 additions and 26 deletions
|
|
@ -313,22 +313,24 @@ function classifyPathArgument(expr: Parser.SyntaxNode): PathArgumentPrefix {
|
|||
}
|
||||
|
||||
/**
|
||||
* Type declarations enclosing `node`, innermost first, by declared name.
|
||||
* Type declarations enclosing `node`, innermost first, by qualified type path.
|
||||
*
|
||||
* The scope a bare constant in a route annotation is resolved against; passed to
|
||||
* `foldKotlinOperands`, which applies it. Collects `class_declaration` (including
|
||||
* interfaces) and `object_declaration`. A `companion_object` adds no link of
|
||||
* its own — members are keyed under the enclosing class one hop up. Skips
|
||||
* unnamed types rather than guessing.
|
||||
* its own — members are keyed under the enclosing class one hop up. For a node
|
||||
* inside `Outer.Inner`, returns `['Outer.Inner', 'Outer']`, matching the keys
|
||||
* produced by `extractKotlinModuleConstants`. Skips unnamed types rather than
|
||||
* guessing.
|
||||
*/
|
||||
function kotlinEnclosingTypeNames(node: Parser.SyntaxNode): string[] {
|
||||
const out: string[] = [];
|
||||
const simpleNames: string[] = [];
|
||||
for (let cur = node.parent; cur; cur = cur.parent) {
|
||||
if (cur.type !== 'class_declaration' && cur.type !== 'object_declaration') continue;
|
||||
const ident = cur.children.find((c) => c.type === 'type_identifier');
|
||||
if (ident) out.push(unquoteKotlinIdentifier(ident.text));
|
||||
if (ident) simpleNames.push(unquoteKotlinIdentifier(ident.text));
|
||||
}
|
||||
return out;
|
||||
return simpleNames.map((_, index) => simpleNames.slice(index).reverse().join('.'));
|
||||
}
|
||||
|
||||
// ─── Kotlin OkHttp builder verb-walk (parity with java-static-path.ts) ──
|
||||
|
|
|
|||
|
|
@ -574,11 +574,12 @@ function initializerOf(property: Parser.SyntaxNode): Parser.SyntaxNode | null {
|
|||
interface KotlinConstDeclaration {
|
||||
/** The declaration's simple name. */
|
||||
readonly name: string;
|
||||
/** `<DeclaringType>.<NAME>`, or null for a top-level declaration. */
|
||||
/** `<Qualified.DeclaringType>.<NAME>`, or null for a top-level declaration. */
|
||||
readonly qualified: string | null;
|
||||
/**
|
||||
* The qualified-key prefixes in LEXICAL scope for this declaration's
|
||||
* initializer, innermost first (`['Inner', 'Outer']`). Empty at file level.
|
||||
* initializer, innermost first (`['Outer.Inner', 'Outer']`). Empty at file
|
||||
* level.
|
||||
*/
|
||||
readonly scopes: readonly string[];
|
||||
/**
|
||||
|
|
@ -757,6 +758,16 @@ export function extractKotlinModuleConstants(tree: Parser.Tree): KotlinModuleCon
|
|||
return ident ? unquoteKotlinIdentifier(ident.text) : null;
|
||||
};
|
||||
|
||||
/** Append one simple type name to its enclosing qualified type path. */
|
||||
const nestedTypeName = (enclosingType: string | null, name: string | null): string | null => {
|
||||
if (name === null) return enclosingType;
|
||||
return enclosingType === null ? name : `${enclosingType}.${name}`;
|
||||
};
|
||||
|
||||
/** Prepend a qualified scope unless it is already the innermost scope. */
|
||||
const withScope = (scope: string | null, scopes: readonly string[]): readonly string[] =>
|
||||
scope === null || scopes[0] === scope ? scopes : [scope, ...scopes];
|
||||
|
||||
const walkDeclarations = (
|
||||
node: Parser.SyntaxNode,
|
||||
enclosingType: string | null,
|
||||
|
|
@ -767,11 +778,13 @@ export function extractKotlinModuleConstants(tree: Parser.Tree): KotlinModuleCon
|
|||
const name = typeNameOf(child);
|
||||
const body = bodyOf(child);
|
||||
if (!body) continue;
|
||||
// Members are reachable only as `A.NAME`; inside the body, `NAME` alone
|
||||
// means this object's member and nothing else, hence the pushed scope.
|
||||
const inner = name === null ? scopes : [name, ...scopes];
|
||||
collectProperties(body, name, inner, false, false);
|
||||
walkDeclarations(body, name, inner);
|
||||
// Carry the full path: a nested object member is `Outer.Inner.NAME`, not
|
||||
// `Inner.NAME`. Inside the body a bare name searches that qualified
|
||||
// scope first, then each enclosing type.
|
||||
const declaredType = nestedTypeName(enclosingType, name);
|
||||
const inner = withScope(declaredType, scopes);
|
||||
collectProperties(body, declaredType, inner, false, false);
|
||||
walkDeclarations(body, declaredType, inner);
|
||||
continue;
|
||||
}
|
||||
if (child.type === 'companion_object') {
|
||||
|
|
@ -782,7 +795,7 @@ export function extractKotlinModuleConstants(tree: Parser.Tree): KotlinModuleCon
|
|||
// simple name is bound inside that class body only, which is a SCOPE and
|
||||
// not a file-level key: it is reached from the reference site by
|
||||
// `qualifyKotlinRefInEnclosingTypes`, through this same `Holder.NAME`.
|
||||
const inner = enclosingType === null ? scopes : [enclosingType, ...scopes];
|
||||
const inner = withScope(enclosingType, scopes);
|
||||
collectProperties(body, enclosingType, inner, false, true);
|
||||
walkDeclarations(body, enclosingType, inner);
|
||||
continue;
|
||||
|
|
@ -792,7 +805,8 @@ export function extractKotlinModuleConstants(tree: Parser.Tree): KotlinModuleCon
|
|||
// only its nested objects and companion contribute constants.
|
||||
const name = typeNameOf(child);
|
||||
const body = bodyOf(child);
|
||||
if (body) walkDeclarations(body, name, scopes);
|
||||
const declaredType = nestedTypeName(enclosingType, name);
|
||||
if (body) walkDeclarations(body, declaredType, withScope(declaredType, scopes));
|
||||
continue;
|
||||
}
|
||||
walkDeclarations(child, enclosingType, scopes);
|
||||
|
|
@ -1056,12 +1070,13 @@ function foldOperands(
|
|||
* bare when none does — the reference-site twin of the `qualifyRef` that
|
||||
* {@link extractKotlinModuleConstants} applies to sibling initializers.
|
||||
*
|
||||
* `enclosingTypes` is the chain of type declarations the reference sits inside,
|
||||
* INNERMOST FIRST (`['Inner', 'Outer']`). A companion member is keyed
|
||||
* `<EnclosingClass>.<NAME>` and is bound unqualified exactly within that class
|
||||
* body — including its nested types, which is why the whole chain is walked and
|
||||
* not just the innermost link. An `object`'s own members are in scope inside its
|
||||
* body under the same `<Owner>.<NAME>` key, so the same walk covers both.
|
||||
* `enclosingTypes` is the chain of qualified type paths the reference sits
|
||||
* inside, INNERMOST FIRST (`['Outer.Inner', 'Outer']`). A companion member is
|
||||
* keyed `<EnclosingClass>.<NAME>` and is bound unqualified exactly within that
|
||||
* class body — including its nested types, which is why the whole chain is
|
||||
* walked and not just the innermost link. An `object`'s own members are in scope
|
||||
* inside its body under the same `<Owner>.<NAME>` key, so the same walk covers
|
||||
* both.
|
||||
*
|
||||
* Innermost-first, and BEFORE the file-level maps the fold consults next, is
|
||||
* Kotlin's own order: a companion member shadows a same-named top-level
|
||||
|
|
|
|||
|
|
@ -761,6 +761,30 @@ class OrderController {
|
|||
).toEqual(['GET /api/v1/orders']);
|
||||
});
|
||||
|
||||
it('resolves a bare companion constant inside a nested class', () => {
|
||||
// Declaration extraction and reference-site qualification must agree on
|
||||
// the full owner path. `ORDERS` here means `Outer.Inner.ORDERS`, not the
|
||||
// nonexistent top-level `Inner.ORDERS`.
|
||||
expect(
|
||||
providers({
|
||||
[CONTROLLER]: `package com.example.app.web
|
||||
|
||||
@RestController
|
||||
class Outer {
|
||||
class Inner {
|
||||
companion object {
|
||||
const val ORDERS = "/nested/orders"
|
||||
}
|
||||
|
||||
@GetMapping(ORDERS)
|
||||
fun list() {}
|
||||
}
|
||||
}
|
||||
`,
|
||||
}),
|
||||
).toEqual(['GET /nested/orders']);
|
||||
});
|
||||
|
||||
it('folds through the file that declares the package, not one whose path imitates it', () => {
|
||||
// The decoy's PATH ends with the imported FQN, but it declares
|
||||
// `package x.com.example.app.api` — a different declaration. Choosing the
|
||||
|
|
|
|||
|
|
@ -793,18 +793,23 @@ class Outer {
|
|||
`,
|
||||
});
|
||||
expect(
|
||||
foldKotlinOperands(key, [{ kind: 'ref', name: 'ORDERS' }], repo, ['Inner', 'Outer']),
|
||||
foldKotlinOperands(key, [{ kind: 'ref', name: 'ORDERS' }], repo, ['Outer.Inner', 'Outer']),
|
||||
).toBe('/inner');
|
||||
expect(
|
||||
foldKotlinOperands(key, [{ kind: 'ref', name: 'ONLY_OUTER' }], repo, ['Inner', 'Outer']),
|
||||
foldKotlinOperands(key, [{ kind: 'ref', name: 'ONLY_OUTER' }], repo, [
|
||||
'Outer.Inner',
|
||||
'Outer',
|
||||
]),
|
||||
).toBe('/only-outer');
|
||||
});
|
||||
|
||||
it('resolves a nested object member through the enclosing object', () => {
|
||||
it('keys a nested object by its full enclosing type path', () => {
|
||||
// `Inner`'s initializer names `P`, which `Inner` does not declare and
|
||||
// `Outer` does; the scope chain is walked innermost-first, so it means
|
||||
// `Outer.P` — not the same-named member of the unrelated `Other`.
|
||||
// `Outer.P` — not the same-named member of the unrelated `Other`. The
|
||||
// declaration itself is reachable as `Outer.Inner.Q`, never `Inner.Q`.
|
||||
const key = 'src/main/kotlin/com/example/app/api/Nested.kt';
|
||||
const controllerKey = 'src/main/kotlin/com/example/app/web/Controller.kt';
|
||||
const repo = repoOf({
|
||||
[key]: `package com.example.app.api
|
||||
|
||||
|
|
@ -818,9 +823,15 @@ object Outer {
|
|||
const val Q = P + "/q"
|
||||
}
|
||||
}
|
||||
`,
|
||||
[controllerKey]: `package com.example.app.web
|
||||
|
||||
import com.example.app.api.Outer
|
||||
`,
|
||||
});
|
||||
expect(resolveKotlinConstant(key, 'Inner.Q', repo)).toBe('/right/q');
|
||||
expect(resolveKotlinConstant(key, 'Outer.Inner.Q', repo)).toBe('/right/q');
|
||||
expect(resolveKotlinConstant(controllerKey, 'Outer.Inner.Q', repo)).toBe('/right/q');
|
||||
expect(resolveKotlinConstant(key, 'Inner.Q', repo)).toBeNull();
|
||||
});
|
||||
|
||||
it('does not fall through to a file-level constant for an unfoldable sibling', () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue