workgroups/vendor/github.com/boumenot/gocover-cobertura/gocover-cobertura.go
Marvin Preuss 1d4ae27878
All checks were successful
continuous-integration/drone/push Build is passing
ci: drone yaml with reusable anchors
2021-09-24 17:34:17 +02:00

283 lines
7.2 KiB
Go

package main
import (
"encoding/xml"
"flag"
"fmt"
"go/ast"
"go/parser"
"go/token"
"io"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"golang.org/x/tools/go/packages"
)
const coberturaDTDDecl = `<!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd">`
var byFiles bool
func fatal(format string, a ...interface{}) {
_, _ = fmt.Fprintf(os.Stderr, format, a...)
os.Exit(1)
}
func main() {
var ignore Ignore
flag.BoolVar(&byFiles, "by-files", false, "code coverage by file, not class")
flag.BoolVar(&ignore.GeneratedFiles, "ignore-gen-files", false, "ignore generated files")
ignoreDirsRe := flag.String("ignore-dirs", "", "ignore dirs matching this regexp")
ignoreFilesRe := flag.String("ignore-files", "", "ignore files matching this regexp")
flag.Parse()
var err error
if *ignoreDirsRe != "" {
ignore.Dirs, err = regexp.Compile(*ignoreDirsRe)
if err != nil {
fatal("Bad -ignore-dirs regexp: %s\n", err)
}
}
if *ignoreFilesRe != "" {
ignore.Files, err = regexp.Compile(*ignoreFilesRe)
if err != nil {
fatal("Bad -ignore-files regexp: %s\n", err)
}
}
if err := convert(os.Stdin, os.Stdout, &ignore); err != nil {
fatal("code coverage conversion failed: %s", err)
}
}
func convert(in io.Reader, out io.Writer, ignore *Ignore) error {
profiles, err := ParseProfiles(in, ignore)
if err != nil {
return err
}
pkgs, err := getPackages(profiles)
if err != nil {
return err
}
sources := make([]*Source, 0)
pkgMap := make(map[string]*packages.Package)
for _, pkg := range pkgs {
sources = appendIfUnique(sources, pkg.Module.Dir)
pkgMap[pkg.ID] = pkg
}
coverage := Coverage{Sources: sources, Packages: nil, Timestamp: time.Now().UnixNano() / int64(time.Millisecond)}
if err := coverage.parseProfiles(profiles, pkgMap, ignore); err != nil {
return err
}
_, _ = fmt.Fprint(out, xml.Header)
_, _ = fmt.Fprintln(out, coberturaDTDDecl)
encoder := xml.NewEncoder(out)
encoder.Indent("", " ")
if err := encoder.Encode(coverage); err != nil {
return err
}
_, _ = fmt.Fprintln(out)
return nil
}
func getPackages(profiles []*Profile) ([]*packages.Package, error) {
if len(profiles) == 0 {
return []*packages.Package{}, nil
}
var pkgNames []string
for _, profile := range profiles {
pkgNames = append(pkgNames, getPackageName(profile.FileName))
}
return packages.Load(&packages.Config{Mode: packages.NeedFiles | packages.NeedModule}, pkgNames...)
}
func appendIfUnique(sources []*Source, dir string) []*Source {
for _, source := range sources {
if source.Path == dir {
return sources
}
}
return append(sources, &Source{dir})
}
func getPackageName(filename string) string {
pkgName, _ := filepath.Split(filename)
// TODO(boumenot): Windows vs. Linux
return strings.TrimRight(strings.TrimRight(pkgName, "\\"), "/")
}
func findAbsFilePath(pkg *packages.Package, profileName string) string {
filename := filepath.Base(profileName)
for _, fullpath := range pkg.GoFiles {
if filepath.Base(fullpath) == filename {
return fullpath
}
}
return ""
}
func (cov *Coverage) parseProfiles(profiles []*Profile, pkgMap map[string]*packages.Package, ignore *Ignore) error {
cov.Packages = []*Package{}
for _, profile := range profiles {
pkgName := getPackageName(profile.FileName)
pkgPkg := pkgMap[pkgName]
if err := cov.parseProfile(profile, pkgPkg, ignore); err != nil {
return err
}
}
cov.LinesValid = cov.NumLines()
cov.LinesCovered = cov.NumLinesWithHits()
cov.LineRate = cov.HitRate()
return nil
}
func (cov *Coverage) parseProfile(profile *Profile, pkgPkg *packages.Package, ignore *Ignore) error {
if pkgPkg == nil || pkgPkg.Module == nil {
return fmt.Errorf("package required when using go modules")
}
fileName := profile.FileName[len(pkgPkg.Module.Path)+1:]
absFilePath := findAbsFilePath(pkgPkg, profile.FileName)
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
}
if ignore.Match(fileName, data) {
return nil
}
pkgPath, _ := filepath.Split(fileName)
pkgPath = strings.TrimRight(strings.TrimRight(pkgPath, "/"), "\\")
pkgPath = filepath.Join(pkgPkg.Module.Path, pkgPath)
// TODO(boumenot): package paths are not file paths, there is a consistent separator
pkgPath = strings.Replace(pkgPath, "\\", "/", -1)
var pkg *Package
for _, p := range cov.Packages {
if p.Name == pkgPath {
pkg = p
}
}
if pkg == nil {
pkg = &Package{Name: pkgPkg.ID, 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)
pkg.LineRate = pkg.HitRate()
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)
method.LineRate = method.Lines.HitRate()
class.Methods = append(class.Methods, method)
for _, line := range method.Lines {
class.Lines = append(class.Lines, line)
}
class.LineRate = class.Lines.HitRate()
}
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.AddOrUpdateLine(i, int64(b.Count))
}
}
return method
}
func (v *fileVisitor) class(n *ast.FuncDecl) *Class {
var className string
if byFiles {
//className = filepath.Base(v.fileName)
//
// NOTE(boumenot): ReportGenerator creates links that collide if names are not distinct.
// This could be an issue in how I am generating the report, but I have not been able
// to figure it out. The work around is to generate a fully qualified name based on
// the file path.
//
// src/lib/util/foo.go -> src.lib.util.foo.go
className = strings.Replace(v.fileName, "/", ".", -1)
className = strings.Replace(className, "\\", ".", -1)
} else {
className = v.recvName(n)
}
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, "*"))
}