Порівняти коміти

...
Автор SHA1 Повідомлення Дата
l0ng-ai 989a93ace8 fix(refactor): type a chained call by its last call in Introduce Variable
When the code has errors (e.g. a statement without its semicolon), Introduce
Variable falls back to a syntax-based declaration and types it from the hover
of the call the expression ends in. That call was found with a greedy regex,
so in `demo.getList().get(0)` the argument list swallowed `().get(0)` and
the hover of `getList` typed the variable as `List<String>`. The trailing
call is now found by matching its parentheses.

Fixes #2509
2026-11-10 23:30:41 +08:00
3 змінених файлів з 37 додано та 5 видалено

No files matched your search

+6 -5
Переглянути файл
@@ -30,6 +30,7 @@ import {
javaTypeFromHover,
literalType,
replacesBareStatement,
trailingName,
variableDeclaration,
type DeclContext,
type DeclResult,
@@ -378,17 +379,17 @@ function usesSemicolons(doc: Text, statement: SyntaxNode): boolean {
/** Return / declared type of a Java call or name from the server's hover, if it answers. */
async function javaHoverType(p: Prepared, expr: SyntaxNode, text: string): Promise<string | null> {
const m = /([A-Za-z_$][\w$]*)\s*(\([\s\S]*\))?\s*$/.exec(text);
if (!m || !/method_invocation|identifier|field_access/.test(expr.kind)) return null;
const last = trailingName(text);
if (!last || !/method_invocation|identifier|field_access/.test(expr.kind)) return null;
const { from } = rangeToOffsets(p.doc, expr.range);
const pos = p.doc.lineAt(from + m.index);
const at = { line: pos.number - 1, col: from + m.index - pos.from };
const pos = p.doc.lineAt(from + last.index);
const at = { line: pos.number - 1, col: from + last.index - pos.from };
try {
const hover = await Promise.race([
call(commands.lspHover(nextRequestId(), p.ctx.doc, at)),
new Promise<null>((resolve) => setTimeout(() => resolve(null), 1500)),
]);
return hover ? javaTypeFromHover(hover.contents.value, m[1]!) : null;
return hover ? javaTypeFromHover(hover.contents.value, last.name) : null;
} catch {
return null;
}
+10
Переглянути файл
@@ -5,6 +5,7 @@ import {
isLiteral,
javaConstructedType,
javaTypeFromHover,
trailingName,
literalType,
replacesBareStatement,
variableDeclaration,
@@ -152,6 +153,15 @@ describe("templates", () => {
expect(isLiteral("kotlin", "identifier", "true")).toBe(true);
});
it("finds the name a chained expression ends in, for the hover that types it", () => {
expect(trailingName("demo.getList().get(0)")).toEqual({ name: "get", index: 15 });
expect(trailingName("a.b(c(d)).e")).toEqual({ name: "e", index: 10 });
expect(trailingName("names()")).toEqual({ name: "names", index: 0 });
expect(trailingName("list.get(i) ")).toEqual({ name: "get", index: 5 });
expect(trailingName("(a + b)")).toBeNull();
expect(trailingName("x)")).toBeNull();
});
it("reads a Java type from a jdtls hover", () => {
expect(javaTypeFromHover("```java\nString java.lang.String.trim()\n```", "trim")).toBe("String");
expect(javaTypeFromHover("```java\njava.util.List<java.lang.String> a.B.names()\n```", "names")).toBe(
+21
Переглянути файл
@@ -229,6 +229,27 @@ export function javaConstructedType(text: string): string | null {
return obj[1]!.replace(/\s+/g, "");
}
/**
* The name an expression ends in — the last call's method (`a.b().c(0)` → `c`) or the last
* name (`a.b` → `b`) — with its offset in `text`. Null when it ends in something else.
*/
export function trailingName(text: string): { name: string; index: number } | null {
let end = text.trimEnd().length;
if (text[end - 1] === ")") {
// Back to the `(` that opens the trailing argument list.
let depth = 0;
let i = end - 1;
for (; i >= 0; i--) {
if (text[i] === ")") depth++;
else if (text[i] === "(" && --depth === 0) break;
}
if (i < 0) return null;
end = text.slice(0, i).trimEnd().length;
}
const m = /[A-Za-z_$][\w$]*$/.exec(text.slice(0, end));
return m ? { name: m[0], index: m.index } : null;
}
/**
* Type of a Java expression from a jdtls-style hover (```java\nString java.lang.String.trim()\n```
* or `String name`), for a call / name ending in `name`. Null when unclear.