blob: fd866264f6c99ea04a4812b011e3f3952ec9ac81 [file] [log] [blame]
package main
import (
"encoding/xml"
"fmt"
"go/ast"
"go/build"
"go/parser"
"go/token"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
)
func main() {
convert(os.Stdin, os.Stdout)
}
func convert(in io.Reader, out io.Writer) {
profiles, err := ParseProfiles(in)
if err != nil {
panic("Can't parse profiles")
}
srcDirs := build.Default.SrcDirs()
sources := make([]*Source, len(srcDirs))
for i, dir := range srcDirs {
sources[i] = &Source{dir}
}
coverage := Coverage{Sources: sources, Packages: nil, Timestamp: time.Now().UnixNano() / int64(time.Millisecond)}
coverage.parseProfiles(profiles)
fmt.Fprintf(out, xml.Header)
fmt.Fprintf(out, "<!DOCTYPE coverage SYSTEM \"http://cobertura.sourceforge.net/xml/coverage-03.dtd\">\n")
encoder := xml.NewEncoder(out)
encoder.Indent("", "\t")
err = encoder.Encode(coverage)
if err != nil {
panic(err)
}
fmt.Fprintln(out)
}
func (cov *Coverage) parseProfiles(profiles []*Profile) error {
cov.Packages = []*Package{}
for _, profile := range profiles {
cov.parseProfile(profile)
}
return nil
}
func (cov *Coverage) parseProfile(profile *Profile) error {
fileName := profile.FileName
absFilePath, err := findFile(fileName)
if err != nil {
return err
}
fset := token.NewFileSet()
parsed, err := parser.ParseFile(fset, absFilePath, nil, 0)
if err != nil {
return err
}
data, err := ioutil.ReadFile(absFilePath)
if err != nil {
return err
}
pkgPath, _ := filepath.Split(fileName)
pkgPath = strings.TrimRight(pkgPath, string(os.PathSeparator))
var pkg *Package
for _, p := range cov.Packages {
if p.Name == pkgPath {
pkg = p
}
}
if pkg == nil {
pkg = &Package{Name: pkgPath, Classes: []*Class{}}
cov.Packages = append(cov.Packages, pkg)
}
visitor := &fileVisitor{
fset: fset,
fileName: fileName,
fileData: data,
classes: make(map[string]*Class),
pkg: pkg,
profile: profile,
}
ast.Walk(visitor, parsed)
return nil
}
type fileVisitor struct {
fset *token.FileSet
fileName string
fileData []byte
pkg *Package
classes map[string]*Class
profile *Profile
}
func (v *fileVisitor) Visit(node ast.Node) ast.Visitor {
switch n := node.(type) {
case *ast.FuncDecl:
class := v.class(n)
method := v.method(n)
class.Methods = append(class.Methods, method)
for _, line := range method.Lines {
class.Lines = append(class.Lines, line)
}
}
return v
}
func (v *fileVisitor) method(n *ast.FuncDecl) *Method {
method := &Method{Name: n.Name.Name}
method.Lines = []*Line{}
start := v.fset.Position(n.Pos())
end := v.fset.Position(n.End())
startLine := start.Line
startCol := start.Column
endLine := end.Line
endCol := end.Column
// The blocks are sorted, so we can stop counting as soon as we reach the end of the relevant block.
for _, b := range v.profile.Blocks {
if b.StartLine > endLine || (b.StartLine == endLine && b.StartCol >= endCol) {
// Past the end of the function.
break
}
if b.EndLine < startLine || (b.EndLine == startLine && b.EndCol <= startCol) {
// Before the beginning of the function
continue
}
for i := b.StartLine; i <= b.EndLine; i++ {
method.Lines = append(method.Lines, &Line{Number: i, Hits: int64(b.Count)})
}
}
return method
}
func (v *fileVisitor) class(n *ast.FuncDecl) *Class {
className := v.recvName(n)
var class *Class = v.classes[className]
if class == nil {
class = &Class{Name: className, Filename: v.fileName, Methods: []*Method{}, Lines: []*Line{}}
v.classes[className] = class
v.pkg.Classes = append(v.pkg.Classes, class)
}
return class
}
func (v *fileVisitor) recvName(n *ast.FuncDecl) string {
if n.Recv == nil {
return "-"
}
recv := n.Recv.List[0].Type
start := v.fset.Position(recv.Pos())
end := v.fset.Position(recv.End())
name := string(v.fileData[start.Offset:end.Offset])
return strings.TrimSpace(strings.TrimLeft(name, "*"))
}