使用go ast从位置获取周围函数名称

huangapple go评论77阅读模式
英文:

Use go ast to get surrounding function name from position

问题

通过源代码本身(无运行时检查),从文件位置获取周围函数名称的最佳方法是什么?

例如,假设我有一些代码:

func MyFunc() {
    doSomething() // package/file.go:215:15
}

我有doSomething的位置,即package/file.go:215:15,有没有一种简单的方法可以获取MyFunc?

英文:

What's the best way to get the surrounding function name from a file position via the source code alone (no runtime checks)?

For example say I have some code:

func MyFunc() {
    doSomething() // package/file.go:215:15
}

And I have the position of doSomething, at package/file.go:215:15, is there a way to easily get MyFunc?

答案1

得分: 6

“最好”的问题总是很难回答,因为这在很大程度上取决于你没有提到的要求。

我想到了两种解决方法,它们各有利弊:

快速而简单的方法

快速而简单的方法是循环遍历代码的每一行,在我们遇到感兴趣的行之前,查找最近的函数声明。这应该是包含我们行的函数。

优点:

  • 快速(相对于构建AST)
  • 易于理解(不需要了解AST和深度优先搜索的工作原理)
  • 当Go代码包含解析器无法处理的错误时有效(对于某些类型的代码检查器或工具可能是需要的)

缺点:

  • 对代码有一些假设,我们假设(和空格是结束函数声明的唯一字符,并且假设代码格式化得不会在一行上有两个函数声明。

解析、AST和深度优先搜索

更“规范正确”的方法是使用“官方”Go解析器解析Go代码。这将生成一个抽象语法树(AST),我们可以使用深度优先搜索(DFS)算法遍历该树,找到包含我们位置的最具体的AST节点,同时也找到最新的函数声明。

优点:

  • 规范正确,解析器处理所有边缘情况、Unicode字符等。

缺点:

  • 速度较慢,因为解析器需要进行大量的工作,构建AST树等。
  • 需要更多的知识来理解和处理。
  • 如果Go文件包含错误,解析器将在这些错误上报错,因此不会给出结果。
英文:

Questions containing the phrase "the best" are always hard to answer since this very much depends on your requirements, which you have not named.

I thought of 2 ways to solve it this, both with their own pro and con list:


Quick and dirty

The quick and dirty approach would be to just loop over every line of code and look for the most recent occurrence of a function deceleration before we hit the line we are interested in. Which should be the function containing our line.

package main

import (
	"bufio"
	"fmt"
	"os"
	"regexp"
	"strconv"
	"strings"
)

func main() {
	if len(os.Args) < 2 {
		usage()
	}

	loc := strings.Split(os.Args[1], ":")
	if len(loc) != 2 {
		usage()
	}

	filePath := loc[0]
	lineStr := loc[1]
	targetLine, err := strconv.Atoi(lineStr)
	if err != nil {
		fmt.Println(err.Error())
		usage()
	}

	f, err := os.Open(filePath)
	if err != nil {
		fmt.Println(err.Error())
		usage()
	}
	defer f.Close()

	lineScanner := bufio.NewScanner(f)
	line := 0
	var lastFunc string
	for lineScanner.Scan() {
		m := funcName.FindStringSubmatch(lineScanner.Text())
		if len(m) > 0 {
			lastFunc = m[1]
		}

		if line == targetLine {
			fmt.Println(lastFunc)
			return
		}

		line++
	}
}

func usage() {
	fmt.Fprintf(os.Stderr, "Usage: %s {file:line}\n", os.Args[0])
	os.Exit(1)
}

// Look for a func followed by anything and ending at a `(` or ` `(space).
var funcName = regexp.MustCompile(`func ([^ (]+)`)

Pros:

  • Quick (relative to building a AST)
  • Easy to understand (no need to know how ASTs and Depth-First-Search works)
  • Works when go code contains errors on which the parser fails (which can be what you want for some types of linters or tools).

Cons:

  • Assumptions about the code, we assume that ( and are the only chars that end a func declaration, and we assume the code is formatted so that there are never 2 func declerations on 1 line.

The more "spec correct" approach is to parse the go code using the "official" go parser which the go compiler also uses. This results in an AST(Abstract Syntax Tree) which we can traverse using the DFS(Depth First Search) algorithm to find the most specific AST node which contains our location, along the way also finding the latest function decleration.

package main

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"regexp"
	"strconv"
	"strings"
)

func main() {
	if len(os.Args) < 2 {
		usage()
	}

	var pos token.Position

	loc := strings.Split(os.Args[1], ":")
	if len(loc) >= 2 {
		pos.Filename = loc[0]
		line, err := strconv.Atoi(loc[1])
		if err != nil {
			fmt.Println(err.Error())
			usage()
		}
		pos.Line = line
	} else {
		usage()
	}

	if len(loc) >= 3 {
		col, err := strconv.Atoi(loc[2])
		if err != nil {
			fmt.Println(err.Error())
			usage()
		}
		pos.Column = col
	}

	file, err := os.Open(pos.Filename)
	if err != nil {
		fmt.Println(err.Error())
		usage()
	}

	fset := token.NewFileSet()
	f, err := parser.ParseFile(fset, "", file, 0)
	if err != nil {
		fmt.Println(err.Error())
		usage()
	}

	var lastFunc *ast.FuncDecl
	ast.Inspect(f, func(n ast.Node) bool {
		if n == nil {
			return false
		}

		// Store the most specific function declaration
		if funcDecl, ok := n.(*ast.FuncDecl); ok {
			lastFunc = funcDecl
		}

		start := fset.Position(n.Pos())
		end := fset.Position(n.End())

		// Don't traverse nodes which don't contain the target line
		if start.Line > pos.Line || end.Line < pos.Line {
			return false
		}

		// If node starts and stops on the same line
		if start.Line == pos.Line && end.Line == pos.Line {
			// Don't traverse nodes which don't contain the target column
			if start.Column > pos.Column || end.Column < pos.Column {
				return false
			}
		}

		// Note, the very last node to be traversed is our target node

		return true
	})

	if lastFunc != nil {
		fmt.Println(lastFunc.Name.String())
	}
}

func usage() {
	fmt.Fprintf(os.Stderr, "Usage: %s {file:line:column}\n", os.Args[0])
	os.Exit(1)
}

Pros:

  • Spec correct, the parser handles all edge cases, unicode chars, ect.

Cons:

  • Slower since the parser has do to a lot of work, building the AST tree, ect.
  • Requires more knowledge to understand / work with
  • If the go file contains errors, the parser will error out on those, thus not giving a result

huangapple
  • 本文由 发表于 2022年4月14日 08:05:07
  • 转载请务必保留本文链接:https://go.coder-hub.com/71865034.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定