格式化后的Golang修改AST出现奇怪缩进问题
格式化后的Golang修改AST出现奇怪缩进问题 我正在开发一个小的重构工具,它允许替换Go仓库中特定结构体字段的值。修改抽象语法树(AST)是有效的,并且相应的字段已被替换,但在用格式化后的AST替换原始源文件后,被替换的部分出现了奇怪的缩进。
我写了一个最小的示例来演示这个问题,可以在以下Playground中查看:https://go.dev/play/p/je4wZaGRZjq。
由于我运气不佳且未能找到此问题的解决方案,因此非常感谢任何关于如何修复此问题的建议。
太好了,很高兴你找到了解决办法!
更多关于格式化后的Golang修改AST出现奇怪缩进问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
这可能有点傻,但你能直接对结果运行 gofmt 吗?
如果有人想关注,我在GitHub上开了一个issue:https://github.com/golang/go/issues/49846。
这让我产生了一个疑问:我是否应该在 github.com/golang/go 上提交一个问题,因为这可能是一个缺陷?让我感到困惑的是,如果这确实是一个缺陷,那么其他人应该早就遇到这个问题了,毕竟通过抽象语法树进行重构并不是什么全新的技术。
不,这不是一个愚蠢的问题。输出实际上已经正确地通过了 gofmt 格式化,尽管它的缩进看起来很奇怪。
为了澄清,运行 gofmt 会保持原有的缩进。
func main() {
fmt.Println("hello world")
}
什么!?我简直不敢相信,直到我亲自运行了代码,结果你确实是对的!这真是太遗憾了。我不知道该怎么办!我刚刚尝试调整了 rawReplacement 中的空白字符,但并没有解决任何问题。
我刚刚尝试了 gofumpt,它生成的输出在我看来更加一致:
package main
import (
"fmt"
"strings"
)
func main() {
s := struct{ A string }{
A: strings.Join(
[]string{
"Hello",
" World!",
},
","),
}
fmt.Println(s.A)
}
@telo_tade 你为我指明了正确的方向,因为我刚刚找到了问题的根源 🎉
我所做的改变是将 parser.ParseExpr 替换为 parser.ParseExprFrom。前者只是 parser.ParseExprFrom 的一个包装函数,并传入一个空的 token.FileSet。这样一来,替换部分的 AST 节点就被分配到了错误的位置,它们被分配到了 example.go 中的行号,而这些行号是在它们被插入语法树之后才存在的。格式化程序似乎能够识别这些位置信息,这完全说得通,因此输出格式变得很奇怪。使用 parser.ParseExprFrom 并传入现有的 fset 可以修复位置信息问题,同时也解决了格式化问题。
replacement, err := parser.ParseExprFrom(fset, "replacement.go", string(rawReplacement), parser.AllErrors|parser.ParseComments)
@telo_tade 感谢您的详细解答!
解决此问题的关键在于正确生成替换节点。您已经非常接近正确答案了,为什么不先生成当前节点,然后修改它(第17和29行),使其符合您的预期呢?
我分享的playground是我正在开发的工具的简化版本,该工具从文件中读取Go表达式,并用它来替换AST中的某个节点。因此,我无法对获取的表达式做任何假设,也就无法修改它。
它们之间的区别在于第(17和29)行与第(118和123)行。您节点中的这两行(17和29)是错误的。
17 . . . . . NamePos: example.go:3:2
29 . . . . . ValuePos: example.go:5:4
118 . . . . . . . . . . . . . . . ValuePos: example.go:7:28
123 . . . . . . . . . . . . . . . ValuePos: example.go:7:36
我认为我还没有完全理解为什么这些特定的行是问题所在。不过,我也会使用 ast.Print 来打印这些AST以调试问题。
再次感谢!
在Go中修改AST后出现缩进问题通常是因为格式化时没有正确处理节点的位置信息。当直接替换AST节点而不更新其位置信息时,go/format包可能无法正确格式化代码。
在你的示例中,主要问题是替换后的节点缺少正确的位置信息。以下是修复后的代码:
package main
import (
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"log"
"strings"
)
func main() {
src := `package main
type Foo struct {
Bar string
}`
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "", src, parser.ParseComments)
if err != nil {
log.Fatal(err)
}
ast.Inspect(f, func(n ast.Node) bool {
if ts, ok := n.(*ast.TypeSpec); ok && ts.Name.Name == "Foo" {
if st, ok := ts.Type.(*ast.StructType); ok {
for _, field := range st.Fields.List {
if field.Names[0].Name == "Bar" {
// 创建新的字段值并设置正确的位置
newValue := &ast.BasicLit{
Kind: token.STRING,
Value: `"new value"`,
}
// 复制原始值的位置信息
if field.Tag != nil {
newValue.ValuePos = field.Tag.ValuePos
} else if field.Type != nil {
// 如果没有tag,使用类型的位置
newValue.ValuePos = field.Type.Pos()
}
// 将新值作为tag添加,而不是替换字段
field.Tag = newValue
}
}
}
}
return true
})
var buf strings.Builder
if err := format.Node(&buf, fset, f); err != nil {
log.Fatal(err)
}
fmt.Println(buf.String())
}
更健壮的解决方案是使用astutil包并确保正确设置位置信息:
package main
import (
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"go/astutil"
"log"
"strings"
)
func main() {
src := `package main
type Foo struct {
Bar string
}`
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "", src, parser.ParseComments)
if err != nil {
log.Fatal(err)
}
// 使用astutil进行更安全的AST操作
astutil.Apply(f, nil, func(c *astutil.Cursor) bool {
if field, ok := c.Node().(*ast.Field); ok {
if len(field.Names) > 0 && field.Names[0].Name == "Bar" {
// 创建新的tag
newTag := &ast.BasicLit{
Kind: token.STRING,
Value: `"new value"`,
ValuePos: field.Type.Pos(), // 设置正确的位置
}
field.Tag = newTag
}
}
return true
})
// 使用format.Node时指定fset
var buf strings.Builder
if err := format.Node(&buf, fset, f); err != nil {
log.Fatal(err)
}
fmt.Println(buf.String())
}
如果你需要完全替换字段而不仅仅是添加tag,可以这样做:
package main
import (
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"log"
"strings"
)
func main() {
src := `package main
type Foo struct {
Bar string
Baz int
}`
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "", src, parser.ParseComments)
if err != nil {
log.Fatal(err)
}
ast.Inspect(f, func(n ast.Node) bool {
if ts, ok := n.(*ast.TypeSpec); ok && ts.Name.Name == "Foo" {
if st, ok := ts.Type.(*ast.StructType); ok {
for i, field := range st.Fields.List {
if field.Names[0].Name == "Bar" {
// 创建新字段
newField := &ast.Field{
Names: []*ast.Ident{ast.NewIdent("Bar")},
Type: &ast.Ident{Name: "string"},
Tag: &ast.BasicLit{Kind: token.STRING, Value: `"new value"`},
}
// 复制位置信息
if field.Type != nil {
newField.Type.SetPos(field.Type.Pos())
}
if field.Tag != nil {
newField.Tag.SetPos(field.Tag.Pos())
}
// 替换字段
st.Fields.List[i] = newField
}
}
}
}
return true
})
var buf strings.Builder
if err := format.Node(&buf, fset, f); err != nil {
log.Fatal(err)
}
fmt.Println(buf.String())
}
关键点是确保新创建的AST节点具有正确的位置信息。如果位置信息不正确,格式化器就无法确定正确的缩进级别。

