github.com/thrasher-corp/golangci-lint@v1.17.3/pkg/lint/load.go (about) 1 package lint 2 3 import ( 4 "context" 5 "fmt" 6 "go/build" 7 "go/types" 8 "os" 9 "path/filepath" 10 "regexp" 11 "strings" 12 "time" 13 14 "github.com/golangci/golangci-lint/pkg/fsutils" 15 16 "github.com/pkg/errors" 17 "golang.org/x/tools/go/loader" 18 "golang.org/x/tools/go/packages" 19 "golang.org/x/tools/go/ssa" 20 "golang.org/x/tools/go/ssa/ssautil" 21 22 "github.com/golangci/golangci-lint/pkg/config" 23 "github.com/golangci/golangci-lint/pkg/exitcodes" 24 "github.com/golangci/golangci-lint/pkg/goutil" 25 "github.com/golangci/golangci-lint/pkg/lint/astcache" 26 "github.com/golangci/golangci-lint/pkg/lint/linter" 27 "github.com/golangci/golangci-lint/pkg/logutils" 28 ) 29 30 type ContextLoader struct { 31 cfg *config.Config 32 log logutils.Log 33 debugf logutils.DebugFunc 34 goenv *goutil.Env 35 pkgTestIDRe *regexp.Regexp 36 lineCache *fsutils.LineCache 37 fileCache *fsutils.FileCache 38 } 39 40 func NewContextLoader(cfg *config.Config, log logutils.Log, goenv *goutil.Env, 41 lineCache *fsutils.LineCache, fileCache *fsutils.FileCache) *ContextLoader { 42 43 return &ContextLoader{ 44 cfg: cfg, 45 log: log, 46 debugf: logutils.Debug("loader"), 47 goenv: goenv, 48 pkgTestIDRe: regexp.MustCompile(`^(.*) \[(.*)\.test\]`), 49 lineCache: lineCache, 50 fileCache: fileCache, 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("GOROOT") 58 if goroot == "" { 59 return 60 } 61 62 os.Setenv("GOROOT", goroot) 63 build.Default.GOROOT = goroot 64 build.Default.BuildTags = cl.cfg.Run.BuildTags 65 } 66 67 func (cl ContextLoader) makeFakeLoaderPackageInfo(pkg *packages.Package) *loader.PackageInfo { 68 var errs []error 69 for _, err := range pkg.Errors { 70 errs = append(errs, err) 71 } 72 73 typeInfo := &types.Info{} 74 if pkg.TypesInfo != nil { 75 typeInfo = pkg.TypesInfo 76 } 77 78 return &loader.PackageInfo{ 79 Pkg: pkg.Types, 80 Importable: true, // not used 81 TransitivelyErrorFree: !pkg.IllTyped, 82 83 // use compiled (preprocessed) go files AST; 84 // AST linters use not preprocessed go files AST 85 Files: pkg.Syntax, 86 Errors: errs, 87 Info: *typeInfo, 88 } 89 } 90 91 func shouldSkipPkg(pkg *packages.Package) bool { 92 // it's an implicit testmain package 93 return pkg.Name == "main" && strings.HasSuffix(pkg.PkgPath, ".test") 94 } 95 96 func (cl ContextLoader) makeFakeLoaderProgram(pkgs []*packages.Package) *loader.Program { 97 var createdPkgs []*loader.PackageInfo 98 for _, pkg := range pkgs { 99 if pkg.IllTyped { 100 // some linters crash on packages with errors, 101 // skip them and warn about them in another place 102 continue 103 } 104 105 pkgInfo := cl.makeFakeLoaderPackageInfo(pkg) 106 createdPkgs = append(createdPkgs, pkgInfo) 107 } 108 109 allPkgs := map[*types.Package]*loader.PackageInfo{} 110 for _, pkg := range createdPkgs { 111 pkg := pkg 112 allPkgs[pkg.Pkg] = pkg 113 } 114 for _, pkg := range pkgs { 115 if pkg.IllTyped { 116 // some linters crash on packages with errors, 117 // skip them and warn about them in another place 118 continue 119 } 120 121 for _, impPkg := range pkg.Imports { 122 // don't use astcache for imported packages: we don't find issues in cgo imported deps 123 pkgInfo := cl.makeFakeLoaderPackageInfo(impPkg) 124 allPkgs[pkgInfo.Pkg] = pkgInfo 125 } 126 } 127 128 return &loader.Program{ 129 Fset: pkgs[0].Fset, 130 Imported: nil, // not used without .Created in any linter 131 Created: createdPkgs, // all initial packages 132 AllPackages: allPkgs, // all initial packages and their depndencies 133 } 134 } 135 136 func (cl ContextLoader) buildSSAProgram(pkgs []*packages.Package) *ssa.Program { 137 startedAt := time.Now() 138 var pkgsBuiltDuration time.Duration 139 defer func() { 140 cl.log.Infof("SSA repr building timing: packages building %s, total %s", 141 pkgsBuiltDuration, time.Since(startedAt)) 142 }() 143 144 ssaProg, _ := ssautil.Packages(pkgs, ssa.GlobalDebug) 145 pkgsBuiltDuration = time.Since(startedAt) 146 ssaProg.Build() 147 return ssaProg 148 } 149 150 func (cl ContextLoader) findLoadMode(linters []*linter.Config) packages.LoadMode { 151 maxLoadMode := packages.LoadFiles 152 for _, lc := range linters { 153 curLoadMode := packages.LoadFiles 154 if lc.NeedsTypeInfo { 155 curLoadMode = packages.LoadSyntax 156 } 157 if lc.NeedsSSARepr { 158 curLoadMode = packages.LoadAllSyntax 159 } 160 if curLoadMode > maxLoadMode { 161 maxLoadMode = curLoadMode 162 } 163 } 164 165 return maxLoadMode 166 } 167 168 func stringifyLoadMode(mode packages.LoadMode) string { 169 switch mode { 170 case packages.LoadFiles: 171 return "load files" 172 case packages.LoadImports: 173 return "load imports" 174 case packages.LoadTypes: 175 return "load types" 176 case packages.LoadSyntax: 177 return "load types and syntax" 178 } 179 // it may be an alias, and may be not 180 if mode == packages.LoadAllSyntax { 181 return "load deps types and syntax" 182 } 183 return "unknown" 184 } 185 186 func (cl ContextLoader) buildArgs() []string { 187 args := cl.cfg.Run.Args 188 if len(args) == 0 { 189 return []string{"./..."} 190 } 191 192 var retArgs []string 193 for _, arg := range args { 194 if strings.HasPrefix(arg, ".") || filepath.IsAbs(arg) { 195 retArgs = append(retArgs, arg) 196 } else { 197 // go/packages doesn't work well if we don't have prefix ./ for local packages 198 retArgs = append(retArgs, fmt.Sprintf(".%c%s", filepath.Separator, arg)) 199 } 200 } 201 202 return retArgs 203 } 204 205 func (cl ContextLoader) makeBuildFlags() ([]string, error) { 206 var buildFlags []string 207 208 if len(cl.cfg.Run.BuildTags) != 0 { 209 // go help build 210 buildFlags = append(buildFlags, "-tags", strings.Join(cl.cfg.Run.BuildTags, " ")) 211 } 212 213 mod := cl.cfg.Run.ModulesDownloadMode 214 if mod != "" { 215 // go help modules 216 allowedMods := []string{"release", "readonly", "vendor"} 217 var ok bool 218 for _, am := range allowedMods { 219 if am == mod { 220 ok = true 221 break 222 } 223 } 224 if !ok { 225 return nil, fmt.Errorf("invalid modules download path %s, only (%s) allowed", mod, strings.Join(allowedMods, "|")) 226 } 227 228 buildFlags = append(buildFlags, fmt.Sprintf("-mod=%s", cl.cfg.Run.ModulesDownloadMode)) 229 } 230 231 return buildFlags, nil 232 } 233 234 func (cl ContextLoader) loadPackages(ctx context.Context, loadMode packages.LoadMode) ([]*packages.Package, error) { 235 defer func(startedAt time.Time) { 236 cl.log.Infof("Go packages loading at mode %s took %s", stringifyLoadMode(loadMode), time.Since(startedAt)) 237 }(time.Now()) 238 239 cl.prepareBuildContext() 240 241 buildFlags, err := cl.makeBuildFlags() 242 if err != nil { 243 return nil, errors.Wrap(err, "failed to make build flags for go list") 244 } 245 246 conf := &packages.Config{ 247 Mode: loadMode, 248 Tests: cl.cfg.Run.AnalyzeTests, 249 Context: ctx, 250 BuildFlags: buildFlags, 251 //TODO: use fset, parsefile, overlay 252 } 253 254 args := cl.buildArgs() 255 cl.debugf("Built loader args are %s", args) 256 pkgs, err := packages.Load(conf, args...) 257 if err != nil { 258 return nil, errors.Wrap(err, "failed to load program with go/packages") 259 } 260 cl.debugf("loaded %d pkgs", len(pkgs)) 261 for i, pkg := range pkgs { 262 var syntaxFiles []string 263 for _, sf := range pkg.Syntax { 264 syntaxFiles = append(syntaxFiles, pkg.Fset.Position(sf.Pos()).Filename) 265 } 266 cl.debugf("Loaded pkg #%d: ID=%s GoFiles=%s CompiledGoFiles=%s Syntax=%s", 267 i, pkg.ID, pkg.GoFiles, pkg.CompiledGoFiles, syntaxFiles) 268 } 269 270 for _, pkg := range pkgs { 271 for _, err := range pkg.Errors { 272 if strings.Contains(err.Msg, "no Go files") { 273 return nil, errors.Wrapf(exitcodes.ErrNoGoFiles, "package %s", pkg.PkgPath) 274 } 275 if strings.Contains(err.Msg, "cannot find package") { 276 // when analyzing not existing directory 277 return nil, errors.Wrap(exitcodes.ErrFailure, err.Msg) 278 } 279 } 280 } 281 282 return cl.filterPackages(pkgs), nil 283 } 284 285 func (cl ContextLoader) tryParseTestPackage(pkg *packages.Package) (name, testName string, isTest bool) { 286 matches := cl.pkgTestIDRe.FindStringSubmatch(pkg.ID) 287 if matches == nil { 288 return "", "", false 289 } 290 291 return matches[1], matches[2], true 292 } 293 294 func (cl ContextLoader) filterPackages(pkgs []*packages.Package) []*packages.Package { 295 packagesWithTests := map[string]bool{} 296 for _, pkg := range pkgs { 297 name, _, isTest := cl.tryParseTestPackage(pkg) 298 if !isTest { 299 continue 300 } 301 packagesWithTests[name] = true 302 } 303 304 cl.debugf("package with tests: %#v", packagesWithTests) 305 306 var retPkgs []*packages.Package 307 for _, pkg := range pkgs { 308 if shouldSkipPkg(pkg) { 309 cl.debugf("skip pkg ID=%s", pkg.ID) 310 continue 311 } 312 313 _, _, isTest := cl.tryParseTestPackage(pkg) 314 if !isTest && packagesWithTests[pkg.PkgPath] { 315 // If tests loading is enabled, 316 // for package with files a.go and a_test.go go/packages loads two packages: 317 // 1. ID=".../a" GoFiles=[a.go] 318 // 2. ID=".../a [.../a.test]" GoFiles=[a.go a_test.go] 319 // We need only the second package, otherwise we can get warnings about unused variables/fields/functions 320 // in a.go if they are used only in a_test.go. 321 cl.debugf("skip pkg ID=%s because we load it with test package", pkg.ID) 322 continue 323 } 324 325 retPkgs = append(retPkgs, pkg) 326 } 327 328 return retPkgs 329 } 330 331 //nolint:gocyclo 332 func (cl ContextLoader) Load(ctx context.Context, linters []*linter.Config) (*linter.Context, error) { 333 loadMode := cl.findLoadMode(linters) 334 pkgs, err := cl.loadPackages(ctx, loadMode) 335 if err != nil { 336 return nil, err 337 } 338 339 if len(pkgs) == 0 { 340 return nil, exitcodes.ErrNoGoFiles 341 } 342 343 var prog *loader.Program 344 if loadMode >= packages.LoadSyntax { 345 prog = cl.makeFakeLoaderProgram(pkgs) 346 } 347 348 var ssaProg *ssa.Program 349 if loadMode == packages.LoadAllSyntax { 350 ssaProg = cl.buildSSAProgram(pkgs) 351 } 352 353 astLog := cl.log.Child("astcache") 354 astCache, err := astcache.LoadFromPackages(pkgs, astLog) 355 if err != nil { 356 return nil, err 357 } 358 359 ret := &linter.Context{ 360 Packages: pkgs, 361 Program: prog, 362 SSAProgram: ssaProg, 363 LoaderConfig: &loader.Config{ 364 Cwd: "", // used by depguard and fallbacked to os.Getcwd 365 Build: nil, // used by depguard and megacheck and fallbacked to build.Default 366 }, 367 Cfg: cl.cfg, 368 ASTCache: astCache, 369 Log: cl.log, 370 FileCache: cl.fileCache, 371 LineCache: cl.lineCache, 372 } 373 374 separateNotCompilingPackages(ret) 375 return ret, nil 376 } 377 378 // separateNotCompilingPackages moves not compiling packages into separate slice: 379 // a lot of linters crash on such packages 380 func separateNotCompilingPackages(lintCtx *linter.Context) { 381 goodPkgs := make([]*packages.Package, 0, len(lintCtx.Packages)) 382 for _, pkg := range lintCtx.Packages { 383 if pkg.IllTyped { 384 lintCtx.NotCompilingPackages = append(lintCtx.NotCompilingPackages, pkg) 385 } else { 386 goodPkgs = append(goodPkgs, pkg) 387 } 388 } 389 390 lintCtx.Packages = goodPkgs 391 if len(lintCtx.NotCompilingPackages) != 0 { 392 lintCtx.Log.Infof("Packages that do not compile: %+v", lintCtx.NotCompilingPackages) 393 } 394 }