github.com/nozzle/golangci-lint@v1.49.0-nz3/pkg/lint/load.go (about) 1 package lint 2 3 import ( 4 "context" 5 "fmt" 6 "go/build" 7 "go/token" 8 "os" 9 "path/filepath" 10 "regexp" 11 "strings" 12 "time" 13 14 "github.com/pkg/errors" 15 "golang.org/x/tools/go/packages" 16 17 "github.com/golangci/golangci-lint/internal/pkgcache" 18 "github.com/golangci/golangci-lint/pkg/config" 19 "github.com/golangci/golangci-lint/pkg/exitcodes" 20 "github.com/golangci/golangci-lint/pkg/fsutils" 21 "github.com/golangci/golangci-lint/pkg/golinters/goanalysis/load" 22 "github.com/golangci/golangci-lint/pkg/goutil" 23 "github.com/golangci/golangci-lint/pkg/lint/linter" 24 "github.com/golangci/golangci-lint/pkg/logutils" 25 ) 26 27 type ContextLoader struct { 28 cfg *config.Config 29 log logutils.Log 30 debugf logutils.DebugFunc 31 goenv *goutil.Env 32 pkgTestIDRe *regexp.Regexp 33 lineCache *fsutils.LineCache 34 fileCache *fsutils.FileCache 35 pkgCache *pkgcache.Cache 36 loadGuard *load.Guard 37 } 38 39 func NewContextLoader(cfg *config.Config, log logutils.Log, goenv *goutil.Env, 40 lineCache *fsutils.LineCache, fileCache *fsutils.FileCache, pkgCache *pkgcache.Cache, loadGuard *load.Guard) *ContextLoader { 41 return &ContextLoader{ 42 cfg: cfg, 43 log: log, 44 debugf: logutils.Debug(logutils.DebugKeyLoader), 45 goenv: goenv, 46 pkgTestIDRe: regexp.MustCompile(`^(.*) \[(.*)\.test\]`), 47 lineCache: lineCache, 48 fileCache: fileCache, 49 pkgCache: pkgCache, 50 loadGuard: loadGuard, 51 } 52 } 53 54 func (cl *ContextLoader) prepareBuildContext() { 55 // Set GOROOT to have working cross-compilation: cross-compiled binaries 56 // have invalid GOROOT. XXX: can't use runtime.GOROOT(). 57 goroot := cl.goenv.Get(goutil.EnvGoRoot) 58 if goroot == "" { 59 return 60 } 61 62 os.Setenv(string(goutil.EnvGoRoot), goroot) 63 build.Default.GOROOT = goroot 64 build.Default.BuildTags = cl.cfg.Run.BuildTags 65 } 66 67 func (cl *ContextLoader) findLoadMode(linters []*linter.Config) packages.LoadMode { 68 loadMode := packages.LoadMode(0) 69 for _, lc := range linters { 70 loadMode |= lc.LoadMode 71 } 72 73 return loadMode 74 } 75 76 func (cl *ContextLoader) buildArgs() []string { 77 args := cl.cfg.Run.Args 78 if len(args) == 0 { 79 return []string{"./..."} 80 } 81 82 var retArgs []string 83 for _, arg := range args { 84 if strings.HasPrefix(arg, ".") || filepath.IsAbs(arg) { 85 retArgs = append(retArgs, arg) 86 } else { 87 // go/packages doesn't work well if we don't have the prefix ./ for local packages 88 retArgs = append(retArgs, fmt.Sprintf(".%c%s", filepath.Separator, arg)) 89 } 90 } 91 92 return retArgs 93 } 94 95 func (cl *ContextLoader) makeBuildFlags() ([]string, error) { 96 var buildFlags []string 97 98 if len(cl.cfg.Run.BuildTags) != 0 { 99 // go help build 100 buildFlags = append(buildFlags, "-tags", strings.Join(cl.cfg.Run.BuildTags, " ")) 101 cl.log.Infof("Using build tags: %v", cl.cfg.Run.BuildTags) 102 } 103 104 mod := cl.cfg.Run.ModulesDownloadMode 105 if mod != "" { 106 // go help modules 107 allowedMods := []string{"mod", "readonly", "vendor"} 108 var ok bool 109 for _, am := range allowedMods { 110 if am == mod { 111 ok = true 112 break 113 } 114 } 115 if !ok { 116 return nil, fmt.Errorf("invalid modules download path %s, only (%s) allowed", mod, strings.Join(allowedMods, "|")) 117 } 118 119 buildFlags = append(buildFlags, fmt.Sprintf("-mod=%s", cl.cfg.Run.ModulesDownloadMode)) 120 } 121 122 return buildFlags, nil 123 } 124 125 func stringifyLoadMode(mode packages.LoadMode) string { 126 m := map[packages.LoadMode]string{ 127 packages.NeedCompiledGoFiles: "compiled_files", 128 packages.NeedDeps: "deps", 129 packages.NeedExportFile: "exports_file", 130 packages.NeedFiles: "files", 131 packages.NeedImports: "imports", 132 packages.NeedName: "name", 133 packages.NeedSyntax: "syntax", 134 packages.NeedTypes: "types", 135 packages.NeedTypesInfo: "types_info", 136 packages.NeedTypesSizes: "types_sizes", 137 } 138 139 var flags []string 140 for flag, flagStr := range m { 141 if mode&flag != 0 { 142 flags = append(flags, flagStr) 143 } 144 } 145 146 return fmt.Sprintf("%d (%s)", mode, strings.Join(flags, "|")) 147 } 148 149 func (cl *ContextLoader) debugPrintLoadedPackages(pkgs []*packages.Package) { 150 cl.debugf("loaded %d pkgs", len(pkgs)) 151 for i, pkg := range pkgs { 152 var syntaxFiles []string 153 for _, sf := range pkg.Syntax { 154 syntaxFiles = append(syntaxFiles, pkg.Fset.Position(sf.Pos()).Filename) 155 } 156 cl.debugf("Loaded pkg #%d: ID=%s GoFiles=%s CompiledGoFiles=%s Syntax=%s", 157 i, pkg.ID, pkg.GoFiles, pkg.CompiledGoFiles, syntaxFiles) 158 } 159 } 160 161 func (cl *ContextLoader) parseLoadedPackagesErrors(pkgs []*packages.Package) error { 162 for _, pkg := range pkgs { 163 for _, err := range pkg.Errors { 164 if strings.Contains(err.Msg, "no Go files") { 165 return errors.Wrapf(exitcodes.ErrNoGoFiles, "package %s", pkg.PkgPath) 166 } 167 if strings.Contains(err.Msg, "cannot find package") { 168 // when analyzing not existing directory 169 return errors.Wrap(exitcodes.ErrFailure, err.Msg) 170 } 171 } 172 } 173 174 return nil 175 } 176 177 func (cl *ContextLoader) loadPackages(ctx context.Context, loadMode packages.LoadMode) ([]*packages.Package, error) { 178 defer func(startedAt time.Time) { 179 cl.log.Infof("Go packages loading at mode %s took %s", stringifyLoadMode(loadMode), time.Since(startedAt)) 180 }(time.Now()) 181 182 cl.prepareBuildContext() 183 184 buildFlags, err := cl.makeBuildFlags() 185 if err != nil { 186 return nil, errors.Wrap(err, "failed to make build flags for go list") 187 } 188 189 conf := &packages.Config{ 190 Mode: loadMode, 191 Tests: cl.cfg.Run.AnalyzeTests, 192 Context: ctx, 193 BuildFlags: buildFlags, 194 Logf: cl.debugf, 195 // TODO: use fset, parsefile, overlay 196 } 197 198 args := cl.buildArgs() 199 cl.debugf("Built loader args are %s", args) 200 pkgs, err := packages.Load(conf, args...) 201 if err != nil { 202 return nil, errors.Wrap(err, "failed to load with go/packages") 203 } 204 205 // Currently, go/packages doesn't guarantee that error will be returned 206 // if context was canceled. See 207 // https://github.com/golang/tools/commit/c5cec6710e927457c3c29d6c156415e8539a5111#r39261855 208 if ctx.Err() != nil { 209 return nil, errors.Wrap(ctx.Err(), "timed out to load packages") 210 } 211 212 if loadMode&packages.NeedSyntax == 0 { 213 // Needed e.g. for go/analysis loading. 214 fset := token.NewFileSet() 215 packages.Visit(pkgs, nil, func(pkg *packages.Package) { 216 pkg.Fset = fset 217 cl.loadGuard.AddMutexForPkg(pkg) 218 }) 219 } 220 221 cl.debugPrintLoadedPackages(pkgs) 222 223 if err := cl.parseLoadedPackagesErrors(pkgs); err != nil { 224 return nil, err 225 } 226 227 return cl.filterTestMainPackages(pkgs), nil 228 } 229 230 func (cl *ContextLoader) tryParseTestPackage(pkg *packages.Package) (name string, isTest bool) { 231 matches := cl.pkgTestIDRe.FindStringSubmatch(pkg.ID) 232 if matches == nil { 233 return "", false 234 } 235 236 return matches[1], true 237 } 238 239 func (cl *ContextLoader) filterTestMainPackages(pkgs []*packages.Package) []*packages.Package { 240 var retPkgs []*packages.Package 241 for _, pkg := range pkgs { 242 if pkg.Name == "main" && strings.HasSuffix(pkg.PkgPath, ".test") { 243 // it's an implicit testmain package 244 cl.debugf("skip pkg ID=%s", pkg.ID) 245 continue 246 } 247 248 retPkgs = append(retPkgs, pkg) 249 } 250 251 return retPkgs 252 } 253 254 func (cl *ContextLoader) filterDuplicatePackages(pkgs []*packages.Package) []*packages.Package { 255 packagesWithTests := map[string]bool{} 256 for _, pkg := range pkgs { 257 name, isTest := cl.tryParseTestPackage(pkg) 258 if !isTest { 259 continue 260 } 261 packagesWithTests[name] = true 262 } 263 264 cl.debugf("package with tests: %#v", packagesWithTests) 265 266 var retPkgs []*packages.Package 267 for _, pkg := range pkgs { 268 _, isTest := cl.tryParseTestPackage(pkg) 269 if !isTest && packagesWithTests[pkg.PkgPath] { 270 // If tests loading is enabled, 271 // for package with files a.go and a_test.go go/packages loads two packages: 272 // 1. ID=".../a" GoFiles=[a.go] 273 // 2. ID=".../a [.../a.test]" GoFiles=[a.go a_test.go] 274 // We need only the second package, otherwise we can get warnings about unused variables/fields/functions 275 // in a.go if they are used only in a_test.go. 276 cl.debugf("skip pkg ID=%s because we load it with test package", pkg.ID) 277 continue 278 } 279 280 retPkgs = append(retPkgs, pkg) 281 } 282 283 return retPkgs 284 } 285 286 func (cl *ContextLoader) Load(ctx context.Context, linters []*linter.Config) (*linter.Context, error) { 287 loadMode := cl.findLoadMode(linters) 288 pkgs, err := cl.loadPackages(ctx, loadMode) 289 if err != nil { 290 return nil, errors.Wrap(err, "failed to load packages") 291 } 292 293 deduplicatedPkgs := cl.filterDuplicatePackages(pkgs) 294 295 if len(deduplicatedPkgs) == 0 { 296 return nil, exitcodes.ErrNoGoFiles 297 } 298 299 ret := &linter.Context{ 300 Packages: deduplicatedPkgs, 301 302 // At least `unused` linters works properly only on original (not deduplicated) packages, 303 // see https://github.com/golangci/golangci-lint/pull/585. 304 OriginalPackages: pkgs, 305 306 Cfg: cl.cfg, 307 Log: cl.log, 308 FileCache: cl.fileCache, 309 LineCache: cl.lineCache, 310 PkgCache: cl.pkgCache, 311 LoadGuard: cl.loadGuard, 312 } 313 314 return ret, nil 315 }