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
|
* The scope a bare constant in a route annotation is resolved against; passed to
|
||||||
* `foldKotlinOperands`, which applies it. Collects `class_declaration` (including
|
* `foldKotlinOperands`, which applies it. Collects `class_declaration` (including
|
||||||
* interfaces) and `object_declaration`. A `companion_object` adds no link of
|
* interfaces) and `object_declaration`. A `companion_object` adds no link of
|
||||||
* its own — members are keyed under the enclosing class one hop up. Skips
|
* its own — members are keyed under the enclosing class one hop up. For a node
|
||||||
* unnamed types rather than guessing.
|
* 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[] {
|
function kotlinEnclosingTypeNames(node: Parser.SyntaxNode): string[] {
|
||||||
const out: string[] = [];
|
const simpleNames: string[] = [];
|
||||||
for (let cur = node.parent; cur; cur = cur.parent) {
|
for (let cur = node.parent; cur; cur = cur.parent) {
|
||||||
if (cur.type !== 'class_declaration' && cur.type !== 'object_declaration') continue;
|
if (cur.type !== 'class_declaration' && cur.type !== 'object_declaration') continue;
|
||||||
const ident = cur.children.find((c) => c.type === 'type_identifier');
|
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) ──
|
// ─── 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 {
|
interface KotlinConstDeclaration {
|
||||||
/** The declaration's simple name. */
|
/** The declaration's simple name. */
|
||||||
readonly name: string;
|
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;
|
readonly qualified: string | null;
|
||||||
/**
|
/**
|
||||||
* The qualified-key prefixes in LEXICAL scope for this declaration's
|
* 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[];
|
readonly scopes: readonly string[];
|
||||||
/**
|
/**
|
||||||
|
|
@ -757,6 +758,16 @@ export function extractKotlinModuleConstants(tree: Parser.Tree): KotlinModuleCon
|
||||||
return ident ? unquoteKotlinIdentifier(ident.text) : null;
|
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 = (
|
const walkDeclarations = (
|
||||||
node: Parser.SyntaxNode,
|
node: Parser.SyntaxNode,
|
||||||
enclosingType: string | null,
|
enclosingType: string | null,
|
||||||
|
|
@ -767,11 +778,13 @@ export function extractKotlinModuleConstants(tree: Parser.Tree): KotlinModuleCon
|
||||||
const name = typeNameOf(child);
|
const name = typeNameOf(child);
|
||||||
const body = bodyOf(child);
|
const body = bodyOf(child);
|
||||||
if (!body) continue;
|
if (!body) continue;
|
||||||
// Members are reachable only as `A.NAME`; inside the body, `NAME` alone
|
// Carry the full path: a nested object member is `Outer.Inner.NAME`, not
|
||||||
// means this object's member and nothing else, hence the pushed scope.
|
// `Inner.NAME`. Inside the body a bare name searches that qualified
|
||||||
const inner = name === null ? scopes : [name, ...scopes];
|
// scope first, then each enclosing type.
|
||||||
collectProperties(body, name, inner, false, false);
|
const declaredType = nestedTypeName(enclosingType, name);
|
||||||
walkDeclarations(body, name, inner);
|
const inner = withScope(declaredType, scopes);
|
||||||
|
collectProperties(body, declaredType, inner, false, false);
|
||||||
|
walkDeclarations(body, declaredType, inner);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (child.type === 'companion_object') {
|
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
|
// 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
|
// not a file-level key: it is reached from the reference site by
|
||||||
// `qualifyKotlinRefInEnclosingTypes`, through this same `Holder.NAME`.
|
// `qualifyKotlinRefInEnclosingTypes`, through this same `Holder.NAME`.
|
||||||
const inner = enclosingType === null ? scopes : [enclosingType, ...scopes];
|
const inner = withScope(enclosingType, scopes);
|
||||||
collectProperties(body, enclosingType, inner, false, true);
|
collectProperties(body, enclosingType, inner, false, true);
|
||||||
walkDeclarations(body, enclosingType, inner);
|
walkDeclarations(body, enclosingType, inner);
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -792,7 +805,8 @@ export function extractKotlinModuleConstants(tree: Parser.Tree): KotlinModuleCon
|
||||||
// only its nested objects and companion contribute constants.
|
// only its nested objects and companion contribute constants.
|
||||||
const name = typeNameOf(child);
|
const name = typeNameOf(child);
|
||||||
const body = bodyOf(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;
|
continue;
|
||||||
}
|
}
|
||||||
walkDeclarations(child, enclosingType, scopes);
|
walkDeclarations(child, enclosingType, scopes);
|
||||||
|
|
@ -1056,12 +1070,13 @@ function foldOperands(
|
||||||
* bare when none does — the reference-site twin of the `qualifyRef` that
|
* bare when none does — the reference-site twin of the `qualifyRef` that
|
||||||
* {@link extractKotlinModuleConstants} applies to sibling initializers.
|
* {@link extractKotlinModuleConstants} applies to sibling initializers.
|
||||||
*
|
*
|
||||||
* `enclosingTypes` is the chain of type declarations the reference sits inside,
|
* `enclosingTypes` is the chain of qualified type paths the reference sits
|
||||||
* INNERMOST FIRST (`['Inner', 'Outer']`). A companion member is keyed
|
* inside, INNERMOST FIRST (`['Outer.Inner', 'Outer']`). A companion member is
|
||||||
* `<EnclosingClass>.<NAME>` and is bound unqualified exactly within that class
|
* keyed `<EnclosingClass>.<NAME>` and is bound unqualified exactly within that
|
||||||
* body — including its nested types, which is why the whole chain is walked and
|
* class body — including its nested types, which is why the whole chain is
|
||||||
* not just the innermost link. An `object`'s own members are in scope inside its
|
* walked and not just the innermost link. An `object`'s own members are in scope
|
||||||
* body under the same `<Owner>.<NAME>` key, so the same walk covers both.
|
* 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
|
* 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
|
* Kotlin's own order: a companion member shadows a same-named top-level
|
||||||
|
|
|
||||||
|
|
@ -761,6 +761,30 @@ class OrderController {
|
||||||
).toEqual(['GET /api/v1/orders']);
|
).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', () => {
|
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
|
// The decoy's PATH ends with the imported FQN, but it declares
|
||||||
// `package x.com.example.app.api` — a different declaration. Choosing the
|
// `package x.com.example.app.api` — a different declaration. Choosing the
|
||||||
|
|
|
||||||
|
|
@ -793,18 +793,23 @@ class Outer {
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
foldKotlinOperands(key, [{ kind: 'ref', name: 'ORDERS' }], repo, ['Inner', 'Outer']),
|
foldKotlinOperands(key, [{ kind: 'ref', name: 'ORDERS' }], repo, ['Outer.Inner', 'Outer']),
|
||||||
).toBe('/inner');
|
).toBe('/inner');
|
||||||
expect(
|
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');
|
).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
|
// `Inner`'s initializer names `P`, which `Inner` does not declare and
|
||||||
// `Outer` does; the scope chain is walked innermost-first, so it means
|
// `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 key = 'src/main/kotlin/com/example/app/api/Nested.kt';
|
||||||
|
const controllerKey = 'src/main/kotlin/com/example/app/web/Controller.kt';
|
||||||
const repo = repoOf({
|
const repo = repoOf({
|
||||||
[key]: `package com.example.app.api
|
[key]: `package com.example.app.api
|
||||||
|
|
||||||
|
|
@ -818,9 +823,15 @@ object Outer {
|
||||||
const val Q = P + "/q"
|
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', () => {
|
it('does not fall through to a file-level constant for an unfoldable sibling', () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue