Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

| 功能 | 说明 |
| --- | --- |
| 跳转到定义 | 类型、方法、构造器、字段、枚举常量、局部变量、参数、类型参数、注解及其属性;按导入、同包、通配导入、嵌套、继承链和调用参数个数解析,支持调用链 `a.b().c` |
| 跳转到定义 | 类型、方法、构造器、字段、枚举常量、局部变量、参数、类型参数、注解及其属性;按导入、同包、通配导入、嵌套、继承链、调用参数个数和实参类型解析,支持调用链 `a.b().c` 和 `Foo.<T>bar()` |
| Lombok | `@Data`/`@Getter`/`@Setter` 生成的访问器跳到字段,`@Builder`、`@Slf4j`、构造器注解跳到注解本身(与 JDT LS 一致) |
| 跳进 JDK | 项目里找不到的名字到本机 JDK 的 `src.zip` 里找,`String`、`List`、`hashCode()` 等都能跳,并能在 JDK 源码里继续跳 |
| 查找引用 | 只列出能跳回这个声明的出现,所以列表和在任一行上跳转的结果一致;`package`/`import` 不算引用 |
Expand Down Expand Up @@ -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.<T>builder()`、限定名注解 `@ExtractorManager.Extractor(...)` 和可变参数参数
`String... keys` 跳不了。

宿主联调:用 Lithe 桌面端固定的 Rust Core(`47c9859`)校验 `plugin.json`、规划 `packaged` 启动、经 Core 启动语言服务器,
会话进入 `ready` 并声明 `definition`、`references`、`documentSymbols`,三种请求都返回正确位置;跳进 JDK 时只在 `${stateDir}`
写入解压副本,插件包文件的 SHA-256 前后一致。ruoyi-vue-pro 跳不了的几乎全是依赖库的注解和它们的属性:
Expand All @@ -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,不一定是项目编译用的版本。
Expand Down
3 changes: 2 additions & 1 deletion docs/releases/v0.0.1.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
0.0.1 是首个版本:不依赖 JDT LS 的轻量 Java 跳转,按插件契约 v0 编写。

功能:
- 跳转到定义:类型、方法、构造器、字段、枚举常量、局部变量、参数、类型参数、注解及其属性;按导入、同包、通配导入、嵌套、继承链和参数个数解析,支持调用链。
- 跳转到定义:类型、方法、构造器、字段、枚举常量、局部变量、参数、类型参数、注解及其属性;按导入、同包、通配导入、嵌套、继承链、参数个数和实参类型解析,支持调用链和 `Foo.<T>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。
Expand Down
13 changes: 12 additions & 1 deletion internal/codenav/UPSTREAM.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<T>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 的进程辅助函数,
Expand Down
189 changes: 189 additions & 0 deletions internal/codenav/jdtls_cases_test.go
Original file line number Diff line number Diff line change
@@ -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<Class<?>> 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 <T> 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<Class<?>> 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<Class<?>>());
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.<String>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)
}
}
}
Loading
Loading