golangci-lint/pkg/result/processors/filename_unadjuster.go
Isaev Denis df4f6766ba
reduce 1.5x memory usage on large repos on repeated runs (#764)
Get rid of AST cache: load AST when needed. Optimize memory allocations
for go/analysis actions.

Relates: #337
2019-10-01 14:52:00 +03:00

124 lines
3.4 KiB
Go

package processors
import (
"go/parser"
"go/token"
"path/filepath"
"strings"
"sync"
"time"
"golang.org/x/tools/go/packages"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result"
)
type posMapper func(pos token.Position) token.Position
// FilenameUnadjuster is needed because a lot of linters use fset.Position(f.Pos())
// to get filename. And they return adjusted filename (e.g. *.qtpl) for an issue. We need
// restore real .go filename to properly output it, parse it, etc.
type FilenameUnadjuster struct {
m map[string]posMapper // map from adjusted filename to position mapper: adjusted -> unadjusted position
log logutils.Log
loggedUnadjustments map[string]bool
}
var _ Processor = &FilenameUnadjuster{}
func processUnadjusterPkg(m map[string]posMapper, pkg *packages.Package, log logutils.Log) {
fset := token.NewFileSet() // it's more memory efficient to not store all in one fset
for _, filename := range pkg.CompiledGoFiles {
// It's important to call func here to run GC
processUnadjusterFile(filename, m, log, fset)
}
}
func processUnadjusterFile(filename string, m map[string]posMapper, log logutils.Log, fset *token.FileSet) {
syntax, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
if err != nil {
// Error will be reported by typecheck
return
}
adjustedFilename := fset.PositionFor(syntax.Pos(), true).Filename
if adjustedFilename == "" {
return
}
unadjustedFilename := fset.PositionFor(syntax.Pos(), false).Filename
if unadjustedFilename == "" || unadjustedFilename == adjustedFilename {
return
}
if !strings.HasSuffix(unadjustedFilename, ".go") {
return // file.go -> /caches/cgo-xxx
}
m[adjustedFilename] = func(adjustedPos token.Position) token.Position {
tokenFile := fset.File(syntax.Pos())
if tokenFile == nil {
log.Warnf("Failed to get token file for %s", adjustedFilename)
return adjustedPos
}
return fset.PositionFor(tokenFile.Pos(adjustedPos.Offset), false)
}
}
func NewFilenameUnadjuster(pkgs []*packages.Package, log logutils.Log) *FilenameUnadjuster {
m := map[string]posMapper{}
startedAt := time.Now()
var wg sync.WaitGroup
wg.Add(len(pkgs))
for _, pkg := range pkgs {
go func(pkg *packages.Package) {
// It's important to call func here to run GC
processUnadjusterPkg(m, pkg, log)
wg.Done()
}(pkg)
}
wg.Wait()
log.Infof("Pre-built %d adjustments in %s", len(m), time.Since(startedAt))
return &FilenameUnadjuster{
m: m,
log: log,
loggedUnadjustments: map[string]bool{},
}
}
func (p FilenameUnadjuster) Name() string {
return "filename_unadjuster"
}
func (p *FilenameUnadjuster) Process(issues []result.Issue) ([]result.Issue, error) {
return transformIssues(issues, func(i *result.Issue) *result.Issue {
issueFilePath := i.FilePath()
if !filepath.IsAbs(i.FilePath()) {
absPath, err := filepath.Abs(i.FilePath())
if err != nil {
p.log.Warnf("failed to build abs path for %q: %s", i.FilePath(), err)
return i
}
issueFilePath = absPath
}
mapper := p.m[issueFilePath]
if mapper == nil {
return i
}
newI := *i
newI.Pos = mapper(i.Pos)
if !p.loggedUnadjustments[i.Pos.Filename] {
p.log.Infof("Unadjusted from %v to %v", i.Pos, newI.Pos)
p.loggedUnadjustments[i.Pos.Filename] = true
}
return &newI
}), nil
}
func (FilenameUnadjuster) Finish() {}