惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
News and Events Feed by Topic
爱范儿
爱范儿
Blog — PlanetScale
Blog — PlanetScale
The GitHub Blog
The GitHub Blog
C
Check Point Blog
小众软件
小众软件
I
InfoQ
罗磊的独立博客
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Engineering at Meta
Engineering at Meta
酷 壳 – CoolShell
酷 壳 – CoolShell
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Hugging Face - Blog
Hugging Face - Blog
博客园 - 三生石上(FineUI控件)
MyScale Blog
MyScale Blog
The Cloudflare Blog
Last Week in AI
Last Week in AI
腾讯CDC
Y
Y Combinator Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
雷峰网
雷峰网
B
Blog
T
Tailwind CSS Blog
MongoDB | Blog
MongoDB | Blog
A
About on SuperTechFans
D
Docker
博客园 - 司徒正美
博客园_首页
Recent Announcements
Recent Announcements
D
DataBreaches.Net
阮一峰的网络日志
阮一峰的网络日志
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
G
Google Developers Blog
Microsoft Security Blog
Microsoft Security Blog
F
Fortinet All Blogs
Stack Overflow Blog
Stack Overflow Blog
aimingoo的专栏
aimingoo的专栏
N
Netflix TechBlog - Medium
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 聂微东
GbyAI
GbyAI
Jina AI
Jina AI
V
V2EX
Vercel News
Vercel News
IT之家
IT之家
WordPress大学
WordPress大学
M
MIT News - Artificial intelligence
NISL@THU
NISL@THU
V
Visual Studio Blog
C
Cybersecurity and Infrastructure Security Agency CISA

重归混沌的BLOG

给silly实现了一个ernro模块 | 重归混沌的BLOG 给silly实现了一个ernro模块 | 重归混沌的BLOG 第一次在生产环境使用 Vibe Coding | 重归混沌的BLOG 第一次在生产环境使用 Vibe Coding | 重归混沌的BLOG API 设计的艰难抉择 | 重归混沌的BLOG API 设计的艰难抉择 | 重归混沌的BLOG 十年 | 重归混沌的BLOG 十年 | 重归混沌的BLOG 在Go语言中如何使XML加载内存无限趋近于0 | 重归混沌的BLOG 在Go语言中如何使XML加载内存无限趋近于0 | 重归混沌的BLOG 对跨服玩法中的分布式一致性问题进行简单抽象 | 重归混沌的BLOG 对跨服玩法中的分布式一致性问题进行简单抽象 | 重归混沌的BLOG Go语言逃逸分析之slice和map | 重归混沌的BLOG Go语言逃逸分析之slice和map | 重归混沌的BLOG 谈谈观测 | 重归混沌的BLOG 谈谈观测 | 重归混沌的BLOG 写了个AI Agent服务端 | 重归混沌的BLOG 写了个AI Agent服务端 | 重归混沌的BLOG 谈谈代码设计中“严丝合缝” | 重归混沌的BLOG 谈谈代码设计中“严丝合缝” | 重归混沌的BLOG 一次艰难的线上游戏服务器内存排查经历 | 重归混沌的BLOG 一次艰难的线上游戏服务器内存排查经历 谈谈游戏服务器中RPC模块的设计 谈谈游戏服务器代码抽象 最近碰到的一个分布式一致性问题 谈谈游戏服务器的自动化测试 对Raft协议的一点理解 使用mmap来学习/proc/pid/smaps 2023(完) 再次实现了一个Lua性能分析器 终于给Silly的定时器增加了取消功能 一次虚拟内存排查经历 游戏服务器分布式数据的一种同步的思路 为silly增加了互斥锁 2022(完) Go语言之闭包篇 一例误用unsafe包引起的内存问题 Go语言之内存篇 初识Go语言 重新抽象图形API 给Lua实现了一个数学库 谈谈跨平台图形API的抽象 寻路和Flocking算法的结合 行为树的一种高效实现 内测过程中Shader出现的问题 彻底解决多国语言 谈谈数据库的选型 再谈Lua热更新(终) 初窥Rust 关于游戏服务器的服务拆分 ECS的初步实现 ECS初探 屏幕空间(SreenSpace)的想象力 一些对辐射度量学的理解 深度缓冲和半透明渲染 Mysql的间隙锁 更新一些GPU相关知识 2020 地形渲染之爬过的坑 Lua5.3 GC源码阅读(5) 实现一个数据库存储队列 再学计算机图形学入门 再谈分布式服务架构 游戏上线一个月后的反思 一次并发Bug 双向链表的三种实现 谈谈随机数的使用 再谈性能优化 2019 Lua中的函数式编程 重构登录逻辑 Unity资源管理(续) 谈谈Unity的资源管理 一次关于Cache的性能分析 历史之2018 DC3算法 移动平台native代码遭遇的坑 从CPU层面谈谈优化 开卷有益(UNIX编程艺术篇) GC竞争问题 通过Mesh投影来实现贴花系统 谈谈我对数据同步的理解 又一个类型提升引起的Bug Lua5.3 GC源码阅读(4) Lua5.3 GC源码阅读(3) Lua5.3 GC源码阅读(2) Lua5.3 GC源码阅读(1) 三角形光栅化时遇到的坑 一次git事故 再见2017 又一个lua调试器 客户端缓存落地方案 Paxos算法 HTTP服务器的特点 一次性能优化经历 关于CPU分支预测 C程序中让两个不同版本的库共存 实现了一个AOI模块 一个高可伸缩的游戏服务器架构 关于网络协议封装的一些新想法
如何基于LanguageServerProtocol来编写lint工具
重归混沌 · 2024-12-18 · via 重归混沌的BLOG

TLDR: 本文并没有任何设计思想,只是为编写基于LSP协议的自定义lint工具留下一丝线索,以便未来使用。

在最近这两年里,我断断续续在项目内写过不少针对项目内go代码的lint工具和代码生成器。

每次在编写lint工具时,就会发现如果能有语义的辅助就可以将lint做得更完美。

然而语义分析的复杂度和开发成本都太高了,所以每次最终都做成了基于AST的简易lint工具。

我时常在想,其实我不需要开发复杂的语义分析功能,只要我可以像vs-code那样,可以查找所有实现, 查找函数定义, 查找符号的所有引用, 我们编写出的lint工具的上限就会大大提高。

幸运的是我们只要编写一个headless的LSP客户端,便可以拥有vs-code中所有关于语义的能力。

这个想法其实我已经萌生了一年多,但一直没有动力和时间去尝试。

直到最近,我终于下定决心尝试一下。

虽然LSP有完整完善的文档,但是有时太全了也是一种负担。

因此这里还是记录一下常用的几个textDocument请求参数:

  1. textDocument/didOpen:
  • 用处: 当一个文件被打开时,通知gopls来初始化对该文件的语言服务支持。这包括加载文件内容,进行语法检查,并准备相关的编辑操作支持。
  • 参数:
    {
    "textDocument": {
    "uri": "file:///path/to/your/file.go",
    "languageId": "go",
    "version": 1,
    "text": "package main\n\nfunc main() {\n    // your code here\n}"
    }
    }
  1. textDocument/didChange:

    • 用处: 当文件内容发生变化时,通知gopls来更新对该文件的语言服务支持。这包括处理文本的增量变化,如添加、删除和修改代码行。
    • 参数:
      {
      "textDocument": {
      "uri": "file:///path/to/your/file.go",
      "version": 2
      },
      "contentChanges": [
      {
      "range": {
      "start": { "line": 2, "character": 0 },
      "end": { "line": 2, "character": 0 }
      },
      "text": "    fmt.Println(\"Hello, world!\")"
      }
      ]
      }
  2. textDocument/didClose:

  • 用处: 当一个文件被关闭时,通知gopls来释放对该文件的语言服务支持。这有助于释放资源并清理掉不再使用的文档信息。
    {
    "textDocument": {
    "uri": "file:///path/to/your/file.go"
    }
    }
  1. textDocument/hover:

    • 用处: 请求文档中的某个位置的悬停信息,通常用于显示鼠标悬停在一个标识符上时的相关帮助内容,如类型信息、文档注释等。
      {
      "textDocument": {
      "uri": "file:///path/to/your/file.go"
      },
      "position": {
      "line": 3,
      "character": 5
      }
      }
  2. textDocument/signatureHelp:

    • 用处: 请求函数签名的帮助信息,当光标停在一个函数的定义上时,会返回函数签名的详细信息和参数描述。
      {
      "textDocument": {
      "uri": "file:///path/to/your/file.go"
      },
      "position": {
      "line": 5,
      "character": 10
      }
      }
  3. textDocument/documentSymbol:

    • 用处: 获取文件中的所有符号信息,包括函数、类型定义等,这对于导航代码结构非常有帮助。
      {
      "textDocument": {
      "uri": "file:///path/to/your/file.go"
      }
      }
  4. textDocument/references:

    • 用处: 查找文档中某个标识符的引用,以帮助开发者了解代码中的使用情况。
      {
      "textDocument": {
      "uri": "file:///path/to/your/file.go"
      },
      "position": {
      "line": 7,
      "character": 0
      }
      }
  5. textDocument/definition:

    • 用处: 请求一个标识符的定义位置,以便快速导航到其实现位置。
      {
      "textDocument": {
      "uri": "file:///path/to/your/file.go"
      },
      "position": {
      "line": 8,
      "character": 5
      }
      }
  6. textDocument/codeAction:

    • 用处: 提供可供用户选择的代码修复建议,如重构、添加注释、优化等。
      {
      "textDocument": {
      "uri": "file:///path/to/your/file.go"
      },
      "range": {
      "start": { "line": 10, "character": 0 },
      "end": { "line": 10, "character": 5 }
      },
      "context": {
      "diagnostics": []
      }
      }
  7. textDocument/codeLens:

    • 用处: 请求代码镜头(Code Lens)信息,如测试覆盖率、TODO 标记等,这些信息直接显示在代码行的旁边。
      {
      "textDocument": {
      "uri": "file:///path/to/your/file.go"
      }
      }
  8. textDocument/documentFormatting:

    • 用处: 对文档进行格式化,以符合编程语言的规范。例如,调整缩进、空格、行末等。
      参数:
      {
      "textDocument": {
      "uri": "file:///path/to/your/file.go"
      },
      "options": {
      "tabSize": 4,
      "insertSpaces": true
      }
      }

下面我将使用上述textDocument请求,编写一个简易的headless客户端,来查找某个函数的所有引用。

首先确保已安装 Go 语言并配置好环境。然后安装 gopls

go install golang.org/x/tools/gopls@latest

在这个工具中,我们使用gopls来查询所有调用了rpc.Call的函数体。

代码如下:

package main

import (
    "context"
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "os"
    "os/exec"
    "path/filepath"
    "strings"

    "github.com/sourcegraph/jsonrpc2"
)

type SymbolKind int
type SymbolTag int

type DocumentSymbol struct {
    Kind  SymbolKind `json:"kind"`
        Name  string `json:"name"`
        Location Location `json:"location"`
}

type Location struct {
    URI   string `json:"uri"`
    Range Range  `json:"range"`
}

type Range struct {
    Start Position `json:"start"`
    End   Position `json:"end"`
}

type Position struct {
    Line      int `json:"line"`
    Character int `json:"character"`
}

type LSPClient struct {
    cmd     *exec.Cmd
    conn    *jsonrpc2.Conn
    rootURI string
}

type stdioReadWriteCloser struct {
    stdin io.WriteCloser
    stdout  io.ReadCloser
}

var _ io.ReadWriteCloser = (*stdioReadWriteCloser)(nil)

func (c stdioReadWriteCloser) Read(p []byte) (n int, err error) {
    n, err = c.stdout.Read(p)
    return n, err
}

func (c stdioReadWriteCloser) Write(p []byte) (n int, err error) {
    return c.stdin.Write(p)
}

func (c stdioReadWriteCloser) Close() error {
    return nil
}

// 简单的handler实现
type handler struct{}

func (h *handler) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) {
    if req.Method == "workspace/configuration" {
        // 返回空数组表示不支持配置
        conn.Reply(ctx, req.ID, []interface{}{})
        return
    }
    conn.Reply(ctx, req.ID, []interface{}{})
    // 可以在这里处理服务器推送的消息
}

func newLSPClient(projectPath string) (*LSPClient, error) {
    // 启动gopls作为语言服务器
    cmd := exec.Command("gopls", "serve")
    cmd.Stderr = os.Stderr

    // 创建管道用于JSON-RPC通信
    stdin, err := cmd.StdinPipe()
    if err != nil {
        return nil, fmt.Errorf("创建stdin管道失败: %v", err)
    }
    stdout, err := cmd.StdoutPipe()
    if err != nil {
        return nil, fmt.Errorf("创建stdout管道失败: %v", err)
    }

    // 启动gopls服务器
    if err := cmd.Start(); err != nil {
        return nil, fmt.Errorf("启动gopls服务器失败: %v", err)
    }

    // 创建JSON-RPC连接
    // 创建连接,传入完整的参数
    conn := jsonrpc2.NewConn(
        context.Background(),
        jsonrpc2.NewBufferedStream(stdioReadWriteCloser{
            stdin:  stdin,
            stdout: stdout,
        }, jsonrpc2.VSCodeObjectCodec{}),
        &handler{},
    )

    // 转换项目路径为URI
    absPath, err := filepath.Abs(projectPath)
    if err != nil {
        return nil, fmt.Errorf("获取绝对路径失败: %v", err)
    }
    rootURI := fmt.Sprintf("%s", absPath)

    return &LSPClient{
        cmd:     cmd,
        conn:    conn,
        rootURI: rootURI,
    }, nil
}
func (c *LSPClient) initialize() error {
    params := map[string]interface{}{
        "processId": os.Getpid(),
        "clientInfo": map[string]string{
            "name":    "Lint",
            "version": "0.1.0",
        },
        "locale":   "zh-cn",
        "rootPath": c.rootURI,
        "rootUri":  "file://" + c.rootURI,
        "trace": "off",
    }

    var result interface{}
    err := c.conn.Call(context.Background(), "initialize", params, &result)
    if err != nil {
        return fmt.Errorf("初始化失败: %v", err)
    }
    log.Println("LSP服务器初始化1")
    params = map[string]interface{}{}
    err = c.conn.Call(context.Background(), "initialized", params, nil)
    if err != nil {
        return fmt.Errorf("初始化失败: %v", err)
    }
    log.Println("LSP服务器初始化2")
    return nil
}

func (c *LSPClient) findReferences(filename string, line, character int) ([]Location, error) {
    // 确保文件路径是绝对路径
    absFilename, err := filepath.Abs(filename)
    if err != nil {
        return nil, fmt.Errorf("获取文件绝对路径失败: %v", err)
    }

    params := map[string]interface{}{
        "textDocument": map[string]string{
            "uri": fmt.Sprintf("file://%s", absFilename),
        },
        "position": map[string]int{
            "line":      line,
            "character": character,
        },
        "context": map[string]bool{
            "includeDeclaration": true,
        },
    }

    var locations []Location
    fmt.Println("findReferences start")
    err = c.conn.Call(context.Background(), "textDocument/references", params, &locations)
    fmt.Println("findReferences finish")
    if err != nil {
        return nil, fmt.Errorf("查找引用失败: %v", err)
    }

    return locations, nil
}

func (c *LSPClient) shutdown() error {
    return c.conn.Call(context.Background(), "shutdown", nil, nil)
}

func main() {
    // 项目根目录
    projectPath := "/home/xxx/app"
    filename := "/home/xxx/app/common/rpc/rpc.go"
    line := 113    // 方法所在行
    character := 15 // 方法所在列

    // 创建LSP客户端
    client, err := newLSPClient(projectPath)
    if err != nil {
        log.Fatalf("创建LSP客户端失败: %v", err)
    }
    defer func() {
        if err := client.shutdown(); err != nil {
            log.Printf("关闭LSP服务器失败: %v", err)
        }
    }()
    fmt.Println("newLSPClient finish")
    // 初始化语言服务器
    if err := client.initialize(); err != nil {
        log.Fatalf("初始化LSP服务器失败: %v", err)
    }
    // 查找引用
    references, err := client.findReferences(filename, line, character)
    if err != nil {
        log.Fatalf("查找引用失败: %v", err)
    }
    // 打印引用位置
    fmt.Println("找到的引用数量:", len(references))
    for _, ref := range references {
        //根据行号获取函数定义
        req := map[string]interface{}{
            "textDocument": map[string]string{
                "uri": ref.URI,
            },
        }
        var syms []DocumentSymbol
        err = client.conn.Call(context.Background(), "textDocument/documentSymbol", req, &syms)
        if err != nil {
            log.Fatalf("查找引用失败: %v", err)
        }
        for _, sym := range syms {
            if sym.Location.Range.Start.Line <= ref.Range.Start.Line && sym.Location.Range.End.Line >= ref.Range.Start.Line {
                content, err := ioutil.ReadFile(sym.Location.URI[7:])
                if err != nil {
                    log.Printf("无法读取文件 %s: %v", sym.Location.URI, err)
                    continue
                }
                    lines := strings.Split(string(content), "\n")
                fmt.Printf("函数定义: %s\n", sym.Name)
                for i := sym.Location.Range.Start.Line; i <= sym.Location.Range.End.Line && i < len(lines); i++ {
                    fmt.Printf("行 %d: %s\n", i+1, lines[i])
                }
                fmt.Println("---")
            }
        }
    }
}

这个示例中,我们的headless将会查找所有调用rpc.Call的函数的函数体。

程序的输出大概如下:

行 137: func (m *Manager) xxx() {
                //此处省略51行
行 188:         rpc.Call(...) {
                //此处省略15行
行 203: }

当我们要编写lint工具时,可以再基于这些函数体来构造AST语法树进行lint

这会大大节省我们开发lint工具的难度,并提升lint工具的开发效率。

ps. 在我们使用jsonRPC来调用language server的过程中,还有可能会被language server调用,因此func (h *handler) Handle(函数才会无脑返回,不然我们jsonRPCcall也会被一直阻塞。