How can I compare two source code files/ ast trees?

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

How can I compare two source code files/ ast trees?

问题

我正在使用templates包生成一些源代码(是否有更好的方法?),在测试中,我需要检查输出是否与预期的源代码匹配。

  • 我尝试了字符串比较,但由于模板包生成的额外空格/换行符,它失败了。我还尝试了format.Source,但没有成功。(失败)
  • 我尝试解析两个源代码的AST(见下文),但即使代码基本相同,除了换行符/空格,AST也不匹配。(失败)
package main

import (
	"fmt"
	"go/parser"
	"go/token"
	"reflect"
)

func main() {
	stub1 := `package main
	 func myfunc(s string) error {
		return nil	
	}`
	stub2 := `package main
	
	 func myfunc(s string) error {
	
		return nil
		
	}`
	fset := token.NewFileSet()
	r1, err := parser.ParseFile(fset, "", stub1, parser.AllErrors)
	if err != nil {
		panic(err)
	}
	fset = token.NewFileSet()
	r2, err := parser.ParseFile(fset, "", stub2, parser.AllErrors)
	if err != nil {
		panic(err)
	}
	if !reflect.DeepEqual(r1, r2) {
		fmt.Printf("e %v, r %s, ", r1, r2)
	}
}

Playground

英文:

I'm generating some source code using the templates package( is there a better method? )and part of the testing I need to check if the output matches the expected source code.

  • I tried a string comparison but it fails due the extra spaces / new lines generated by the templates package. I've also tried format.Source with not success. ( FAIL)

  • I tried to parse the ast of the both sources (see bellow) but the ast doesn't match either even if the code is basically same except the new lines / spaces. (FAIL)

     package main
    
      import (
      	"fmt"
      	"go/parser"
      	"go/token"
      	"reflect"
      )
    
      func main() {
      	stub1 := `package main
      	 func myfunc(s string) error {
      		return nil	
      	}`
      	stub2 := `package main
    
      	 func myfunc(s string) error {
    
      		return nil
    
      	}`
      	fset := token.NewFileSet()
      	r1, err := parser.ParseFile(fset, "", stub1, parser.AllErrors)
      	if err != nil {
      		panic(err)
      	}
      	fset = token.NewFileSet()
      	r2, err := parser.ParseFile(fset, "", stub2, parser.AllErrors)
      	if err != nil {
      		panic(err)
      	}
      	if !reflect.DeepEqual(r1, r2) {
      		fmt.Printf("e %v, r %s, ", r1, r2)
      	}
      }
    

Playground

答案1

得分: 8

好的,以下是翻译好的内容:

嗯,实现这个的一种简单方法是使用go/printer库,它可以更好地控制输出格式,并且基本上就像在源代码上运行gofmt,对两个树进行规范化:

package main
import (
    "fmt"
    "go/parser"
    "go/token"
    "go/printer"
    "bytes"
)

func main() {
    stub1 := `package main
     func myfunc(s string) error {
        return nil  
    }`
    stub2 := `package main

     func myfunc(s string) error {

        return nil

    }`

    fset1 := token.NewFileSet()
    r1, err := parser.ParseFile(fset1, "", stub1, parser.AllErrors)
    if err != nil {
        panic(err)
    }
    fset2 := token.NewFileSet()
    r2, err := parser.ParseFile(fset1, "", stub2, parser.AllErrors)
    if err != nil {
        panic(err)
    }
   
    // 为每个源树创建两个输出缓冲区
    out1 := bytes.NewBuffer(nil)
    out2 := bytes.NewBuffer(nil)
    
    // 对两个源树使用相同的打印机配置
    conf := &printer.Config{Mode: printer.TabIndent | printer.UseSpaces, Tabwidth: 8}
    
    // 打印到两个输出缓冲区
    if err := conf.Fprint(out1, fset1, r1); err != nil {
        panic(err)
    }
    if err := conf.Fprint(out2, fset2, r2); err != nil {
        panic(err)
    }
    

    // 它们应该是相同的!
    if string(out1.Bytes()) != string(out2.Bytes()) {
        panic(string(out1.Bytes()) + "\n" + string(out2.Bytes()))
    } else {
        fmt.Println("A-OKAY!")
    }
}

当然,这段代码需要重构,以免看起来太愚蠢。另一种方法是,不使用DeepEqual,而是创建一个自己的树比较函数,跳过不相关的节点。

英文:

Well, one simple way to achieve this is to use the go/printer library, that gives you better control of output formatting, and is basically like running gofmt on the source, normalizing both trees:

package main
import (
"fmt"
"go/parser"
"go/token"
"go/printer"
//"reflect"
"bytes"
)
func main() {
stub1 := `package main
func myfunc(s string) error {
return nil  
}`
stub2 := `package main
func myfunc(s string) error {
return nil
}`
fset1 := token.NewFileSet()
r1, err := parser.ParseFile(fset1, "", stub1, parser.AllErrors)
if err != nil {
panic(err)
}
fset2 := token.NewFileSet()
r2, err := parser.ParseFile(fset1, "", stub2, parser.AllErrors)
if err != nil {
panic(err)
}
// we create two output buffers for each source tree
out1 := bytes.NewBuffer(nil)
out2 := bytes.NewBuffer(nil)
// we use the same printer config for both
conf := &printer.Config{Mode: printer.TabIndent | printer.UseSpaces, Tabwidth: 8}
// print to both outputs
if err := conf.Fprint(out1, fset1, r1); err != nil {
panic(err)
}
if err := conf.Fprint(out2, fset2, r2); err != nil {
panic(err)
}
// they should be identical!
if string(out1.Bytes()) != string(out2.Bytes()) {
panic(string(out1.Bytes()) +"\n" + string(out2.Bytes()))
} else {
fmt.Println("A-OKAY!")
}
}

Of course this code needs to be refactored to not look as stupid. Another approach is instead of using DeepEqual, create a tree comparison function yourself, that skips irrelevant nodes.

答案2

得分: 4

这比我想象的要简单。我所要做的就是删除空的换行符(在格式化之后)。以下是代码。

package main

import (
	"fmt"
	"go/format"
	"strings"
)

func main() {
	a, err := fmtSource(stub1)
	if err != nil {
		panic(err)
	}
	b, err := fmtSource(stub2)
	if err != nil {
		panic(err)
	}
	if a != b {
		fmt.Printf("a %v, \n b %v", a, b)
	}
}

func fmtSource(source string) (string, error) {
	if !strings.Contains(source, "package") {
		source = "package main\n" + source
	}
	b, err := format.Source([]byte(source))
	if err != nil {
		return "", err
	}
	// cleanLine replaces double space with one space
	cleanLine := func(s string) string {
		sa := strings.Fields(s)
		return strings.Join(sa, " ")
	}
	lines := strings.Split(string(b), "\n")
	n := 0
	var startLn *int
	for _, line := range lines {
		if line != "" {
			line = cleanLine(line)
			lines[n] = line
			if startLn == nil {
				x := n
				startLn = &x
			}
			n++
		}
	}
	lines = lines[*startLn:n]
	// Add final "" entry to get trailing newline from Join.
	if n > 0 && lines[n-1] != "" {
		lines = append(lines, "")
	}

	// Make it pretty
	b, err = format.Source([]byte(strings.Join(lines, "\n")))
	if err != nil {
		return "", err
	}
	return string(b), nil
}
英文:

This was easier than I thought. All I had to do was to remove the empty new lines(after formatting). Below is the code.

    package main
import (
"fmt"
"go/format"
"strings"
)
func main() {
a, err := fmtSource(stub1)
if err != nil {
panic(err)
}
b, err := fmtSource(stub2)
if err != nil {
panic(err)
}
if a != b {
fmt.Printf("a %v, \n b %v", a, b)
}
}
func fmtSource(source string) (string, error) {
if !strings.Contains(source, "package") {
source = "package main\n" + source
}
b, err := format.Source([]byte(source))
if err != nil {
return "", err
}
// cleanLine replaces double space with one space
cleanLine := func(s string)string{
sa := strings.Fields(s)
return strings.Join(sa, " ")
}
lines := strings.Split(string(b), "\n")
n := 0
var startLn *int
for _, line := range lines {
if line != "" {
line = cleanLine(line)
lines[n] = line
if startLn == nil {
x := n
startLn = &x
}
n++
}
}
lines = lines[*startLn:n]
// Add final "" entry to get trailing newline from Join.
if n > 0 && lines[n-1] != "" {
lines = append(lines, "")
}
// Make it pretty 
b, err = format.Source([]byte(strings.Join(lines, "\n")))
if err != nil {
return "", err
}
return string(b), nil
}

huangapple
  • 本文由 发表于 2015年5月13日 00:10:37
  • 转载请务必保留本文链接:https://go.coder-hub.com/30196236.html
匿名

发表评论

匿名网友

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

确定