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