diff --git a/README.md b/README.md index 1c1a98f..09be0a7 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ | 功能 | 说明 | | --- | --- | -| 跳转到定义 | 类型、方法、构造器、字段、枚举常量、局部变量、参数、类型参数、注解及其属性;按导入、同包、通配导入、嵌套、继承链和调用参数个数解析,支持调用链 `a.b().c` | +| 跳转到定义 | 类型、方法、构造器、字段、枚举常量、局部变量、参数、类型参数、注解及其属性;按导入、同包、通配导入、嵌套、继承链、调用参数个数和实参类型解析,支持调用链 `a.b().c` 和 `Foo.bar()` | | Lombok | `@Data`/`@Getter`/`@Setter` 生成的访问器跳到字段,`@Builder`、`@Slf4j`、构造器注解跳到注解本身(与 JDT LS 一致) | | 跳进 JDK | 项目里找不到的名字到本机 JDK 的 `src.zip` 里找,`String`、`List`、`hashCode()` 等都能跳,并能在 JDK 源码里继续跳 | | 查找引用 | 只列出能跳回这个声明的出现,所以列表和在任一行上跳转的结果一致;`package`/`import` 不算引用 | @@ -47,6 +47,30 @@ 查找引用 20 次,p50 约 56 ms,最慢 0.13 s。 +### 和 JDT LS 对比跳转目标 + +在 Nacos 上用同样的取样方式取 3 组、每组 1500 个位置,同时问 JDT LS(Maven 导入完成后)和本插件,比较跳到的文件和行 +(跳进 JDK 的按类比较,因为两边的 JDK 版本和源码不一定相同): + +| 结果 | 种子 2 | 种子 7 | 种子 11 | +| --- | --- | --- | --- | +| 两边跳到同一处(项目内) | 1099 | 1071 | 1086 | +| 两边跳到 JDK 的同一个类 | 326 | 337 | 327 | +| 两边都没有结果 | 8 | 14 | 17 | +| 只有 JDT LS 能跳:依赖库 | 59 | 72 | 64 | +| 只有 JDT LS 能跳:项目内或 JDK | 8 | 3 | 5 | +| 只有本插件能跳 | 0 | 2 | 0 | +| 两边都跳了但不一样 | 0 | 1 | 1 | + +“两边都跳了但不一样”的两处都不是跳错:`org.xml.sax.Attributes` 本身就在 JDK 里(JDT LS 把它当成依赖库), +`Properties.putAll` 是 JDK 9 起才有的重写(这个模块用 JDK 8 编译,JDT LS 跳到 `Hashtable.putAll`,本插件读的是 JDK 17)。 +只有 JDT LS 能跳的项目内位置是 `target/generated-sources` 里 protobuf 生成的类、泛型推断(`list.get(i).getName()`) +和依赖库类型继承来的方法(`e.getMessage()`,`e` 的类型来自 Spring)。 + +对比中发现并已修复的问题:参数个数相同的重载取了第一个(`new AccessException("...")` 跳到了 `AccessException(int)`), +显式类型实参调用 `RestResult.builder()`、限定名注解 `@ExtractorManager.Extractor(...)` 和可变参数参数 +`String... keys` 跳不了。 + 宿主联调:用 Lithe 桌面端固定的 Rust Core(`47c9859`)校验 `plugin.json`、规划 `packaged` 启动、经 Core 启动语言服务器, 会话进入 `ready` 并声明 `definition`、`references`、`documentSymbols`,三种请求都返回正确位置;跳进 JDK 时只在 `${stateDir}` 写入解压副本,插件包文件的 SHA-256 前后一致。ruoyi-vue-pro 跳不了的几乎全是依赖库的注解和它们的属性: @@ -57,7 +81,8 @@ Swagger 的 `@Schema(description, example, requiredMode)`、Lombok 的 `@Data` - **不读依赖库**:Spring、MyBatis、Lombok、Swagger 等依赖里的声明找不到(返回“没有定义”,不会跳错)。 计划:从 Maven/Gradle 插件取 classpath,按需读取 `sources.jar`,没有源码时显示只有签名的存根。 -- **没有类型推断**:重载只按参数个数选择;lambda 参数的类型和泛型替换后的返回类型推不出来, +- **没有类型推断**:重载先按参数个数选,个数相同时按实参里写得出来的类型挑(字面量、强转、`new` 表达式、 + 变量和字段声明的类型、能跟下去的调用链),都看不出时取第一个;lambda 参数的类型和泛型替换后的返回类型推不出来, 例如 `list.get(0).getName()` 里的 `getName`、`users.stream().map(u -> u.getName())` 里的 `getName`; 以字面量为限定符的调用(`"".trim()`)也推不出类型。 - **只认一个 JDK**:用的是上面顺序找到的 JDK,不一定是项目编译用的版本。 diff --git a/docs/releases/v0.0.1.md b/docs/releases/v0.0.1.md index 6541810..ecdfc7e 100644 --- a/docs/releases/v0.0.1.md +++ b/docs/releases/v0.0.1.md @@ -1,11 +1,12 @@ 0.0.1 是首个版本:不依赖 JDT LS 的轻量 Java 跳转,按插件契约 v0 编写。 功能: -- 跳转到定义:类型、方法、构造器、字段、枚举常量、局部变量、参数、类型参数、注解及其属性;按导入、同包、通配导入、嵌套、继承链和参数个数解析,支持调用链。 +- 跳转到定义:类型、方法、构造器、字段、枚举常量、局部变量、参数、类型参数、注解及其属性;按导入、同包、通配导入、嵌套、继承链、参数个数和实参类型解析,支持调用链和 `Foo.bar()`。 - Lombok:访问器跳到字段,`@Builder`、`@Slf4j` 和构造器注解生成的成员跳到注解。 - 跳进本机 JDK 的 `src.zip`,并能在 JDK 源码里继续跳;JDK 源码以只读副本打开,放在插件状态目录里。 - 查找引用:只列出能跳回该声明的出现。 - 文档结构(大纲)。 +- 在 Nacos 上和 JDT LS 对比 4500 个位置,两边都有结果时目标一致(两处差异来自 JDK 版本和 JDK 自带的 `org.xml.sax`)。 - 打开的文件按编辑器里未保存的内容索引。 平台:macOS(Apple Silicon、Intel)、Windows(x64、ARM64),每个平台一个 ZIP。 diff --git a/internal/codenav/UPSTREAM.md b/internal/codenav/UPSTREAM.md index 8061129..44fbc8b 100644 --- a/internal/codenav/UPSTREAM.md +++ b/internal/codenav/UPSTREAM.md @@ -9,10 +9,21 @@ ## 同步规则 -- 除下面列出的文件,本目录的 `.go` 文件与上游**逐字节一致**,不要在这里直接改;需要改解析规则时, +- 除下面列出的文件和改动,本目录的 `.go` 文件与上游**逐字节一致**,不要在这里直接改;需要改解析规则时, 先在 Viff 修(先补一条能失败的测试),再整目录拷回来。 - Lithe 新增的代码放在单独的文件里,文件开头注明“This file is a Lithe addition”: - `symbols.go`:文档结构(`textDocument/documentSymbol`)。 + - `overloads.go`:参数个数相同的重载按实参类型挑选。来自和 JDT LS 在 Nacos 上的对比:唯一跳错的 + 两处都是同参数个数的重载(`new AccessException("...")` 跳到了 `AccessException(int)`)。 + - `qualified.go`:显式类型实参调用 `Foo.bar()` 和限定名注解 `@Outer.Inner(...)`。 + - `jdtls_cases_test.go`:上面几条以及可变参数参数的测试,都来自同一次对比。 +- Viff 还没有发布 codenav,这些只能先在这里改。它们对上游文件的改动只有下面几处, + 上游发布后把这些改动连同上面的新文件提给 Viff,再恢复逐字节一致: + - `parse.go`:`Member` 加 `Params` 字段,解析成员时赋值 `m.Params = p.paramTypes(j, pe)`。 + - `resolve.go`:`findMethod`、`pickCtor`、`methodInScope` 的参数个数换成 `call`(参数个数加左括号位置), + 参数个数相同的候选交给 `bestOverload`;`resolveAt` 和 `typeOfExpr` 的限定符用 `qualifierDot` 找, + 跳过显式类型实参;限定名注解在 `resolveAt` 里按嵌套类型解析(`annotationName`);`findLocal` 认出 + `String... keys` 这样的可变参数参数(原来类型在 `...` 前面时找不到)。 - 同步后运行 `go test ./...`。 `internal/jdk/find.go` 改编自 Viff 的 `internal/platform/jdk.go`(去掉了 Viff 的进程辅助函数, diff --git a/internal/codenav/jdtls_cases_test.go b/internal/codenav/jdtls_cases_test.go new file mode 100644 index 0000000..bde2c32 --- /dev/null +++ b/internal/codenav/jdtls_cases_test.go @@ -0,0 +1,189 @@ +package codenav + +// This file is a Lithe addition: cases found by comparing against JDT LS on +// Nacos where this index answered differently or not at all. + +import ( + "strings" + "testing" +) + +var overloadProject = map[string]string{ + "src/o/Error.java": `package o; + +public class Error { + public Error() {} + public Error(int code) {} + public Error(String msg) {} + public Error(Error cause) {} +} +`, + "src/o/Cause.java": `package o; + +public class Cause extends Error {} +`, + "src/o/Cache.java": `package o; + +import java.util.Set; + +public class Cache { + public void init(String packageName) {} + public void init(Set> classes) {} + public void init(long ttl, boolean lazy) {} + public void init(Object key, boolean lazy) {} + public void put(char c) {} + public void put(double d) {} + public void put(Cache other) {} + public static Cache builder() { return null; } + public static void check(boolean ok, Object message) {} + public static void check(boolean ok, String template, Object... args) {} + public Cache self() { return this; } +} +`, + "src/o/Use.java": `package o; + +import java.util.HashSet; +import java.util.Set; + +class Use { + void run(Set> classes, Cache cache) { + Error a = new Error("user not found!"); + Error b = new Error(404); + Error c = new Error(new Cause()); + Error d = new Error("a" + 1); + cache.init(classes); + cache.init("com.acme"); + cache.init(new HashSet>()); + cache.init(5L, true); + cache.init(cache, false); + cache.put('x'); + cache.put(1.5); + cache.put(-2); + cache.put(cache); + cache.put((Cache) null); + Cache.builder().self(); + Cache.check(classes.isEmpty(), "no classes"); + Cache.check(true, "%s missing", "x"); + } +} +`, +} + +func TestOverloadsWithTheSameArity(t *testing.T) { + ix := NewIndex() + for p, src := range overloadProject { + ix.Put(ParseFile(p, []byte(src))) + } + const use = "src/o/Use.java" + at := func(path, needle string) Location { + src := overloadProject[path] + off := strings.Index(src, needle) + if off < 0 { + t.Fatalf("%q not found in %s", needle, path) + } + return Location{Path: path, Line: strings.Count(src[:off], "\n"), Column: off - (strings.LastIndexByte(src[:off], '\n') + 1)} + } + const errT, cache = "src/o/Error.java", "src/o/Cache.java" + cases := []struct { + name, click string + skip int + want Location + }{ + {"string literal picks the String constructor", `Error("user`, 0, at(errT, "Error(String msg)")}, + {"int literal picks the int constructor", `Error(404)`, 0, at(errT, "Error(int code)")}, + {"subtype argument picks the supertype parameter", `Error(new Cause())`, 0, at(errT, "Error(Error cause)")}, + {"string concatenation is a String", `Error("a" + 1)`, 0, at(errT, "Error(String msg)")}, + {"typed parameter picks the matching overload", `init(classes)`, 0, at(cache, "init(Set")}, + {"string literal picks String over Set", `init("com`, 0, at(cache, "init(String")}, + {"new expression type", `init(new HashSet`, 0, at(cache, "init(Set")}, + {"long literal", `init(5L`, 0, at(cache, "init(long")}, + {"reference argument goes to Object", `init(cache, false)`, 0, at(cache, "init(Object")}, + {"char literal", `put('x')`, 0, at(cache, "put(char")}, + {"double literal", `put(1.5)`, 0, at(cache, "put(double")}, + {"negative int widens to double, not char", `put(-2)`, 0, at(cache, "put(double")}, + {"project type", `put(cache)`, 0, at(cache, "put(Cache")}, + {"cast", `put((Cache)`, 0, at(cache, "put(Cache")}, + {"explicit type arguments", `builder()`, 0, at(cache, "builder()")}, + {"a fixed-arity overload beats varargs", `check(classes`, 0, at(cache, "check(boolean ok, Object")}, + {"varargs when the fixed one cannot take the arguments", `check(true`, 0, at(cache, "check(boolean ok, String")}, + {"call after explicit type arguments", `self();`, 0, at(cache, "self()")}, + } + src := []byte(overloadProject[use]) + for _, c := range cases { + off := strings.Index(string(src), c.click) + if off < 0 { + t.Fatalf("%s: %q not found", c.name, c.click) + } + got, ok := ix.Resolve(use, src, off+c.skip) + if !ok { + t.Errorf("%s: no declaration, want %v", c.name, c.want) + continue + } + if got.Path != c.want.Path || got.Line != c.want.Line || got.Column != c.want.Column { + t.Errorf("%s: got %v, want %v", c.name, got, c.want) + } + } +} + +var jdtlsCaseProject = map[string]string{ + "src/q/Manager.java": `package q; + +public class Manager { + public @interface Extractor { + Class rpc(); + } +} +`, + "src/q/Handler.java": `package q; + +class Handler { + @Manager.Extractor(rpc = Handler.class) + void handle() {} + + static void check(Class raw, final Type... typeArguments) { + if (typeArguments.length != 0) { + return; + } + } + + byte[] key(String... keys) { return keys[0].getBytes(); } +} +`, +} + +func TestJDTLSCases(t *testing.T) { + ix := NewIndex() + for p, src := range jdtlsCaseProject { + ix.Put(ParseFile(p, []byte(src))) + } + const h = "src/q/Handler.java" + at := func(path, needle string, nth int) Location { + src := jdtlsCaseProject[path] + off := offsetOf(t, src, needle, nth, 0) + return Location{Path: path, Line: strings.Count(src[:off], "\n"), Column: off - (strings.LastIndexByte(src[:off], '\n') + 1)} + } + cases := []struct { + name, click string + nth int + want Location + }{ + {"qualified annotation", "Extractor(", 0, at("src/q/Manager.java", "Extractor {", 0)}, + {"attribute of a qualified annotation", "rpc =", 0, at("src/q/Manager.java", "rpc()", 0)}, + {"varargs parameter declaration", "typeArguments)", 0, at(h, "typeArguments)", 0)}, + {"varargs parameter use", "typeArguments.length", 0, at(h, "typeArguments)", 0)}, + {"varargs parameter after a plain type", "keys)", 0, at(h, "keys)", 0)}, + {"varargs parameter indexed", "keys[0]", 0, at(h, "keys)", 0)}, + } + src := []byte(jdtlsCaseProject[h]) + for _, c := range cases { + off := offsetOf(t, string(src), c.click, c.nth, 0) + got, ok := ix.Resolve(h, src, off) + if !ok { + t.Errorf("%s: no declaration, want %v", c.name, c.want) + continue + } + if got.Path != c.want.Path || got.Line != c.want.Line || got.Column != c.want.Column { + t.Errorf("%s: got %v, want %v", c.name, got, c.want) + } + } +} diff --git a/internal/codenav/overloads.go b/internal/codenav/overloads.go new file mode 100644 index 0000000..c5458b8 --- /dev/null +++ b/internal/codenav/overloads.go @@ -0,0 +1,360 @@ +package codenav + +// This file is a Lithe addition. Comparing against JDT LS on Nacos showed the +// only wrong jumps came from overloads with the same number of parameters +// (Error(int) vs Error(String)): the first one by arity was taken. The index +// keeps each parameter's simple type name, and a call site with several +// candidates scores them against what the arguments are known to be. Nothing +// is inferred beyond what is written: literals, casts, `new` expressions, +// declared types of locals and fields, and the expression types the resolver +// already follows. A tie keeps the first candidate, as before. + +import "strings" + +// call is a call site: its argument count (-1 when unknown) and the token of +// its '(' (-1 when there is none, such as a method reference). +type call struct{ ar, open int } + +var noCall = call{ar: -1, open: -1} + +func (q *qctx) callAt(open int) call { + if !q.p.is(open, "(") { + return noCall + } + return call{ar: q.callArity(open), open: open} +} + +// paramTypes returns the simple type names of the parameters between the +// parentheses at open and end (exclusive), generics and dims stripped, +// joined by commas and interned: members with the same signature share one +// string, which keeps the index small. +func (p *parser) paramTypes(open, end int) string { + if end-open <= 2 { + return "" + } + var out []byte + s, depth, angle := open+1, 0, 0 + for k := open + 1; k < end; k++ { + switch { + case k == end-1: + case p.is(k, "(") || p.is(k, "[") || p.is(k, "{"): + depth++ + continue + case p.is(k, ")") || p.is(k, "]") || p.is(k, "}"): + depth-- + continue + case p.is(k, "<") && p.ident(k-1): + angle++ + continue + case p.is(k, ">") && angle > 0: + angle-- + continue + case p.is(k, ",") && depth == 0 && angle == 0: + default: + continue + } + name := k - 1 + for name > s && !p.ident(name) { // String args[] + name-- + } + if len(out) > 0 { + out = append(out, ',') + } + out = append(out, simpleName(p.typeText(s, name))...) + s = k + 1 + } + return p.in.s(out) +} + +func simpleName(t string) string { + return t[strings.LastIndexByte(t, '.')+1:] +} + +// argType is what an argument is known to be. name is "" when unknown; t is +// set when the type was resolved; prim names a primitive; null is the +// literal null. +type argType struct { + name string + t *Type + prim string + null bool +} + +func (q *qctx) argTypes(open int) []argType { + p := q.p + end := p.skipBalanced(open) + if end-open <= 2 { + return nil + } + ctx := q.ctxType(open) + var out []argType + s := open + 1 + for k := open + 1; k < end; k++ { + switch { + case k == end-1: + case p.is(k, "(") || p.is(k, "[") || p.is(k, "{"): + k = p.skipBalanced(k) - 1 + continue + case p.is(k, "<") && (p.is(k-1, ".") || p.ident(k-1) && isUpper(p.src[p.toks[k-1].S])): + if e := p.skipBalanced(k); p.is(e-1, ">") && e <= end-1 { + k = e - 1 + } + continue + case p.is(k, ","): + default: + continue + } + out = append(out, q.argTypeOf(s, k, ctx)) + s = k + 1 + } + return out +} + +func (q *qctx) argTypeOf(s, e int, ctx *Type) argType { + p := q.p + if s >= e { + return argType{} + } + if p.is(s, "-") || p.is(s, "+") { // -1, +2.0 + if e-s == 2 && p.toks[s+1].Kind == tNumber { + s++ + } + } + if e-s == 1 { + switch tok := p.toks[s]; { + case tok.Kind == tString: + return argType{name: "String"} + case tok.Kind == tChar: + return argType{prim: "char"} + case tok.Kind == tNumber: + return argType{prim: numberType(p.txt(s))} + } + switch p.txt(s) { + case "true", "false": + return argType{prim: "boolean"} + case "null": + return argType{null: true} + } + } + // Concatenation with a string literal at the top level is a String. + for k := s; k < e; k++ { + if p.is(k, "(") || p.is(k, "[") || p.is(k, "{") { + k = p.skipBalanced(k) - 1 + continue + } + if p.is(k, "<") && (p.is(k-1, ".") || p.ident(k-1) && isUpper(p.src[p.toks[k-1].S])) { + if ge := p.skipBalanced(k); p.is(ge-1, ">") && ge <= e { + k = ge - 1 + continue + } + } + if p.is(k, "+") && (p.toks[k-1].Kind == tString || k+1 < e && p.toks[k+1].Kind == tString) { + return argType{name: "String"} + } + if p.is(k, "?") || p.is(k, "->") || p.is(k, "=") || p.is(k, "==") { + return argType{} // conditional, lambda, comparison: not followed + } + } + if e-s >= 3 && p.is(e-1, "class") && p.is(e-2, ".") { + return argType{name: "Class"} + } + // A cast: (Foo) x, (int) y. + if p.is(s, "(") { + if ce := p.skipBalanced(s); ce < e && q.typeStartBack(ce-2) == s+1 && (p.ident(ce) || p.is(ce, "(") || p.toks[ce].Kind != tPunct) { + name := simpleName(p.typeText(s+1, ce-1)) + if primitives[name] { + return argType{prim: name} + } + return argType{name: name, t: q.resolveText(p.typeText(s+1, ce-1), q.file, ctx)} + } + } + if t := q.typeOfExpr(e-1, ctx); t != nil { + return argType{name: t.Name, t: t} + } + // Names whose declared type is written but not in the index (JDK or + // library types without sources). + if p.is(s, "new") && p.ident(s+1) { + k := s + 1 + for p.is(k+1, ".") && p.ident(k+2) { + k += 2 + } + if name := p.txt(k); !primitives[name] && (p.is(k+1, "(") || p.is(k+1, "<")) { + return argType{name: name} + } + return argType{} + } + if e-s == 1 && p.ident(s) { + name := p.txt(s) + if l, ok := q.findLocal(name, s); ok && l.typeS >= 0 { + if text := p.typeText(l.typeS, l.typ); text != "var" { + if tn := simpleName(text); primitives[tn] { + return argType{prim: tn} + } else { + return argType{name: tn} + } + } + return argType{} + } + if m := q.fieldInScope(name, ctx); m != nil && m.Type != "" { + if tn := simpleName(m.Type); primitives[tn] { + return argType{prim: tn} + } else { + return argType{name: tn} + } + } + } + return argType{} +} + +func numberType(lit string) string { + l := strings.ToLower(strings.ReplaceAll(lit, "_", "")) + switch { + case strings.HasSuffix(l, "l"): + return "long" + case strings.HasPrefix(l, "0x") || strings.HasPrefix(l, "0b"): + return "int" + case strings.HasSuffix(l, "f"): + return "float" + case strings.HasSuffix(l, "d") || strings.ContainsAny(l, ".e"): + return "double" + } + return "int" +} + +var boxes = map[string]string{"boolean": "Boolean", "byte": "Byte", "char": "Character", "short": "Short", + "int": "Integer", "long": "Long", "float": "Float", "double": "Double"} + +var widens = map[string]string{"byte": "short int long float double", "short": "int long float double", + "char": "int long float double", "int": "long float double", "long": "float double", "float": "double"} + +// finalTypes are JDK types nothing else extends, with what they can be +// passed as besides themselves. +var finalTypes = map[string]string{ + "String": "Object CharSequence Comparable Serializable Constable ConstantDesc", + "Class": "Object Serializable Type GenericDeclaration AnnotatedElement", + "Boolean": "Object Comparable Serializable Constable", + "Character": "Object Comparable Serializable Constable", + "Byte": "Object Number Comparable Serializable Constable", + "Short": "Object Number Comparable Serializable Constable", + "Integer": "Object Number Comparable Serializable Constable ConstantDesc", + "Long": "Object Number Comparable Serializable Constable ConstantDesc", + "Float": "Object Number Comparable Serializable Constable ConstantDesc", + "Double": "Object Number Comparable Serializable Constable ConstantDesc", +} + +func hasWord(list, w string) bool { + for _, f := range strings.Fields(list) { + if f == w { + return true + } + } + return false +} + +// typeVar reports a parameter type that is most likely a type variable (T, E). +func typeVar(p string) bool { + return len(p) == 1 && isUpper(p[0]) +} + +// fit scores passing a to a parameter of type param: 3 exact, 2 by +// widening, boxing or subtyping, 1 as Object or another JDK supertype, 0 +// unknown, negative when it cannot be passed. +func (q *qctx) fit(a argType, param string) int { + switch { + case typeVar(param): + return 0 + case a.null: + if primitives[param] { + return -2 + } + return 0 + case a.prim != "": + switch { + case param == a.prim: + return 3 + case hasWord(widens[a.prim], param), param == boxes[a.prim]: + return 2 + case primitives[param]: + return -2 + case param == "Object" || param == "Serializable" || param == "Comparable" || + param == "Number" && a.prim != "boolean" && a.prim != "char": + return 1 + } + return -2 + case a.name == "": + return 0 + case a.name == param: + return 3 + } + if primitives[param] { + for prim, box := range boxes { + if box == a.name && (prim == param || hasWord(widens[prim], param)) { + return 2 + } + } + return -2 + } + if supers, ok := finalTypes[a.name]; ok { + if hasWord(supers, param) { + return 1 + } + return -2 + } + if _, ok := finalTypes[param]; ok { + return -2 // nothing but the type itself is a String or an Integer + } + if a.t != nil { + for _, h := range q.hierarchy(a.t)[1:] { + if h.Name == param { + return 2 + } + } + if param == "Object" { + return 1 + } + return -1 // the hierarchy may leave the index through a library type + } + if param == "Object" { + return 1 + } + return 0 +} + +// bestOverload picks the candidate that fits the arguments at c best. As in +// Java, a candidate every argument can be passed to without varargs +// expansion wins over one that needs it; within a group the highest score +// wins and the first one wins a tie. +func (q *qctx) bestOverload(ms []*Member, c call) *Member { + if len(ms) == 1 || c.open < 0 { + return ms[0] + } + args := q.argTypes(c.open) + rank := func(m *Member) (group, score int) { + ok := true + params := strings.Split(m.Params, ",") + for k, a := range args { + if m.Params == "" { + break + } + pk := min(k, len(params)-1) // varargs elements + f := q.fit(a, params[pk]) + ok = ok && f >= 0 + score += f + } + switch { + case ok && !m.Varargs: + return 2, score + case ok: + return 1, score + } + return 0, score + } + best := ms[0] + bg, bs := rank(best) + for _, m := range ms[1:] { + if g, s := rank(m); g > bg || g == bg && s > bs { + best, bg, bs = m, g, s + } + } + return best +} diff --git a/internal/codenav/parse.go b/internal/codenav/parse.go index e28ba04..e50f0bb 100644 --- a/internal/codenav/parse.go +++ b/internal/codenav/parse.go @@ -78,6 +78,7 @@ type Member struct { Type string // declared/return type, generics and dims stripped; "" for ctors Arity int16 Varargs bool + Params string // parameters' simple type names, comma-separated (Lithe addition, see overloads.go) Owner *Type Line, Col int32 Lombok uint8 // field-level @Getter/@Setter @@ -451,6 +452,7 @@ func (p *parser) parseBody(i int, owner *Type, enum bool) int { } pe := p.skipBalanced(j) m.Arity, m.Varargs = p.arity(j, pe) + m.Params = p.paramTypes(j, pe) owner.Members = append(owner.Members, m) k := pe sawDefault := false diff --git a/internal/codenav/qualified.go b/internal/codenav/qualified.go new file mode 100644 index 0000000..65b18c2 --- /dev/null +++ b/internal/codenav/qualified.go @@ -0,0 +1,51 @@ +package codenav + +// This file is a Lithe addition: qualified names the upstream resolver did +// not follow, found by comparing against JDT LS on Nacos. + +// qualifierDot returns the '.' or '::' that qualifies the name at ti, +// looking past explicit type arguments (Foo.bar(), Foo::bar), or -1. +func (q *qctx) qualifierDot(ti int) int { + p := q.p + i := ti - 1 + if p.is(i, ">") { + d := 0 + for ; i >= 0; i-- { + switch { + case p.is(i, ">"): + d++ + case p.is(i, "<"): + d-- + case p.ident(i) || p.is(i, ".") || p.is(i, ",") || p.is(i, "?") || p.is(i, "[") || p.is(i, "]") || p.is(i, "&") || p.is(i, "@"): + default: + return -1 + } + if d == 0 { + break + } + } + i-- + } + if p.is(i, ".") || p.is(i, "::") { + return i + } + return -1 +} + +// annotationName reports that the name after dot ends an annotation's +// qualified type name, as Inner in @Outer.Inner(...). +func (q *qctx) annotationName(dot int) bool { + p := q.p + if !p.is(dot, ".") { + return false + } + for k := dot - 1; p.ident(k); k -= 2 { + if p.is(k-1, "@") { + return true + } + if !p.is(k-1, ".") { + return false + } + } + return false +} diff --git a/internal/codenav/resolve.go b/internal/codenav/resolve.go index 8ab53aa..53b53bf 100644 --- a/internal/codenav/resolve.go +++ b/internal/codenav/resolve.go @@ -215,7 +215,7 @@ func (q *qctx) resolveAt(ti int) []hit { if at == nil { return nil } - return q.mloc(q.findMethod(at, name, 0), "annotation-attr") + return q.mloc(q.findMethod(at, name, call{ar: 0, open: -1}), "annotation-attr") } } if p.is(ti-1, "case") || p.is(ti-1, ",") && q.inCaseLabel(ti) { @@ -225,10 +225,11 @@ func (q *qctx) resolveAt(ti int) []hit { } } } + dot := q.qualifierDot(ti) switch { - case p.is(ti-1, ".") || p.is(ti-1, "::"): - ref := p.is(ti-1, "::") - ot := q.typeOfExpr(ti-2, ctx) + case dot >= 0: + ref := p.is(dot, "::") + ot := q.typeOfExpr(dot-1, ctx) if ot == nil { // A package-qualified type name: com.foo.Bar if t := q.qualifiedType(ti); t != nil { @@ -236,23 +237,26 @@ func (q *qctx) resolveAt(ti int) []hit { } return nil } + if q.annotationName(dot) { // @Outer.Inner (Lithe addition) + return q.loc(q.findNested(ot, name), "annotation") + } if isCall && q.afterNew(ti) { - if c := q.pickCtor(ot, q.callArity(ti+1)); c != nil && ot.Name == name { + if c := q.pickCtor(ot, q.callAt(ti+1)); c != nil && ot.Name == name { return q.mloc(c, "ctor") } if t := q.findNested(ot, name); t != nil { - if c := q.pickCtor(t, q.callArity(ti+1)); c != nil { + if c := q.pickCtor(t, q.callAt(ti+1)); c != nil { return q.mloc(c, "ctor") } return q.loc(t, "new-type") } } if isCall || ref { - ar := -1 + c := noCall if isCall { - ar = q.callArity(ti + 1) + c = q.callAt(ti + 1) } - if m := q.findMethod(ot, name, ar); m != nil { + if m := q.findMethod(ot, name, c); m != nil { return q.mloc(m, "member-call") } return nil @@ -274,13 +278,13 @@ func (q *qctx) resolveAt(ti int) []hit { if p.is(open, "<") { open = p.skipBalanced(open) } - if c := q.pickCtor(t, q.callArity(open)); c != nil { + if c := q.pickCtor(t, q.callAt(open)); c != nil { return q.mloc(c, "ctor") } } return q.loc(t, "new-type") case isCall: - if m := q.methodInScope(name, q.callArity(ti+1), ctx); m != nil { + if m := q.methodInScope(name, q.callAt(ti+1), ctx); m != nil { return q.mloc(m, "call") } return nil @@ -637,7 +641,8 @@ func (q *qctx) builderOf(t *Type) *Type { return b } -func (q *qctx) findMethod(t *Type, name string, ar int) *Member { +func (q *qctx) findMethod(t *Type, name string, c call) *Member { + ar := c.ar // Members of a synthetic @Builder type: one setter per field, and build(). if t.Outer != nil && q.builders[t.Outer] == t { owner := t.Outer @@ -650,12 +655,13 @@ func (q *qctx) findMethod(t *Type, name string, ar int) *Member { return nil } var first *Member + var fits []*Member h := q.hierarchy(t) for _, ht := range h { for _, m := range ht.Members { if m.Kind == kMethod && m.Name == name { if arityOK(m, ar) { - return m + fits = append(fits, m) } if first == nil { first = m @@ -663,6 +669,9 @@ func (q *qctx) findMethod(t *Type, name string, ar int) *Member { } } } + if len(fits) > 0 { + return q.bestOverload(fits, c) + } if first != nil { return first } @@ -721,18 +730,23 @@ func (q *qctx) findField(t *Type, name string) *Member { return nil } -func (q *qctx) pickCtor(t *Type, ar int) *Member { +func (q *qctx) pickCtor(t *Type, c call) *Member { + ar := c.ar var first *Member + var fits []*Member for _, m := range t.Members { if m.Kind == kCtor { if arityOK(m, ar) { - return m + fits = append(fits, m) } if first == nil { first = m } } } + if len(fits) > 0 { + return q.bestOverload(fits, c) + } if first != nil { return first } @@ -765,14 +779,14 @@ func (q *qctx) staticOwners(name string) []*Type { return out } -func (q *qctx) methodInScope(name string, ar int, ctx *Type) *Member { +func (q *qctx) methodInScope(name string, c call, ctx *Type) *Member { for t := ctx; t != nil; t = t.Outer { - if m := q.findMethod(t, name, ar); m != nil { + if m := q.findMethod(t, name, c); m != nil { return m } } for _, t := range q.staticOwners(name) { - if m := q.findMethod(t, name, ar); m != nil { + if m := q.findMethod(t, name, c); m != nil { return m } } @@ -928,14 +942,14 @@ func (q *qctx) typeOfExpr(end int, ctx *Type) *Type { if nm == "this" || nm == "super" || javaKeywords[nm] && nm != "this" { return nil } - ar := q.callArity(open) + c := q.callAt(open) var m *Member - if p.is(before-1, ".") { - if ot := q.typeOfExpr(before-2, ctx); ot != nil { - m = q.findMethod(ot, nm, ar) + if dot := q.qualifierDot(before); dot >= 0 { + if ot := q.typeOfExpr(dot-1, ctx); ot != nil { + m = q.findMethod(ot, nm, c) } } else { - m = q.methodInScope(nm, ar, ctx) + m = q.methodInScope(nm, c, ctx) } if m == nil { return nil @@ -1104,8 +1118,12 @@ func (q *qctx) findLocal(name string, at int) (local, bool) { if !ok { continue } - if ts := q.typeStartBack(k - 1); ts >= 0 { - add(k, ts, k) + te := k + if p.is(k-1, "...") { // varargs parameter (Lithe addition) + te = k - 1 + } + if ts := q.typeStartBack(te - 1); ts >= 0 { + add(k, ts, te) } } for i := len(decls) - 1; i >= 0; i-- {