diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/cache/cache.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/cache/cache.go index da80f706..682686e2 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/cache/cache.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/cache/cache.go @@ -166,7 +166,7 @@ func (c *Cache) computePkgHash(pkg *packages.Package) (hashResults, error) { fmt.Fprintf(key, "pkgpath %s\n", pkg.PkgPath) - for _, f := range pkg.CompiledGoFiles { + for _, f := range slices.Concat(pkg.CompiledGoFiles, pkg.IgnoredFiles) { h, fErr := c.fileHash(f) if fErr != nil { return nil, fmt.Errorf("failed to calculate file %s hash: %w", f, fErr) diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/analysisinternal/analysis.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/analysisinternal/analysis.go index bb12600d..b613d167 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/analysisinternal/analysis.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/analysisinternal/analysis.go @@ -8,25 +8,30 @@ package analysisinternal import ( "fmt" - "os" + "slices" "golang.org/x/tools/go/analysis" ) -// MakeReadFile returns a simple implementation of the Pass.ReadFile function. -func MakeReadFile(pass *analysis.Pass) func(filename string) ([]byte, error) { +// A ReadFileFunc is a function that returns the +// contents of a file, such as [os.ReadFile]. +type ReadFileFunc = func(filename string) ([]byte, error) + +// CheckedReadFile returns a wrapper around a Pass.ReadFile +// function that performs the appropriate checks. +func CheckedReadFile(pass *analysis.Pass, readFile ReadFileFunc) ReadFileFunc { return func(filename string) ([]byte, error) { if err := CheckReadable(pass, filename); err != nil { return nil, err } - return os.ReadFile(filename) + return readFile(filename) } } // CheckReadable enforces the access policy defined by the ReadFile field of [analysis.Pass]. func CheckReadable(pass *analysis.Pass, filename string) error { - if slicesContains(pass.OtherFiles, filename) || - slicesContains(pass.IgnoredFiles, filename) { + if slices.Contains(pass.OtherFiles, filename) || + slices.Contains(pass.IgnoredFiles, filename) { return nil } for _, f := range pass.Files { @@ -36,13 +41,3 @@ func CheckReadable(pass *analysis.Pass, filename string) error { } return fmt.Errorf("Pass.ReadFile: %s is not among OtherFiles, IgnoredFiles, or names of Files", filename) } - -// TODO(adonovan): use go1.21 slices.Contains. -func slicesContains[S ~[]E, E comparable](slice S, x E) bool { - for _, elem := range slice { - if elem == x { - return true - } - } - return false -} diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/diff.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/diff.go index a13547b7..c12bdfd2 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/diff.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/diff.go @@ -7,6 +7,7 @@ package diff import ( "fmt" + "slices" "sort" "strings" ) @@ -64,7 +65,7 @@ func ApplyBytes(src []byte, edits []Edit) ([]byte, error) { // It may return a different slice. func validate(src string, edits []Edit) ([]Edit, int, error) { if !sort.IsSorted(editsSort(edits)) { - edits = append([]Edit(nil), edits...) + edits = slices.Clone(edits) SortEdits(edits) } diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/lcs/common.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/lcs/common.go index c3e82dd2..27fa9ecb 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/lcs/common.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/lcs/common.go @@ -51,7 +51,7 @@ func (l lcs) fix() lcs { // from the set of diagonals in l, find a maximal non-conflicting set // this problem may be NP-complete, but we use a greedy heuristic, // which is quadratic, but with a better data structure, could be D log D. - // indepedent is not enough: {0,3,1} and {3,0,2} can't both occur in an lcs + // independent is not enough: {0,3,1} and {3,0,2} can't both occur in an lcs // which has to have monotone x and y if len(l) == 0 { return nil diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/lcs/doc.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/lcs/doc.go index 9029dd20..aa4b0fb5 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/lcs/doc.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/lcs/doc.go @@ -139,7 +139,7 @@ computed labels. That is the worst case. Had the code noticed (x,y)=(u,v)=(3,3) from the edgegraph. The implementation looks for a number of special cases to try to avoid computing an extra forward path. If the two-sided algorithm has stop early (because D has become too large) it will have found a forward LCS and a -backwards LCS. Ideally these go with disjoint prefixes and suffixes of A and B, but disjointness may fail and the two +backwards LCS. Ideally these go with disjoint prefixes and suffixes of A and B, but disjointedness may fail and the two computed LCS may conflict. (An easy example is where A is a suffix of B, and shares a short prefix. The backwards LCS is all of A, and the forward LCS is a prefix of A.) The algorithm combines the two to form a best-effort LCS. In the worst case the forward partial LCS may have to diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/lcs/old.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/lcs/old.go index 4353da15..4c346706 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/lcs/old.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/lcs/old.go @@ -105,7 +105,7 @@ func forward(e *editGraph) lcs { return ans } // from D to D+1 - for D := 0; D < e.limit; D++ { + for D := range e.limit { e.setForward(D+1, -(D + 1), e.getForward(D, -D)) if ok, ans := e.fdone(D+1, -(D + 1)); ok { return ans @@ -199,13 +199,14 @@ func (e *editGraph) bdone(D, k int) (bool, lcs) { } // run the backward algorithm, until success or up to the limit on D. +// (used only by tests) func backward(e *editGraph) lcs { e.setBackward(0, 0, e.ux) if ok, ans := e.bdone(0, 0); ok { return ans } // from D to D+1 - for D := 0; D < e.limit; D++ { + for D := range e.limit { e.setBackward(D+1, -(D + 1), e.getBackward(D, -D)-1) if ok, ans := e.bdone(D+1, -(D + 1)); ok { return ans @@ -299,7 +300,7 @@ func twosided(e *editGraph) lcs { e.setBackward(0, 0, e.ux) // from D to D+1 - for D := 0; D < e.limit; D++ { + for D := range e.limit { // just finished a backwards pass, so check if got, ok := e.twoDone(D, D); ok { return e.twolcs(D, D, got) @@ -376,10 +377,7 @@ func (e *editGraph) twoDone(df, db int) (int, bool) { if (df+db+e.delta)%2 != 0 { return 0, false // diagonals cannot overlap } - kmin := -db + e.delta - if -df > kmin { - kmin = -df - } + kmin := max(-df, -db+e.delta) kmax := db + e.delta if df < kmax { kmax = df diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/ndiff.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/ndiff.go index 3a735583..66ceb8bf 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/ndiff.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/ndiff.go @@ -72,7 +72,7 @@ func diffRunes(before, after []rune) []Edit { func runes(bytes []byte) []rune { n := utf8.RuneCount(bytes) runes := make([]rune, n) - for i := 0; i < n; i++ { + for i := range n { r, sz := utf8.DecodeRune(bytes) bytes = bytes[sz:] runes[i] = r diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/unified.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/unified.go index cfbda610..9a786dbb 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/unified.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/unified.go @@ -129,12 +129,12 @@ func toUnified(fromName, toName string, content string, edits []Edit, contextLin switch { case h != nil && start == last: - //direct extension + // direct extension case h != nil && start <= last+gap: - //within range of previous lines, add the joiners + // within range of previous lines, add the joiners addEqualLines(h, lines, last, start) default: - //need to start a new hunk + // need to start a new hunk if h != nil { // add the edge to the previous hunk addEqualLines(h, lines, last, last+contextLines) diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/commands/custom.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/commands/custom.go index 8f04f012..083a884e 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/commands/custom.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/commands/custom.go @@ -5,6 +5,7 @@ import ( "log" "os" + "github.com/fatih/color" "github.com/spf13/cobra" "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/commands/internal" @@ -13,11 +14,19 @@ import ( const envKeepTempFiles = "CUSTOM_GCL_KEEP_TEMP_FILES" +type customOptions struct { + version string + name string + destination string +} + type customCommand struct { cmd *cobra.Command cfg *internal.Configuration + opts customOptions + log logutils.Log } @@ -33,6 +42,13 @@ func newCustomCommand(logger logutils.Log) *customCommand { SilenceUsage: true, } + flagSet := customCmd.PersistentFlags() + flagSet.SortFlags = false // sort them as they are defined here + + flagSet.StringVar(&c.opts.version, "version", "", color.GreenString("The golangci-lint version used to build the custom binary")) + flagSet.StringVar(&c.opts.name, "name", "", color.GreenString("The name of the custom binary")) + flagSet.StringVar(&c.opts.destination, "destination", "", color.GreenString("The directory path used to store the custom binary")) + c.cmd = customCmd return c @@ -44,6 +60,18 @@ func (c *customCommand) preRunE(_ *cobra.Command, _ []string) error { return err } + if c.opts.version != "" { + cfg.Version = c.opts.version + } + + if c.opts.name != "" { + cfg.Name = c.opts.name + } + + if c.opts.destination != "" { + cfg.Destination = c.opts.destination + } + err = cfg.Validate() if err != nil { return err diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/commands/flagsets.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/commands/flagsets.go index 708d2b84..cb549131 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/commands/flagsets.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/commands/flagsets.go @@ -138,5 +138,5 @@ func setupIssuesFlagSet(v *viper.Viper, fs *pflag.FlagSet) { internal.AddFlagAndBind(v, fs, fs.Bool, "whole-files", "issues.whole-files", false, color.GreenString("Show issues in any part of update files (requires new-from-rev or new-from-patch)")) internal.AddFlagAndBind(v, fs, fs.Bool, "fix", "issues.fix", false, - color.GreenString("Fix found issues (if it's supported by the linter)")) + color.GreenString("Apply the fixes detected by the linters and formatters (if it's supported by the linter)")) } diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/commands/fmt.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/commands/fmt.go index 8795bea4..13666f0d 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/commands/fmt.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/commands/fmt.go @@ -113,14 +113,11 @@ func (c *fmtCommand) preRunE(_ *cobra.Command, _ []string) error { } func (c *fmtCommand) execute(_ *cobra.Command, args []string) error { - paths, err := cleanArgs(args) - if err != nil { - return fmt.Errorf("failed to clean arguments: %w", err) - } + paths := cleanArgs(args) c.log.Infof("Formatting Go files...") - err = c.runner.Run(paths) + err := c.runner.Run(paths) if err != nil { return fmt.Errorf("failed to process files: %w", err) } @@ -134,25 +131,15 @@ func (c *fmtCommand) persistentPostRun(_ *cobra.Command, _ []string) { } } -func cleanArgs(args []string) ([]string, error) { +func cleanArgs(args []string) []string { if len(args) == 0 { - abs, err := filepath.Abs(".") - if err != nil { - return nil, err - } - - return []string{abs}, nil + return []string{"."} } var expanded []string for _, arg := range args { - abs, err := filepath.Abs(strings.ReplaceAll(arg, "...", "")) - if err != nil { - return nil, err - } - - expanded = append(expanded, abs) + expanded = append(expanded, filepath.Clean(strings.ReplaceAll(arg, "...", ""))) } - return expanded, nil + return expanded } diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/commands/internal/builder.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/commands/internal/builder.go index 93d4d65c..5b721f01 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/commands/internal/builder.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/commands/internal/builder.go @@ -92,7 +92,7 @@ func (b Builder) clone(ctx context.Context) error { //nolint:gosec // the variable is sanitized. cmd := exec.CommandContext(ctx, "git", "clone", "--branch", sanitizeVersion(b.cfg.Version), - "--single-branch", "--depth", "1", "-c advice.detachedHead=false", "-q", + "--single-branch", "--depth", "1", "-c", "advice.detachedHead=false", "-q", "https://github.com/golangci/golangci-lint.git", ) cmd.Dir = b.root @@ -257,7 +257,7 @@ func (b Builder) createVersion(orig string) (string, error) { continue } - dh, err := dirhash.HashDir(plugin.Path, "", dirhash.DefaultHash) + dh, err := hashDir(plugin.Path, "", dirhash.DefaultHash) if err != nil { return "", fmt.Errorf("hash plugin directory: %w", err) } diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/commands/internal/dirhash.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/commands/internal/dirhash.go new file mode 100644 index 00000000..16ea6a85 --- /dev/null +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/commands/internal/dirhash.go @@ -0,0 +1,93 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package internal + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "golang.org/x/mod/sumdb/dirhash" +) + +// Slightly modified copy of [dirhash.HashDir]. +// https://github.com/golang/mod/blob/v0.28.0/sumdb/dirhash/hash.go#L67-L79 +func hashDir(dir, prefix string, hash dirhash.Hash) (string, error) { + files, err := dirFiles(dir, prefix) + if err != nil { + return "", err + } + + osOpen := func(name string) (io.ReadCloser, error) { + return os.Open(filepath.Join(dir, strings.TrimPrefix(name, prefix))) + } + + return hash(files, osOpen) +} + +// Modified copy of [dirhash.DirFiles]. +// https://github.com/golang/mod/blob/v0.28.0/sumdb/dirhash/hash.go#L81-L109 +// And adapted to globally follows the rules from https://github.com/golang/mod/blob/v0.28.0/zip/zip.go +func dirFiles(dir, prefix string) ([]string, error) { + var files []string + + dir = filepath.Clean(dir) + + err := filepath.Walk(dir, func(file string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + if dir == file { + // Don't skip the top-level directory. + return nil + } + + switch info.Name() { + // Skip vendor and node directories. + case "vendor", "node_modules": + return filepath.SkipDir + + // Skip VCS directories. + case ".bzr", ".git", ".hg", ".svn": + return filepath.SkipDir + } + + // Skip submodules (directories containing go.mod files). + if goModInfo, err := os.Lstat(filepath.Join(dir, "go.mod")); err == nil && !goModInfo.IsDir() { + return filepath.SkipDir + } + + return nil + } + + if file == dir { + return fmt.Errorf("%s is not a directory", dir) + } + + if !info.Mode().IsRegular() { + return nil + } + + rel := file + + if dir != "." { + rel = file[len(dir)+1:] + } + + f := filepath.Join(prefix, rel) + + files = append(files, filepath.ToSlash(f)) + + return nil + }) + if err != nil { + return nil, err + } + return files, nil +} diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/commands/root.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/commands/root.go index 6cb03e48..690728a8 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/commands/root.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/commands/root.go @@ -127,7 +127,7 @@ func forceRootParsePersistentFlags() (*rootOptions, error) { fs := pflag.NewFlagSet("config flag set", pflag.ContinueOnError) // Ignore unknown flags because we will parse the command flags later. - fs.ParseErrorsWhitelist = pflag.ParseErrorsWhitelist{UnknownFlags: true} + fs.ParseErrorsAllowlist = pflag.ParseErrorsAllowlist{UnknownFlags: true} opts := &rootOptions{} diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/config/linters_settings.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/config/linters_settings.go index 9394f89b..fefa94ca 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/config/linters_settings.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/config/linters_settings.go @@ -24,6 +24,9 @@ var defaultLintersSettings = LintersSettings{ Dupl: DuplSettings{ Threshold: 150, }, + EmbeddedStructFieldCheck: EmbeddedStructFieldCheckSettings{ + EmptyLine: true, + }, ErrorLint: ErrorLintSettings{ Errorf: true, ErrorfMulti: true, @@ -128,6 +131,7 @@ var defaultLintersSettings = LintersSettings{ StrConcat: true, BoolFormat: true, HexFormat: true, + ConcatLoop: true, }, Prealloc: PreallocSettings{ Simple: true, @@ -159,6 +163,9 @@ var defaultLintersSettings = LintersSettings{ SkipRegexp: `(export|internal)_test\.go`, AllowPackages: []string{"main"}, }, + Unqueryvet: UnqueryvetSettings{ + CheckSQLBuilders: true, + }, Unused: UnusedSettings{ FieldWritesAreUses: true, PostStatementsAreReads: false, @@ -239,6 +246,7 @@ type LintersSettings struct { Goconst GoConstSettings `mapstructure:"goconst"` Gocritic GoCriticSettings `mapstructure:"gocritic"` Gocyclo GoCycloSettings `mapstructure:"gocyclo"` + Godoclint GodoclintSettings `mapstructure:"godoclint"` Godot GodotSettings `mapstructure:"godot"` Godox GodoxSettings `mapstructure:"godox"` Goheader GoHeaderSettings `mapstructure:"goheader"` @@ -246,12 +254,15 @@ type LintersSettings struct { Gomodguard GoModGuardSettings `mapstructure:"gomodguard"` Gosec GoSecSettings `mapstructure:"gosec"` Gosmopolitan GosmopolitanSettings `mapstructure:"gosmopolitan"` + Unqueryvet UnqueryvetSettings `mapstructure:"unqueryvet"` Govet GovetSettings `mapstructure:"govet"` Grouper GrouperSettings `mapstructure:"grouper"` Iface IfaceSettings `mapstructure:"iface"` ImportAs ImportAsSettings `mapstructure:"importas"` Inamedparam INamedParamSettings `mapstructure:"inamedparam"` + Ineffassign IneffassignSettings `mapstructure:"ineffassign"` InterfaceBloat InterfaceBloatSettings `mapstructure:"interfacebloat"` + IotaMixing IotaMixingSettings `mapstructure:"iotamixing"` Ireturn IreturnSettings `mapstructure:"ireturn"` Lll LllSettings `mapstructure:"lll"` LoggerCheck LoggerCheckSettings `mapstructure:"loggercheck"` @@ -259,6 +270,7 @@ type LintersSettings struct { Makezero MakezeroSettings `mapstructure:"makezero"` Misspell MisspellSettings `mapstructure:"misspell"` Mnd MndSettings `mapstructure:"mnd"` + Modernize ModernizeSettings `mapstructure:"modernize"` MustTag MustTagSettings `mapstructure:"musttag"` Nakedret NakedretSettings `mapstructure:"nakedret"` Nestif NestifSettings `mapstructure:"nestif"` @@ -374,12 +386,14 @@ type DuplSettings struct { } type DupWordSettings struct { - Keywords []string `mapstructure:"keywords"` - Ignore []string `mapstructure:"ignore"` + Keywords []string `mapstructure:"keywords"` + Ignore []string `mapstructure:"ignore"` + CommentsOnly bool `mapstructure:"comments-only"` } type EmbeddedStructFieldCheckSettings struct { ForbidMutex bool `mapstructure:"forbid-mutex"` + EmptyLine bool `mapstructure:"empty-line"` } type ErrcheckSettings struct { @@ -471,6 +485,7 @@ type GinkgoLinterSettings struct { ForbidSpecPollution bool `mapstructure:"forbid-spec-pollution"` ForceSucceedForFuncs bool `mapstructure:"force-succeed"` ForceAssertionDescription bool `mapstructure:"force-assertion-description"` + ForeToNot bool `mapstructure:"force-tonot"` } type GoChecksumTypeSettings struct { @@ -515,6 +530,24 @@ type GoCycloSettings struct { MinComplexity int `mapstructure:"min-complexity"` } +type GodoclintSettings struct { + Default *string `mapstructure:"default"` + Enable []string `mapstructure:"enable"` + Disable []string `mapstructure:"disable"` + Options struct { + MaxLen struct { + Length *uint `mapstructure:"length"` + } `mapstructure:"max-len"` + RequireDoc struct { + IgnoreExported *bool `mapstructure:"ignore-exported"` + IgnoreUnexported *bool `mapstructure:"ignore-unexported"` + } `mapstructure:"require-doc"` + StartWithName struct { + IncludeUnexported *bool `mapstructure:"include-unexported"` + } `mapstructure:"start-with-name"` + } `mapstructure:"options"` +} + type GodotSettings struct { Scope string `mapstructure:"scope"` Exclude []string `mapstructure:"exclude"` @@ -640,10 +673,18 @@ type INamedParamSettings struct { SkipSingleParam bool `mapstructure:"skip-single-param"` } +type IneffassignSettings struct { + CheckEscapingErrors bool `mapstructure:"check-escaping-errors"` +} + type InterfaceBloatSettings struct { Max int `mapstructure:"max"` } +type IotaMixingSettings struct { + ReportIndividual bool `mapstructure:"report-individual"` +} + type IreturnSettings struct { Allow []string `mapstructure:"allow"` Reject []string `mapstructure:"reject"` @@ -720,6 +761,10 @@ type MndSettings struct { IgnoredFunctions []string `mapstructure:"ignored-functions"` } +type ModernizeSettings struct { + Disable []string `mapstructure:"disable"` +} + type NoLintLintSettings struct { RequireExplanation bool `mapstructure:"require-explanation"` RequireSpecific bool `mapstructure:"require-specific"` @@ -751,6 +796,9 @@ type PerfSprintSettings struct { BoolFormat bool `mapstructure:"bool-format"` HexFormat bool `mapstructure:"hex-format"` + + ConcatLoop bool `mapstructure:"concat-loop"` + LoopOtherOps bool `mapstructure:"loop-other-ops"` } type PreallocSettings struct { @@ -786,15 +834,16 @@ type RecvcheckSettings struct { } type ReviveSettings struct { - Go string `mapstructure:"-"` - MaxOpenFiles int `mapstructure:"max-open-files"` - Confidence float64 `mapstructure:"confidence"` - Severity string `mapstructure:"severity"` - EnableAllRules bool `mapstructure:"enable-all-rules"` - Rules []ReviveRule `mapstructure:"rules"` - ErrorCode int `mapstructure:"error-code"` - WarningCode int `mapstructure:"warning-code"` - Directives []ReviveDirective `mapstructure:"directives"` + Go string `mapstructure:"-"` + MaxOpenFiles int `mapstructure:"max-open-files"` + Confidence float64 `mapstructure:"confidence"` + Severity string `mapstructure:"severity"` + EnableAllRules bool `mapstructure:"enable-all-rules"` + EnableDefaultRules bool `mapstructure:"enable-default-rules"` + Rules []ReviveRule `mapstructure:"rules"` + ErrorCode int `mapstructure:"error-code"` + WarningCode int `mapstructure:"warning-code"` + Directives []ReviveDirective `mapstructure:"directives"` } type ReviveRule struct { @@ -971,6 +1020,11 @@ type UnparamSettings struct { CheckExported bool `mapstructure:"check-exported"` } +type UnqueryvetSettings struct { + CheckSQLBuilders bool `mapstructure:"check-sql-builders"` + AllowedPatterns []string `mapstructure:"allowed-patterns"` +} + type UnusedSettings struct { FieldWritesAreUses bool `mapstructure:"field-writes-are-uses"` PostStatementsAreReads bool `mapstructure:"post-statements-are-reads"` diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/goanalysis/pkgerrors/extract.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/goanalysis/pkgerrors/extract.go index d1257e66..76a4c902 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/goanalysis/pkgerrors/extract.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/goanalysis/pkgerrors/extract.go @@ -2,6 +2,7 @@ package pkgerrors import ( "fmt" + "maps" "regexp" "strings" @@ -18,7 +19,9 @@ func extractErrors(pkg *packages.Package) []packages.Error { return errors } + skippedErrors := map[string]packages.Error{} seenErrors := map[string]bool{} + var uniqErrors []packages.Error for _, err := range errors { msg := stackCrusher(err.Error()) @@ -26,15 +29,35 @@ func extractErrors(pkg *packages.Package) []packages.Error { continue } + // This `if` is important to avoid duplicate errors. + // The goal is to keep the most relevant error. if msg != err.Error() { + prev, alreadySkip := skippedErrors[msg] + if !alreadySkip { + skippedErrors[msg] = err + continue + } + + if len(err.Error()) < len(prev.Error()) { + skippedErrors[msg] = err + } + continue } + delete(skippedErrors, msg) + seenErrors[msg] = true uniqErrors = append(uniqErrors, err) } + // In some cases, the error stack doesn't contain the tip error. + // We must keep at least one of the original errors that contain the specific message. + for skippedError := range maps.Values(skippedErrors) { + uniqErrors = append(uniqErrors, skippedError) + } + if len(pkg.GoFiles) != 0 { // errors were extracted from deps and have at least one file in package for i := range uniqErrors { diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/goanalysis/runner_checker.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/goanalysis/runner_checker.go index 13235a00..e5eb291f 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/goanalysis/runner_checker.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/goanalysis/runner_checker.go @@ -12,6 +12,7 @@ import ( "errors" "fmt" "go/types" + "os" "reflect" "time" @@ -160,7 +161,7 @@ func (act *action) analyze() { AllObjectFacts: act.AllObjectFacts, AllPackageFacts: act.AllPackageFacts, } - pass.ReadFile = analysisinternal.MakeReadFile(pass) + pass.ReadFile = analysisinternal.CheckedReadFile(pass, os.ReadFile) act.pass = pass act.runner.passToPkgGuard.Lock() diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/goanalysis/runner_loadingpackage.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/goanalysis/runner_loadingpackage.go index 47e0ea1d..3b8cb7d0 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/goanalysis/runner_loadingpackage.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/goanalysis/runner_loadingpackage.go @@ -63,21 +63,22 @@ func (lp *loadingPackage) analyzeRecursive(ctx context.Context, cancel context.C } func (lp *loadingPackage) analyze(ctx context.Context, cancel context.CancelFunc, loadMode LoadMode, loadSem chan struct{}) { - loadSem <- struct{}{} - defer func() { - <-loadSem - }() - select { case <-ctx.Done(): return - default: + case loadSem <- struct{}{}: + defer func() { + <-loadSem + }() } // Save memory on unused more fields. defer lp.decUse(loadMode < LoadModeWholeProgram) if err := lp.loadWithFacts(loadMode); err != nil { + // Note: this error is ignored when there is no facts loading (e.g. with 98% of linters). + // But this is not a problem because the errors are added to the package.Errors. + // You through an error, try to add it to actions, but there is no action annnddd it's gone! werr := fmt.Errorf("failed to load package %s: %w", lp.pkg.Name, err) // Don't need to write error to errCh, it will be extracted and reported on another layer. @@ -88,6 +89,10 @@ func (lp *loadingPackage) analyze(ctx context.Context, cancel context.CancelFunc act.Err = werr } + if len(lp.actions) == 0 { + lp.log.Warnf("no action but there is an error: %v", err) + } + return } @@ -239,9 +244,11 @@ func (lp *loadingPackage) loadFromExportData() error { return fmt.Errorf("dependency %q hasn't been loaded yet", path) } } + if pkg.ExportFile == "" { return fmt.Errorf("no export data for %q", pkg.ID) } + f, err := os.Open(pkg.ExportFile) if err != nil { return err @@ -332,13 +339,15 @@ func (lp *loadingPackage) loadImportedPackageWithFacts(loadMode LoadMode) error if srcErr := lp.loadFromSource(loadMode); srcErr != nil { return srcErr } + // Make sure this package can't be imported successfully pkg.Errors = append(pkg.Errors, packages.Error{ Pos: "-", Msg: fmt.Sprintf("could not load export data: %s", err), Kind: packages.ParseError, }) - return fmt.Errorf("could not load export data: %w", err) + + return nil } } diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/goformat/runner.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/goformat/runner.go index 0fd5765c..b0824fe4 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/goformat/runner.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/goformat/runner.go @@ -223,8 +223,14 @@ func NewRunnerOptions(cfg *config.Config, diff, diffColored, stdin bool) (Runner return RunnerOptions{}, fmt.Errorf("get base path: %w", err) } + // Required to be consistent with `RunnerOptions.MatchAnyPattern`. + absBasePath, err := filepath.Abs(basePath) + if err != nil { + return RunnerOptions{}, err + } + opts := RunnerOptions{ - basePath: basePath, + basePath: absBasePath, generated: cfg.Formatters.Exclusions.Generated, diff: diff || diffColored, colors: diffColored, @@ -251,7 +257,12 @@ func (o RunnerOptions) MatchAnyPattern(path string) (bool, error) { return false, nil } - rel, err := filepath.Rel(o.basePath, path) + abs, err := filepath.Abs(path) + if err != nil { + return false, err + } + + rel, err := filepath.Rel(o.basePath, abs) if err != nil { return false, err } @@ -272,7 +283,7 @@ func skipDir(name string) bool { return true default: - return strings.HasPrefix(name, ".") + return strings.HasPrefix(name, ".") && name != "." } } diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/asciicheck/asciicheck.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/asciicheck/asciicheck.go index ecf93450..d0db39bd 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/asciicheck/asciicheck.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/asciicheck/asciicheck.go @@ -1,7 +1,7 @@ package asciicheck import ( - "github.com/tdakkota/asciicheck" + "github.com/golangci/asciicheck" "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/goanalysis" ) diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/contextcheck/contextcheck.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/contextcheck/contextcheck.go index 3bfa4b49..6ebec627 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/contextcheck/contextcheck.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/contextcheck/contextcheck.go @@ -2,6 +2,8 @@ package contextcheck import ( "github.com/kkHAIKE/contextcheck" + "golang.org/x/tools/go/analysis/passes/ctrlflow" + "golang.org/x/tools/go/analysis/passes/inspect" "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/goanalysis" "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/lint/linter" @@ -9,6 +11,11 @@ import ( func New() *goanalysis.Linter { analyzer := contextcheck.NewAnalyzer(contextcheck.Configuration{}) + // TODO(ldez) there is a problem with this linter: + // I think the problem related to facts. + // The BuildSSA pass has been changed inside (0.39.0): + // https://github.com/golang/tools/commit/b74c09864920a69a4d2f6ef0ecb4f9cff226893a + analyzer.Requires = append(analyzer.Requires, ctrlflow.Analyzer, inspect.Analyzer) return goanalysis. NewLinterFromAnalyzer(analyzer). diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/dupword/dupword.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/dupword/dupword.go index b051f0b8..63947525 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/dupword/dupword.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/dupword/dupword.go @@ -14,8 +14,9 @@ func New(settings *config.DupWordSettings) *goanalysis.Linter { if settings != nil { cfg = map[string]any{ - "keyword": strings.Join(settings.Keywords, ","), - "ignore": strings.Join(settings.Ignore, ","), + "keyword": strings.Join(settings.Keywords, ","), + "ignore": strings.Join(settings.Ignore, ","), + "comments-only": settings.CommentsOnly, } } diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/embeddedstructfieldcheck/embeddedstructfieldcheck.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/embeddedstructfieldcheck/embeddedstructfieldcheck.go index 08245fb1..42e23195 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/embeddedstructfieldcheck/embeddedstructfieldcheck.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/embeddedstructfieldcheck/embeddedstructfieldcheck.go @@ -12,7 +12,8 @@ func New(settings *config.EmbeddedStructFieldCheckSettings) *goanalysis.Linter { if settings != nil { cfg = map[string]any{ - analyzer.ForbidMutexName: settings.ForbidMutex, + analyzer.ForbidMutexCheck: settings.ForbidMutex, + analyzer.EmptyLineCheck: settings.EmptyLine, } } diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/ginkgolinter/ginkgolinter.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/ginkgolinter/ginkgolinter.go index 6f473639..09f8b71b 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/ginkgolinter/ginkgolinter.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/ginkgolinter/ginkgolinter.go @@ -26,6 +26,7 @@ func New(settings *config.GinkgoLinterSettings) *goanalysis.Linter { ForbidSpecPollution: settings.ForbidSpecPollution, ForceSucceedForFuncs: settings.ForceSucceedForFuncs, ForceAssertionDescription: settings.ForceAssertionDescription, + ForeToNot: settings.ForeToNot, } } diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/godoclint/godoclint.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/godoclint/godoclint.go new file mode 100644 index 00000000..b14c08a6 --- /dev/null +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/godoclint/godoclint.go @@ -0,0 +1,109 @@ +package godoclint + +import ( + "errors" + "fmt" + "slices" + + glcompose "github.com/godoc-lint/godoc-lint/pkg/compose" + glconfig "github.com/godoc-lint/godoc-lint/pkg/config" + "github.com/godoc-lint/godoc-lint/pkg/model" + + "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/config" + "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/goanalysis" + "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/internal" +) + +func New(settings *config.GodoclintSettings) *goanalysis.Linter { + var pcfg glconfig.PlainConfig + + if settings != nil { + err := checkSettings(settings) + if err != nil { + internal.LinterLogger.Fatalf("godoclint: %v", err) + } + + // The following options are explicitly ignored: they must be handled globally with exclusions or nolint directives. + // - Include + // - Exclude + + // The following options are explicitly ignored: these options cannot work as expected because the global configuration about tests. + // - Options.MaxLenIncludeTests + // - Options.PkgDocIncludeTests + // - Options.SinglePkgDocIncludeTests + // - Options.RequirePkgDocIncludeTests + // - Options.RequireDocIncludeTests + // - Options.StartWithNameIncludeTests + // - Options.NoUnusedLinkIncludeTests + + pcfg = glconfig.PlainConfig{ + Default: settings.Default, + Enable: settings.Enable, + Disable: settings.Disable, + Options: &glconfig.PlainRuleOptions{ + MaxLenLength: settings.Options.MaxLen.Length, + MaxLenIncludeTests: pointer(true), + PkgDocIncludeTests: pointer(false), + SinglePkgDocIncludeTests: pointer(true), + RequirePkgDocIncludeTests: pointer(false), + RequireDocIncludeTests: pointer(true), + RequireDocIgnoreExported: settings.Options.RequireDoc.IgnoreExported, + RequireDocIgnoreUnexported: settings.Options.RequireDoc.IgnoreUnexported, + StartWithNameIncludeTests: pointer(false), + StartWithNameIncludeUnexported: settings.Options.StartWithName.IncludeUnexported, + NoUnusedLinkIncludeTests: pointer(true), + }, + } + } + + composition := glcompose.Compose(glcompose.CompositionConfig{ + BaseDirPlainConfig: &pcfg, + ExitFunc: func(_ int, err error) { + internal.LinterLogger.Errorf("godoclint: %v", err) + }, + }) + + return goanalysis. + NewLinterFromAnalyzer(composition.Analyzer.GetAnalyzer()). + WithLoadMode(goanalysis.LoadModeSyntax) +} + +func checkSettings(settings *config.GodoclintSettings) error { + switch deref(settings.Default) { + case string(model.DefaultSetAll): + if len(settings.Enable) > 0 { + return errors.New("cannot use 'enable' with 'default=all'") + } + + case string(model.DefaultSetNone): + if len(settings.Disable) > 0 { + return errors.New("cannot use 'disable' with 'default=none'") + } + + default: + for _, rule := range settings.Enable { + if slices.Contains(settings.Disable, rule) { + return fmt.Errorf("a rule cannot be enabled and disabled at the same time: '%s'", rule) + } + } + + for _, rule := range settings.Disable { + if slices.Contains(settings.Enable, rule) { + return fmt.Errorf("a rule cannot be enabled and disabled at the same time: '%s'", rule) + } + } + } + + return nil +} + +func pointer[T any](v T) *T { return &v } + +func deref[T any](v *T) T { + if v == nil { + var zero T + return zero + } + + return *v +} diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/govet/govet.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/govet/govet.go index b3f29de4..0685824e 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/govet/govet.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/govet/govet.go @@ -109,7 +109,7 @@ var ( waitgroup.Analyzer, } - // https://github.com/golang/go/blob/go1.23.0/src/cmd/vet/main.go#L55-L87 + // https://github.com/golang/go/blob/go1.25.2/src/cmd/vet/main.go#L57-L91 defaultAnalyzers = []*analysis.Analyzer{ appends.Analyzer, asmdecl.Analyzer, @@ -124,6 +124,7 @@ var ( directive.Analyzer, errorsas.Analyzer, framepointer.Analyzer, + hostport.Analyzer, httpresponse.Analyzer, ifaceassert.Analyzer, loopclosure.Analyzer, @@ -144,6 +145,7 @@ var ( unreachable.Analyzer, unsafeptr.Analyzer, unusedresult.Analyzer, + waitgroup.Analyzer, } ) diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/ineffassign/ineffassign.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/ineffassign/ineffassign.go index 8aaf0c88..3dccd785 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/ineffassign/ineffassign.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/ineffassign/ineffassign.go @@ -3,12 +3,21 @@ package ineffassign import ( "github.com/gordonklaus/ineffassign/pkg/ineffassign" + "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/config" "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/goanalysis" ) -func New() *goanalysis.Linter { +func New(settings *config.IneffassignSettings) *goanalysis.Linter { + var cfg map[string]any + + if settings != nil { + cfg = map[string]any{ + "check-escaping-errors": settings.CheckEscapingErrors, + } + } + return goanalysis. NewLinterFromAnalyzer(ineffassign.Analyzer). - WithDesc("Detects when assignments to existing variables are not used"). + WithConfig(cfg). WithLoadMode(goanalysis.LoadModeSyntax) } diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/iotamixing/iotamixing.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/iotamixing/iotamixing.go new file mode 100644 index 00000000..4b5d40f5 --- /dev/null +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/iotamixing/iotamixing.go @@ -0,0 +1,26 @@ +package iotamixing + +import ( + im "github.com/AdminBenni/iota-mixing/pkg/analyzer" + "github.com/AdminBenni/iota-mixing/pkg/analyzer/flags" + + "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/config" + "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/goanalysis" +) + +func New(settings *config.IotaMixingSettings) *goanalysis.Linter { + cfg := map[string]any{} + + if settings != nil { + cfg[flags.ReportIndividualFlagName] = settings.ReportIndividual + } + + analyzer := im.GetIotaMixingAnalyzer() + + flags.SetupFlags(&analyzer.Flags) + + return goanalysis. + NewLinterFromAnalyzer(analyzer). + WithConfig(cfg). + WithLoadMode(goanalysis.LoadModeSyntax) +} diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/modernize/modernize.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/modernize/modernize.go new file mode 100644 index 00000000..628a66db --- /dev/null +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/modernize/modernize.go @@ -0,0 +1,34 @@ +package modernize + +import ( + "slices" + + "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/config" + "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/goanalysis" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/modernize" +) + +func New(settings *config.ModernizeSettings) *goanalysis.Linter { + var analyzers []*analysis.Analyzer + + if settings == nil { + analyzers = modernize.Suite + } else { + for _, analyzer := range modernize.Suite { + if slices.Contains(settings.Disable, analyzer.Name) { + continue + } + + analyzers = append(analyzers, analyzer) + } + } + + return goanalysis.NewLinter( + "modernize", + "A suite of analyzers that suggest simplifications to Go code, using modern language and library features.", + analyzers, + nil). + WithLoadMode(goanalysis.LoadModeTypesInfo) +} diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/perfsprint/perfsprint.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/perfsprint/perfsprint.go index 023bafd5..8e7129a2 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/perfsprint/perfsprint.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/perfsprint/perfsprint.go @@ -28,6 +28,9 @@ func New(settings *config.PerfSprintSettings) *goanalysis.Linter { cfg["bool-format"] = settings.BoolFormat cfg["hex-format"] = settings.HexFormat + + cfg["concat-loop"] = settings.ConcatLoop + cfg["loop-other-ops"] = settings.LoopOtherOps } return goanalysis. diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/revive/revive.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/revive/revive.go index 1a1f1a3b..e931bd94 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/revive/revive.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/revive/revive.go @@ -169,8 +169,8 @@ func (w *wrapper) toIssue(pass *analysis.Pass, failure *lint.Failure) *goanalysi // This function mimics the GetConfig function of revive. // This allows to get default values and right types. // https://github.com/golangci/golangci-lint/issues/1745 -// https://github.com/mgechev/revive/blob/v1.6.0/config/config.go#L230 -// https://github.com/mgechev/revive/blob/v1.6.0/config/config.go#L182-L188 +// https://github.com/mgechev/revive/blob/v1.13.0/config/config.go#L249 +// https://github.com/mgechev/revive/blob/v1.13.0/config/config.go#L198-L204 func getConfig(cfg *config.ReviveSettings) (*lint.Config, error) { conf := defaultConfig() @@ -269,7 +269,7 @@ func safeTomlSlice(r []any) []any { } // This element is not exported by revive, so we need copy the code. -// Extracted from https://github.com/mgechev/revive/blob/v1.11.0/config/config.go#L166 +// Extracted from https://github.com/mgechev/revive/blob/v1.13.0/config/config.go#L16 var defaultRules = []lint.Rule{ &rule.VarDeclarationsRule{}, &rule.PackageCommentsRule{}, @@ -325,14 +325,20 @@ var allRules = append([]lint.Rule{ &rule.FileLengthLimitRule{}, &rule.FilenameFormatRule{}, &rule.FlagParamRule{}, + &rule.ForbiddenCallInWgGoRule{}, &rule.FunctionLength{}, &rule.FunctionResultsLimitRule{}, &rule.GetReturnRule{}, &rule.IdenticalBranchesRule{}, + &rule.IdenticalIfElseIfBranchesRule{}, + &rule.IdenticalIfElseIfConditionsRule{}, + &rule.IdenticalSwitchBranchesRule{}, + &rule.IdenticalSwitchConditionsRule{}, &rule.IfReturnRule{}, &rule.ImportAliasNamingRule{}, &rule.ImportsBlocklistRule{}, &rule.ImportShadowingRule{}, + &rule.InefficientMapLookupRule{}, &rule.LineLengthLimitRule{}, &rule.MaxControlNestingRule{}, &rule.MaxPublicStructsRule{}, @@ -340,6 +346,7 @@ var allRules = append([]lint.Rule{ &rule.ModifiesValRecRule{}, &rule.NestedStructs{}, &rule.OptimizeOperandsOrderRule{}, + &rule.PackageDirectoryMismatchRule{}, &rule.RangeValAddress{}, &rule.RangeValInClosureRule{}, &rule.RedundantBuildTagRule{}, @@ -355,19 +362,23 @@ var allRules = append([]lint.Rule{ &rule.UnexportedNamingRule{}, &rule.UnhandledErrorRule{}, &rule.UnnecessaryFormatRule{}, + &rule.UnnecessaryIfRule{}, &rule.UnnecessaryStmtRule{}, + &rule.UnsecureURLSchemeRule{}, &rule.UnusedReceiverRule{}, &rule.UseAnyRule{}, &rule.UseErrorsNewRule{}, &rule.UseFmtPrintRule{}, &rule.UselessBreak{}, + &rule.UselessFallthroughRule{}, + &rule.UseWaitGroupGoRule{}, &rule.WaitGroupByValueRule{}, }, defaultRules...) const defaultConfidence = 0.8 // This element is not exported by revive, so we need copy the code. -// Extracted from https://github.com/mgechev/revive/blob/v1.11.0/config/config.go#L198 +// Extracted from https://github.com/mgechev/revive/blob/v1.13.0/config/config.go#L209 func normalizeConfig(cfg *lint.Config) { // NOTE(ldez): this custom section for golangci-lint should be kept. // --- @@ -378,19 +389,22 @@ func normalizeConfig(cfg *lint.Config) { if len(cfg.Rules) == 0 { cfg.Rules = map[string]lint.RuleConfig{} } - if cfg.EnableAllRules { - // Add to the configuration all rules not yet present in it - for _, r := range allRules { + + addRules := func(config *lint.Config, rules []lint.Rule) { + for _, r := range rules { ruleName := r.Name() - _, alreadyInConf := cfg.Rules[ruleName] - if alreadyInConf { - continue + if _, ok := config.Rules[ruleName]; !ok { + config.Rules[ruleName] = lint.RuleConfig{} } - // Add the rule with an empty conf for - cfg.Rules[ruleName] = lint.RuleConfig{} } } + if cfg.EnableAllRules { + addRules(cfg, allRules) + } else if cfg.EnableDefaultRules { + addRules(cfg, defaultRules) + } + severity := cfg.Severity if severity != "" { for k, v := range cfg.Rules { @@ -409,7 +423,7 @@ func normalizeConfig(cfg *lint.Config) { } // This element is not exported by revive, so we need copy the code. -// Extracted from https://github.com/mgechev/revive/blob/v1.11.0/config/config.go#L266 +// Extracted from https://github.com/mgechev/revive/blob/v1.13.0/config/config.go#L280 func defaultConfig() *lint.Config { defaultConfig := lint.Config{ Confidence: defaultConfidence, @@ -455,7 +469,7 @@ func extractRulesName(rules []lint.Rule) []string { return names } -// Extracted from https://github.com/mgechev/revive/blob/v1.11.0/formatter/severity.go +// Extracted from https://github.com/mgechev/revive/blob/v1.13.0/formatter/severity.go // Modified to use pointers (related to hugeParam rule). func severity(cfg *lint.Config, failure *lint.Failure) lint.Severity { if cfg, ok := cfg.Rules[failure.RuleName]; ok && cfg.Severity == lint.SeverityError { diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/unqueryvet/unqueryvet.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/unqueryvet/unqueryvet.go new file mode 100644 index 00000000..b37d4208 --- /dev/null +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/unqueryvet/unqueryvet.go @@ -0,0 +1,24 @@ +package unqueryvet + +import ( + "github.com/MirrexOne/unqueryvet" + pkgconfig "github.com/MirrexOne/unqueryvet/pkg/config" + + "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/config" + "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/goanalysis" +) + +func New(settings *config.UnqueryvetSettings) *goanalysis.Linter { + cfg := pkgconfig.DefaultSettings() + + if settings != nil { + cfg.CheckSQLBuilders = settings.CheckSQLBuilders + if len(settings.AllowedPatterns) > 0 { + cfg.AllowedPatterns = settings.AllowedPatterns + } + } + + return goanalysis. + NewLinterFromAnalyzer(unqueryvet.NewWithConfig(&cfg)). + WithLoadMode(goanalysis.LoadModeSyntax) +} diff --git a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/lint/lintersdb/builder_linter.go b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/lint/lintersdb/builder_linter.go index e3be1fe2..403bbd6b 100644 --- a/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/lint/lintersdb/builder_linter.go +++ b/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/lint/lintersdb/builder_linter.go @@ -43,6 +43,7 @@ import ( "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/goconst" "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/gocritic" "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/gocyclo" + "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/godoclint" "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/godot" "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/godox" "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/gofmt" @@ -63,6 +64,7 @@ import ( "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/ineffassign" "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/interfacebloat" "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/intrange" + "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/iotamixing" "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/ireturn" "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/lll" "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/loggercheck" @@ -71,6 +73,7 @@ import ( "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/mirror" "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/misspell" "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/mnd" + "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/modernize" "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/musttag" "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/nakedret" "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/nestif" @@ -107,6 +110,7 @@ import ( "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/tparallel" "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/unconvert" "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/unparam" + "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/unqueryvet" "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/unused" "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/usestdlibvars" "github.com/palantir/godel-distgo-asset-dist-golangci-lint/generated_src/golangcilint/internal/github.com/golangci/golangci-lint/v2/pkg/golinters/usetesting" @@ -151,7 +155,7 @@ func (LinterBuilder) Build(cfg *config.Config) ([]*linter.Config, error) { linter.NewConfig(asciicheck.New()). WithSince("v1.26.0"). - WithURL("https://github.com/tdakkota/asciicheck"), + WithURL("https://github.com/golangci/asciicheck"), linter.NewConfig(bidichk.New(&cfg.Linters.Settings.BiDiChk)). WithSince("v1.43.0"). @@ -331,6 +335,10 @@ func (LinterBuilder) Build(cfg *config.Config) ([]*linter.Config, error) { WithSince("v1.0.0"). WithURL("https://github.com/fzipp/gocyclo"), + linter.NewConfig(godoclint.New(&cfg.Linters.Settings.Godoclint)). + WithSince("v2.5.0"). + WithURL("https://github.com/godoc-lint/godoc-lint"), + linter.NewConfig(godot.New(&cfg.Linters.Settings.Godot)). WithSince("v1.25.0"). WithAutoFix(). @@ -375,6 +383,11 @@ func (LinterBuilder) Build(cfg *config.Config) ([]*linter.Config, error) { WithSince("v1.22.0"). WithURL("https://github.com/tommy-muehle/go-mnd"), + linter.NewConfig(modernize.New(&cfg.Linters.Settings.Modernize)). + WithSince("v2.6.0"). + WithLoadForGoAnalysis(). + WithURL("https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize"), + linter.NewConfig(gomoddirectives.New(&cfg.Linters.Settings.GoModDirectives)). WithSince("v1.39.0"). WithURL("https://github.com/ldez/gomoddirectives"), @@ -424,7 +437,7 @@ func (LinterBuilder) Build(cfg *config.Config) ([]*linter.Config, error) { WithSince("v1.55.0"). WithURL("https://github.com/macabu/inamedparam"), - linter.NewConfig(ineffassign.New()). + linter.NewConfig(ineffassign.New(&cfg.Linters.Settings.Ineffassign)). WithGroups(config.GroupStandard). WithSince("v1.0.0"). WithURL("https://github.com/gordonklaus/ineffassign"), @@ -440,6 +453,10 @@ func (LinterBuilder) Build(cfg *config.Config) ([]*linter.Config, error) { WithURL("https://github.com/ckaznocha/intrange"). WithNoopFallback(cfg, linter.IsGoLowerThanGo122()), + linter.NewConfig(iotamixing.New(&cfg.Linters.Settings.IotaMixing)). + WithSince("v2.5.0"). + WithURL("https://github.com/AdminBenni/iota-mixing"), + linter.NewConfig(ireturn.New(&cfg.Linters.Settings.Ireturn)). WithSince("v1.43.0"). WithLoadForGoAnalysis(). @@ -597,7 +614,7 @@ func (LinterBuilder) Build(cfg *config.Config) ([]*linter.Config, error) { WithSince("v1.0.0"). WithLoadForGoAnalysis(). WithAutoFix(). - WithURL("https://staticcheck.dev/"), + WithURL("https://github.com/dominikh/go-tools"), linter.NewConfig(swaggo.New()). WithSince("v2.2.0"). @@ -652,6 +669,10 @@ func (LinterBuilder) Build(cfg *config.Config) ([]*linter.Config, error) { WithLoadForGoAnalysis(). WithURL("https://github.com/mvdan/unparam"), + linter.NewConfig(unqueryvet.New(&cfg.Linters.Settings.Unqueryvet)). + WithSince("v2.5.0"). + WithURL("https://github.com/MirrexOne/unqueryvet"), + linter.NewConfig(unused.New(&cfg.Linters.Settings.Unused)). WithGroups(config.GroupStandard). WithSince("v1.20.0"). diff --git a/go.mod b/go.mod index ca5c6fe6..f62fef99 100644 --- a/go.mod +++ b/go.mod @@ -8,12 +8,14 @@ require ( dev.gaijin.team/go/exhaustruct/v4 v4.0.0 github.com/4meepo/tagalign v1.4.3 github.com/Abirdcfly/dupword v0.1.7 + github.com/AdminBenni/iota-mixing v1.0.0 github.com/AlwxSin/noinlineerr v1.0.5 github.com/Antonboom/errname v1.1.1 github.com/Antonboom/nilnil v1.1.1 github.com/Antonboom/testifylint v1.6.4 github.com/BurntSushi/toml v1.5.0 github.com/Djarvur/go-err113 v0.1.1 + github.com/MirrexOne/unqueryvet v1.3.0 github.com/OpenPeeDeeP/depguard/v2 v2.2.1 github.com/alecthomas/chroma/v2 v2.20.0 github.com/alecthomas/go-check-sumtype v0.3.1 @@ -45,7 +47,9 @@ require ( github.com/go-critic/go-critic v0.14.2 github.com/go-viper/mapstructure/v2 v2.4.0 github.com/go-xmlfmt/xmlfmt v1.1.3 + github.com/godoc-lint/godoc-lint v0.10.2 github.com/gofrs/flock v0.13.0 + github.com/golangci/asciicheck v0.5.0 github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 github.com/golangci/go-printf-func-name v0.1.1 github.com/golangci/gofmt v0.0.0-20250704145412-3e58ba0443c6 @@ -76,7 +80,7 @@ require ( github.com/ldez/usetesting v0.5.0 github.com/leonklingele/grouper v1.1.2 github.com/macabu/inamedparam v0.2.0 - github.com/manuelarte/embeddedstructfieldcheck v0.3.0 + github.com/manuelarte/embeddedstructfieldcheck v0.4.0 github.com/manuelarte/funcorder v0.5.0 github.com/maratori/testableexamples v1.0.1 github.com/maratori/testpackage v1.1.2 @@ -118,7 +122,6 @@ require ( github.com/ssgreg/nlreturn/v2 v2.2.1 github.com/stbenjam/no-sprintf-host-port v0.3.1 github.com/stretchr/testify v1.11.1 - github.com/tdakkota/asciicheck v0.4.1 github.com/tetafro/godot v1.5.4 github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 github.com/timonwong/loggercheck v0.11.0 @@ -185,7 +188,7 @@ require ( github.com/go-toolsmith/typep v1.1.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/golang/snappy v1.0.0 // indirect - github.com/golangci/golangci-lint/v2 v2.4.0 // indirect + github.com/golangci/golangci-lint/v2 v2.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-containerregistry v0.20.7 // indirect github.com/gostaticanalysis/analysisutil v0.7.1 // indirect diff --git a/go.sum b/go.sum index 3f393bf8..6661f9d5 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/4meepo/tagalign v1.4.3 h1:Bnu7jGWwbfpAie2vyl63Zup5KuRv21olsPIha53BJr8 github.com/4meepo/tagalign v1.4.3/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c= github.com/Abirdcfly/dupword v0.1.7 h1:2j8sInznrje4I0CMisSL6ipEBkeJUJAmK1/lfoNGWrQ= github.com/Abirdcfly/dupword v0.1.7/go.mod h1:K0DkBeOebJ4VyOICFdppB23Q0YMOgVafM0zYW0n9lF4= +github.com/AdminBenni/iota-mixing v1.0.0 h1:Os6lpjG2dp/AE5fYBPAA1zfa2qMdCAWwPMCgpwKq7wo= +github.com/AdminBenni/iota-mixing v1.0.0/go.mod h1:i4+tpAaB+qMVIV9OK3m4/DAynOd5bQFaOu+2AhtBCNY= github.com/AlwxSin/noinlineerr v1.0.5 h1:RUjt63wk1AYWTXtVXbSqemlbVTb23JOSRiNsshj7TbY= github.com/AlwxSin/noinlineerr v1.0.5/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc= github.com/Antonboom/errname v1.1.1 h1:bllB7mlIbTVzO9jmSWVWLjxTEbGBVQ1Ff/ClQgtPw9Q= @@ -26,6 +28,8 @@ github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/MirrexOne/unqueryvet v1.3.0 h1:5slWSomgqpYU4zFuZ3NNOfOUxVPlXFDBPAVasZOGlAY= +github.com/MirrexOne/unqueryvet v1.3.0/go.mod h1:IWwCwMQlSWjAIteW0t+28Q5vouyktfujzYznSIWiuOg= github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4= github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= @@ -179,19 +183,23 @@ github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUW github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/godoc-lint/godoc-lint v0.10.2 h1:dksNgK+zebnVlj4Fx83CRnCmPO0qRat/9xfFsir1nfg= +github.com/godoc-lint/godoc-lint v0.10.2/go.mod h1:KleLcHu/CGSvkjUH2RvZyoK1MBC7pDQg4NxMYLcBBsw= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0= +github.com/golangci/asciicheck v0.5.0/go.mod h1:5RMNAInbNFw2krqN6ibBxN/zfRFa9S6tA1nPdM0l8qQ= github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 h1:WUvBfQL6EW/40l6OmeSBYQJNSif4O11+bmWEz+C7FYw= github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E= github.com/golangci/go-printf-func-name v0.1.1 h1:hIYTFJqAGp1iwoIfsNTpoq1xZAarogrvjO9AfiW3B4U= github.com/golangci/go-printf-func-name v0.1.1/go.mod h1:Es64MpWEZbh0UBtTAICOZiB+miW53w/K9Or/4QogJss= github.com/golangci/gofmt v0.0.0-20250704145412-3e58ba0443c6 h1:jlKy3uQkETB3zMBK8utduvojT+If2nDAM1pWpEzXjaY= github.com/golangci/gofmt v0.0.0-20250704145412-3e58ba0443c6/go.mod h1:OyaRySOXorMn8zJqFku8YsKptIhPkANyKKTMC+rqMCs= -github.com/golangci/golangci-lint/v2 v2.4.0 h1:qz6O6vr7kVzXJqyvHjHSz5fA3D+PM8v96QU5gxZCNWM= -github.com/golangci/golangci-lint/v2 v2.4.0/go.mod h1:Oq7vuAf6L1iNL34uHDcsIF6Mnc0amOPdsT3/GlpHD+I= +github.com/golangci/golangci-lint/v2 v2.7.0 h1:jcjBA3azjzV0VEsQaBaH/27WHeEQbnKzbWUhXQs+4DU= +github.com/golangci/golangci-lint/v2 v2.7.0/go.mod h1:ekW32uOX47mpRlfPlSscuJPprm6pCEYIDagudfLrx34= github.com/golangci/golines v0.0.0-20250821215611-d4663ad2c370 h1:O2u8NCU/gGczNpU7/yjZIAvXMHLwKCAKsNc8axyQPWU= github.com/golangci/golines v0.0.0-20250821215611-d4663ad2c370/go.mod h1:k9mmcyWKSTMcPPvQUCfRWWQ9VHJ1U9Dc0R7kaXAgtnQ= github.com/golangci/misspell v0.7.0 h1:4GOHr/T1lTW0hhR4tgaaV1WS/lJ+ncvYCoFKmqJsj0c= @@ -295,8 +303,8 @@ github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQ github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE= github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U= -github.com/manuelarte/embeddedstructfieldcheck v0.3.0 h1:VhGqK8gANDvFYDxQkjPbv7/gDJtsGU9k6qj/hC2hgso= -github.com/manuelarte/embeddedstructfieldcheck v0.3.0/go.mod h1:LSo/IQpPfx1dXMcX4ibZCYA7Yy6ayZHIaOGM70+1Wy8= +github.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww= +github.com/manuelarte/embeddedstructfieldcheck v0.4.0/go.mod h1:z8dFSyXqp+fC6NLDSljRJeNQJJDWnY7RoWFzV3PC6UM= github.com/manuelarte/funcorder v0.5.0 h1:llMuHXXbg7tD0i/LNw8vGnkDTHFpTnWqKPI85Rknc+8= github.com/manuelarte/funcorder v0.5.0/go.mod h1:Yt3CiUQthSBMBxjShjdXMexmzpP8YGvGLjrxJNkO2hA= github.com/maratori/testableexamples v1.0.1 h1:HfOQXs+XgfeRBJ+Wz0XfH+FHnoY9TVqL6Fcevpzy4q8= @@ -476,8 +484,6 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tdakkota/asciicheck v0.4.1 h1:bm0tbcmi0jezRA2b5kg4ozmMuGAFotKI3RZfrhfovg8= -github.com/tdakkota/asciicheck v0.4.1/go.mod h1:0k7M3rCfRXb0Z6bwgvkEIMleKH3kXNz9UqJ9Xuqopr8= github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA= github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag= diff --git a/vendor/github.com/AdminBenni/iota-mixing/LICENSE b/vendor/github.com/AdminBenni/iota-mixing/LICENSE new file mode 100644 index 00000000..06e009ff --- /dev/null +++ b/vendor/github.com/AdminBenni/iota-mixing/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Benedikt Aron Þjóðbjargarson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/AdminBenni/iota-mixing/pkg/analyzer/analyzer.go b/vendor/github.com/AdminBenni/iota-mixing/pkg/analyzer/analyzer.go new file mode 100644 index 00000000..4c56ebdd --- /dev/null +++ b/vendor/github.com/AdminBenni/iota-mixing/pkg/analyzer/analyzer.go @@ -0,0 +1,118 @@ +package analyzer + +import ( + "go/ast" + "go/token" + "log" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + + "github.com/AdminBenni/iota-mixing/pkg/analyzer/flags" +) + +func GetIotaMixingAnalyzer() *analysis.Analyzer { + return &analysis.Analyzer{ + Name: "iotamixing", + Doc: "checks if iotas are being used in const blocks with other non-iota declarations.", + Run: run, + Requires: []*analysis.Analyzer{inspect.Analyzer}, + } +} + +func run(pass *analysis.Pass) (interface{}, error) { + ASTInspector := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) //nolint:forcetypeassert // will always be correct type + + // we only need to check Generic Declarations + nodeFilter := []ast.Node{ + (*ast.GenDecl)(nil), + } + + ASTInspector.Preorder(nodeFilter, func(node ast.Node) { checkGenericDeclaration(node, pass) }) + + return interface{}(nil), nil +} + +func checkGenericDeclaration(node ast.Node, pass *analysis.Pass) { + decl := node.(*ast.GenDecl) //nolint:forcetypeassert // filtered for this node, will always be this type + + if decl.Tok != token.CONST { + return + } + + checkConstDeclaration(decl, pass) +} + +func checkConstDeclaration(decl *ast.GenDecl, pass *analysis.Pass) { + iotaFound := false + valued := make([]*ast.ValueSpec, 0, len(decl.Specs)) + + // traverse specs inside const block + for _, spec := range decl.Specs { + if specVal, ok := spec.(*ast.ValueSpec); ok { + iotaFound, valued = checkValueSpec(specVal, iotaFound, valued) + } + } + + if !iotaFound { + return + } + + // there was an iota, now depending on the report-individual flag we must either + // report the const block or all regular valued specs that are mixing with the iota + switch flags.ReportIndividualFlag() { + case flags.TrueString: + for _, value := range valued { + pass.Reportf( + value.Pos(), + "%s is a const with r-val in same const block as iota. keep iotas in separate const blocks", + getName(value), + ) + } + default: //nolint:gocritic // default logs error and falls through to "false" case, simplest in this order + log.Printf( + "warning: unsupported value '%s' for flag %s, assuming value 'false'.", + flags.ReportIndividualFlag(), flags.ReportIndividualFlagName, + ) + + fallthrough + case flags.FalseString: + if len(valued) == 0 { + return + } + + pass.Reportf(decl.Pos(), "iota mixing. keep iotas in separate blocks to consts with r-val") + } +} + +func checkValueSpec(spec *ast.ValueSpec, iotaFound bool, valued []*ast.ValueSpec) (bool, []*ast.ValueSpec) { + // traverse through values (r-val) of spec and look for iota + for _, expr := range spec.Values { + if idn, ok := expr.(*ast.Ident); ok && idn.Name == "iota" { + return true, valued + } + } + + // iota wasn't found, add to valued spec list if there is an r-val + if len(spec.Values) > 0 { + return iotaFound, append(valued, spec) + } + + return iotaFound, valued +} + +func getName(spec *ast.ValueSpec) string { + sb := strings.Builder{} + + for i, ident := range spec.Names { + sb.WriteString(ident.Name) + + if i < len(spec.Names)-1 { + sb.WriteString(", ") + } + } + + return sb.String() +} diff --git a/vendor/github.com/AdminBenni/iota-mixing/pkg/analyzer/flags/flags.go b/vendor/github.com/AdminBenni/iota-mixing/pkg/analyzer/flags/flags.go new file mode 100644 index 00000000..8f9b73b6 --- /dev/null +++ b/vendor/github.com/AdminBenni/iota-mixing/pkg/analyzer/flags/flags.go @@ -0,0 +1,23 @@ +package flags + +import "flag" + +const ( + TrueString = "true" + FalseString = "false" + + ReportIndividualFlagName = "report-individual" + reportIndividualFlagUsage = "whether or not to report individual consts rather than just the const block." +) + +var ( + reportIndividualFlag *string //nolint:gochecknoglobals // only used in this file, not too plussed +) + +func SetupFlags(flags *flag.FlagSet) { + reportIndividualFlag = flags.String(ReportIndividualFlagName, FalseString, reportIndividualFlagUsage) +} + +func ReportIndividualFlag() string { + return *reportIndividualFlag +} diff --git a/vendor/github.com/MirrexOne/unqueryvet/.gitignore b/vendor/github.com/MirrexOne/unqueryvet/.gitignore new file mode 100644 index 00000000..bd2a7877 --- /dev/null +++ b/vendor/github.com/MirrexOne/unqueryvet/.gitignore @@ -0,0 +1,43 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +/unqueryvet + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool +*.out +coverage.html + +# Dependency directories +vendor/ + +# Go workspace file +go.work +go.work.sum + +# IDE files +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Temporary files +*.tmp +*.log +go.work +.golangci.local.yml diff --git a/vendor/github.com/MirrexOne/unqueryvet/.golangci.yml b/vendor/github.com/MirrexOne/unqueryvet/.golangci.yml new file mode 100644 index 00000000..d604d323 --- /dev/null +++ b/vendor/github.com/MirrexOne/unqueryvet/.golangci.yml @@ -0,0 +1,20 @@ +version: "2" + +formatters: + enable: + - gofumpt + - goimports + settings: + gofumpt: + extra-rules: true + +linters: + exclusions: + warn-unused: true + presets: + - comments + - std-error-handling + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 diff --git a/vendor/github.com/MirrexOne/unqueryvet/LICENSE b/vendor/github.com/MirrexOne/unqueryvet/LICENSE new file mode 100644 index 00000000..278a6115 --- /dev/null +++ b/vendor/github.com/MirrexOne/unqueryvet/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 MirrexOne + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/MirrexOne/unqueryvet/Makefile b/vendor/github.com/MirrexOne/unqueryvet/Makefile new file mode 100644 index 00000000..d92d3068 --- /dev/null +++ b/vendor/github.com/MirrexOne/unqueryvet/Makefile @@ -0,0 +1,93 @@ +.PHONY: all test build fmt fmt-check lint clean install help + +# Default target +all: fmt test build + +# Run tests +test: + @echo "Running tests..." + @go test -v -race -coverprofile=coverage.out ./... + +# Build the binary +build: + @echo "Building unqueryvet..." + @go build -v ./cmd/unqueryvet + +# Format code with gofmt -s +fmt: + @echo "Formatting code..." + @find . -name "*.go" -not -path "./vendor/*" -exec gofmt -s -w {} + + @go fmt ./... + +# Check if code is formatted +fmt-check: + @echo "Checking code formatting..." + @if [ -n "$$(find . -name '*.go' -not -path './vendor/*' -exec gofmt -s -l {} +)" ]; then \ + echo "The following files need formatting:"; \ + find . -name '*.go' -not -path './vendor/*' -exec gofmt -s -l {} +; \ + exit 1; \ + else \ + echo "All files are properly formatted"; \ + fi + +# Run linter +lint: + @echo "Running linter..." + @if command -v golangci-lint > /dev/null 2>&1; then \ + ./lint-local.sh ./...; \ + else \ + echo "golangci-lint not installed. Install it from https://golangci-lint.run/usage/install/"; \ + exit 1; \ + fi + +# Clean build artifacts +clean: + @echo "Cleaning..." + @rm -f unqueryvet + @rm -f coverage.out + @rm -f .golangci.local.yml + @go clean + +# Install the binary +install: + @echo "Installing unqueryvet..." + @go install ./cmd/unqueryvet + +# Run unqueryvet on the project itself +check: + @echo "Running unqueryvet on project..." + @go run ./cmd/unqueryvet ./... + +# Generate coverage report +coverage: test + @echo "Generating coverage report..." + @go tool cover -html=coverage.out -o coverage.html + @echo "Coverage report generated: coverage.html" + +# Run benchmarks +bench: + @echo "Running benchmarks..." + @go test -bench=. -benchmem ./internal/analyzer + +# Update dependencies +deps: + @echo "Updating dependencies..." + @go mod tidy + @go mod verify + +# Help target +help: + @echo "Available targets:" + @echo " make - Format, test, and build" + @echo " make test - Run tests with race detection" + @echo " make build - Build the unqueryvet binary" + @echo " make fmt - Format all Go files with gofmt -s" + @echo " make fmt-check - Check if files are formatted" + @echo " make lint - Run golangci-lint" + @echo " make clean - Remove build artifacts" + @echo " make install - Install unqueryvet binary" + @echo " make check - Run unqueryvet on the project" + @echo " make coverage - Generate coverage report" + @echo " make bench - Run benchmarks" + @echo " make deps - Update and verify dependencies" + @echo " make help - Show this help message" diff --git a/vendor/github.com/MirrexOne/unqueryvet/README.md b/vendor/github.com/MirrexOne/unqueryvet/README.md new file mode 100644 index 00000000..407f9d28 --- /dev/null +++ b/vendor/github.com/MirrexOne/unqueryvet/README.md @@ -0,0 +1,260 @@ +# unqueryvet + +[![Go Report Card](https://goreportcard.com/badge/github.com/MirrexOne/unqueryvet)](https://goreportcard.com/report/github.com/MirrexOne/unqueryvet) +[![GoDoc](https://godoc.org/github.com/MirrexOne/unqueryvet?status.svg)](https://godoc.org/github.com/MirrexOne/unqueryvet) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) + +unqueryvet is a Go static analysis tool (linter) that detects `SELECT *` usage in SQL queries and SQL builders, encouraging explicit column selection for better performance, maintainability, and API stability. + +## Features + +- **Detects `SELECT *` in string literals** - Finds problematic queries in your Go code +- **Constants and variables support** - Detects `SELECT *` in const and var declarations +- **SQL Builder support** - Works with popular SQL builders like Squirrel, GORM, etc. +- **Highly configurable** - Extensive configuration options for different use cases +- **Supports `//nolint:unqueryvet`** - Standard Go linting suppression +- **golangci-lint integration** - Works seamlessly with golangci-lint +- **Zero false positives** - Smart pattern recognition for acceptable `SELECT *` usage +- **Fast and lightweight** - Built on golang.org/x/tools/go/analysis + +## Why avoid `SELECT *`? + +- **Performance**: Selecting unnecessary columns wastes network bandwidth and memory +- **Maintainability**: Schema changes can break your application unexpectedly +- **Security**: May expose sensitive data that shouldn't be returned +- **API Stability**: Adding new columns can break clients that depend on column order + +## Informative Error Messages + +Unqueryvet provides context-specific messages that explain WHY you should avoid `SELECT *`: + +```go +// Basic queries +query := "SELECT * FROM users" +// avoid SELECT * - explicitly specify needed columns for better performance, maintainability and stability + +// SQL Builders +query := squirrel.Select("*").From("users") +// avoid SELECT * in SQL builder - explicitly specify columns to prevent unnecessary data transfer and schema change issues + +// Empty Select() +query := squirrel.Select() +// SQL builder Select() without columns defaults to SELECT * - add specific columns with .Columns() method +``` + +## Quick Start + +### As a standalone tool + +```bash +go install github.com/MirrexOne/unqueryvet/cmd/unqueryvet@latest +unqueryvet ./... +``` + +### With golangci-lint (Recommended) + +Add to your `.golangci.yml`: + +```yaml +version: "2" + +linters: + enable: + - unqueryvet + + settings: + unqueryvet: + check-sql-builders: true + # By default, no functions are ignored - minimal configuration + # ignored-functions: + # - "fmt.Printf" + # - "log.Printf" + # allowed-patterns: + # - "SELECT \\* FROM information_schema\\..*" + # - "SELECT \\* FROM pg_catalog\\..*" +``` + +## Examples + +### Problematic code (will trigger warnings) + +```go +// Constants with SELECT * +const QueryUsers = "SELECT * FROM users" + +// Variables with SELECT * +var QueryOrders = "SELECT * FROM orders" + +// String literals with SELECT * +query := "SELECT * FROM users" +rows, err := db.Query("SELECT * FROM orders WHERE status = ?", "active") + +// SQL builders with SELECT * +query := squirrel.Select("*").From("products") +query := builder.Select().Columns("*").From("inventory") +``` + +### Good code (recommended) + +```go +// Constants with explicit columns +const QueryUsers = "SELECT id, name, email FROM users" + +// Variables with explicit columns +var QueryOrders = "SELECT id, status, total FROM orders" + +// String literals with explicit column selection +query := "SELECT id, name, email FROM users" +rows, err := db.Query("SELECT id, total FROM orders WHERE status = ?", "active") + +// SQL builders with explicit columns +query := squirrel.Select("id", "name", "price").From("products") +query := builder.Select().Columns("id", "quantity", "location").From("inventory") +``` + +### Acceptable SELECT * usage (won't trigger warnings) + +```go +// System/meta queries +"SELECT * FROM information_schema.tables" +"SELECT * FROM pg_catalog.pg_tables" + +// Aggregate functions +"SELECT COUNT(*) FROM users" +"SELECT MAX(*) FROM scores" + +// With nolint suppression +query := "SELECT * FROM debug_table" //nolint:unqueryvet +``` + +## Configuration + +Unqueryvet is highly configurable to fit your project's needs: + +```yaml +version: "2" + +linters: + settings: + unqueryvet: + # Enable/disable SQL builder checking (default: true) + check-sql-builders: true + + # Default allowed patterns (automatically included): + # - COUNT(*), MAX(*), MIN(*) functions + # - information_schema, pg_catalog, sys schema queries + # You can add more patterns if needed: + # allowed-patterns: + # - "SELECT \\* FROM temp_.*" +``` + +## Supported SQL Builders + +Unqueryvet supports popular SQL builders out of the box: + +- **Squirrel** - `squirrel.Select("*")`, `Select().Columns("*")` +- **GORM** - Custom query methods +- **SQLBoiler** - Generated query methods +- **Custom builders** - Any builder using `Select()` patterns + +## Integration Examples + +### GitHub Actions + +```yaml +name: Lint +on: [push, pull_request] +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 + - name: golangci-lint + uses: golangci/golangci-lint-action@v8 + with: + version: latest + args: --enable unqueryvet +``` + +## Command Line Options + +When used as a standalone tool: + +```bash +# Check all packages +unqueryvet ./... + +# Check specific packages +unqueryvet ./cmd/... ./internal/... + +# With custom config file +unqueryvet -config=.unqueryvet.yml ./... + +# Verbose output +unqueryvet -v ./... +``` + +## Performance + +Unqueryvet is designed to be fast and lightweight: + +- **Parallel processing**: Analyzes multiple files concurrently +- **Incremental analysis**: Only analyzes changed files when possible +- **Minimal memory footprint**: Efficient AST traversal +- **Smart caching**: Reuses analysis results when appropriate + +## Advanced Usage + +### Custom Patterns + +You can define custom regex patterns for acceptable `SELECT *` usage: + +```yaml +allowed-patterns: + # Allow SELECT * from temporary tables + - "SELECT \\* FROM temp_\\w+" + # Allow SELECT * in migration scripts + - "SELECT \\* FROM.*-- migration" + # Allow SELECT * for specific schemas + - "SELECT \\* FROM audit\\..+" +``` + +### Integration with Custom SQL Builders + +For custom SQL builders, Unqueryvet looks for these patterns: + +```go +// Method chaining +builder.Select("*") // Direct SELECT * +builder.Select().Columns("*") // Chained SELECT * + +// Variable tracking +query := builder.Select() // Empty select +// If no .Columns() call follows, triggers warning +``` + +### Running Tests + +```bash +go test ./... +go test -race ./... +go test -bench=. ./... +``` + +### Development Setup + +```bash +git clone https://github.com/MirrexOne/unqueryvet.git +cd unqueryvet +go mod tidy +go test ./... +``` + +## License + +MIT License - see [LICENSE](LICENSE) file for details. + +## Support + +- **Bug Reports**: [GitHub Issues](https://github.com/MirrexOne/unqueryvet/issues) diff --git a/vendor/github.com/MirrexOne/unqueryvet/analyzer.go b/vendor/github.com/MirrexOne/unqueryvet/analyzer.go new file mode 100644 index 00000000..28c3ba6e --- /dev/null +++ b/vendor/github.com/MirrexOne/unqueryvet/analyzer.go @@ -0,0 +1,27 @@ +// Package unqueryvet provides a Go static analysis tool that detects SELECT * usage +package unqueryvet + +import ( + "golang.org/x/tools/go/analysis" + + "github.com/MirrexOne/unqueryvet/internal/analyzer" + "github.com/MirrexOne/unqueryvet/pkg/config" +) + +// Analyzer is the main unqueryvet analyzer instance +// This is the primary export that golangci-lint will use +var Analyzer = analyzer.NewAnalyzer() + +// New creates a new instance of the unqueryvet analyzer +func New() *analysis.Analyzer { + return Analyzer +} + +// NewWithConfig creates a new analyzer instance with custom configuration +// This is the recommended way to use unqueryvet with custom settings +func NewWithConfig(cfg *config.UnqueryvetSettings) *analysis.Analyzer { + if cfg == nil { + return Analyzer + } + return analyzer.NewAnalyzerWithSettings(*cfg) +} diff --git a/vendor/github.com/MirrexOne/unqueryvet/config.go b/vendor/github.com/MirrexOne/unqueryvet/config.go new file mode 100644 index 00000000..03626ad3 --- /dev/null +++ b/vendor/github.com/MirrexOne/unqueryvet/config.go @@ -0,0 +1,11 @@ +package unqueryvet + +import "github.com/MirrexOne/unqueryvet/pkg/config" + +// Settings is a type alias for UnqueryvetSettings from the config package. +type Settings = config.UnqueryvetSettings + +// DefaultSettings returns the default configuration for Unqueryvet. +func DefaultSettings() Settings { + return config.DefaultSettings() +} diff --git a/vendor/github.com/MirrexOne/unqueryvet/internal/analyzer/analyzer.go b/vendor/github.com/MirrexOne/unqueryvet/internal/analyzer/analyzer.go new file mode 100644 index 00000000..ce9b9874 --- /dev/null +++ b/vendor/github.com/MirrexOne/unqueryvet/internal/analyzer/analyzer.go @@ -0,0 +1,465 @@ +// Package analyzer provides the SQL static analysis implementation for detecting SELECT * usage. +package analyzer + +import ( + "go/ast" + "go/token" + "regexp" + "strconv" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + + "github.com/MirrexOne/unqueryvet/pkg/config" +) + +const ( + // selectKeyword is the SQL SELECT method name in builders + selectKeyword = "Select" + // columnKeyword is the SQL Column method name in builders + columnKeyword = "Column" + // columnsKeyword is the SQL Columns method name in builders + columnsKeyword = "Columns" + // defaultWarningMessage is the standard warning for SELECT * usage + defaultWarningMessage = "avoid SELECT * - explicitly specify needed columns for better performance, maintainability and stability" +) + +// NewAnalyzer creates the Unqueryvet analyzer with enhanced logic for production use +func NewAnalyzer() *analysis.Analyzer { + return &analysis.Analyzer{ + Name: "unqueryvet", + Doc: "detects SELECT * in SQL queries and SQL builders, preventing performance issues and encouraging explicit column selection", + Run: run, + Requires: []*analysis.Analyzer{inspect.Analyzer}, + } +} + +// NewAnalyzerWithSettings creates analyzer with provided settings for golangci-lint integration +func NewAnalyzerWithSettings(s config.UnqueryvetSettings) *analysis.Analyzer { + return &analysis.Analyzer{ + Name: "unqueryvet", + Doc: "detects SELECT * in SQL queries and SQL builders, preventing performance issues and encouraging explicit column selection", + Run: func(pass *analysis.Pass) (any, error) { + return RunWithConfig(pass, &s) + }, + Requires: []*analysis.Analyzer{inspect.Analyzer}, + } +} + +// RunWithConfig performs analysis with provided configuration +// This is the main entry point for configured analysis +func RunWithConfig(pass *analysis.Pass, cfg *config.UnqueryvetSettings) (any, error) { + insp := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + + // Use provided configuration or default if nil + if cfg == nil { + defaultSettings := config.DefaultSettings() + cfg = &defaultSettings + } + + // Define AST node types we're interested in + nodeFilter := []ast.Node{ + (*ast.CallExpr)(nil), // Function/method calls + (*ast.File)(nil), // Files (for SQL builder analysis) + (*ast.AssignStmt)(nil), // Assignment statements for standalone literals + (*ast.GenDecl)(nil), // General declarations (const, var, type) + } + + // Walk through all AST nodes and analyze them + insp.Preorder(nodeFilter, func(n ast.Node) { + switch node := n.(type) { + case *ast.File: + // Analyze SQL builders only if enabled in configuration + if cfg.CheckSQLBuilders { + analyzeSQLBuilders(pass, node) + } + case *ast.AssignStmt: + // Check assignment statements for standalone SQL literals + checkAssignStmt(pass, node, cfg) + case *ast.GenDecl: + // Check constant and variable declarations + checkGenDecl(pass, node, cfg) + case *ast.CallExpr: + // Analyze function calls for SQL with SELECT * usage + checkCallExpr(pass, node, cfg) + } + }) + + return nil, nil +} + +// run performs the main analysis of Go code files for SELECT * usage +func run(pass *analysis.Pass) (any, error) { + insp := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + + // Define AST node types we're interested in + nodeFilter := []ast.Node{ + (*ast.CallExpr)(nil), // Function/method calls + (*ast.File)(nil), // Files (for SQL builder analysis) + (*ast.AssignStmt)(nil), // Assignment statements for standalone literals + (*ast.GenDecl)(nil), // General declarations (const, var) + } + + // Always use default settings since passing settings through ResultOf doesn't work reliably + defaultSettings := config.DefaultSettings() + cfg := &defaultSettings + + // Walk through all AST nodes and analyze them + insp.Preorder(nodeFilter, func(n ast.Node) { + switch node := n.(type) { + case *ast.File: + // Analyze SQL builders only if enabled in configuration + if cfg.CheckSQLBuilders { + analyzeSQLBuilders(pass, node) + } + case *ast.AssignStmt: + // Check assignment statements for standalone SQL literals + checkAssignStmt(pass, node, cfg) + case *ast.GenDecl: + // Check constant and variable declarations + checkGenDecl(pass, node, cfg) + case *ast.CallExpr: + // Analyze function calls for SQL with SELECT * usage + checkCallExpr(pass, node, cfg) + } + }) + + return nil, nil +} + +// checkAssignStmt checks assignment statements for standalone SQL literals +func checkAssignStmt(pass *analysis.Pass, stmt *ast.AssignStmt, cfg *config.UnqueryvetSettings) { + // Check right-hand side expressions for string literals with SELECT * + for _, expr := range stmt.Rhs { + // Only check direct string literals, not function calls + if lit, ok := expr.(*ast.BasicLit); ok && lit.Kind == token.STRING { + content := normalizeSQLQuery(lit.Value) + if isSelectStarQuery(content, cfg) { + pass.Report(analysis.Diagnostic{ + Pos: lit.Pos(), + Message: getWarningMessage(), + }) + } + } + } +} + +// checkGenDecl checks general declarations (const, var) for SELECT * in SQL queries +func checkGenDecl(pass *analysis.Pass, decl *ast.GenDecl, cfg *config.UnqueryvetSettings) { + // Only check const and var declarations + if decl.Tok != token.CONST && decl.Tok != token.VAR { + return + } + + // Iterate through all specifications in the declaration + for _, spec := range decl.Specs { + // Type assert to ValueSpec (const/var specifications) + valueSpec, ok := spec.(*ast.ValueSpec) + if !ok { + continue + } + + // Check all values in the specification + for _, value := range valueSpec.Values { + // Only check direct string literals + if lit, ok := value.(*ast.BasicLit); ok && lit.Kind == token.STRING { + content := normalizeSQLQuery(lit.Value) + if isSelectStarQuery(content, cfg) { + pass.Report(analysis.Diagnostic{ + Pos: lit.Pos(), + Message: getWarningMessage(), + }) + } + } + } + } +} + +// checkCallExpr analyzes function calls for SQL with SELECT * usage +// Includes checking arguments and SQL builders +func checkCallExpr(pass *analysis.Pass, call *ast.CallExpr, cfg *config.UnqueryvetSettings) { + // Check SQL builders for SELECT * in arguments + if cfg.CheckSQLBuilders && isSQLBuilderSelectStar(call) { + pass.Report(analysis.Diagnostic{ + Pos: call.Pos(), + Message: getDetailedWarningMessage("sql_builder"), + }) + return + } + + // Check function call arguments for strings with SELECT * + for _, arg := range call.Args { + if lit, ok := arg.(*ast.BasicLit); ok && lit.Kind == token.STRING { + content := normalizeSQLQuery(lit.Value) + if isSelectStarQuery(content, cfg) { + pass.Report(analysis.Diagnostic{ + Pos: lit.Pos(), + Message: getWarningMessage(), + }) + } + } + } +} + +// NormalizeSQLQuery normalizes SQL query for analysis with advanced escape sequence handling. +// Exported for testing purposes. +func NormalizeSQLQuery(query string) string { + return normalizeSQLQuery(query) +} + +func normalizeSQLQuery(query string) string { + if len(query) < 2 { + return query + } + + first, last := query[0], query[len(query)-1] + + // 1. Handle different quote types with escape sequence processing + if first == '"' && last == '"' { + // For regular strings check for escape sequences + if !strings.Contains(query, "\\") { + query = trimQuotes(query) + } else if unquoted, err := strconv.Unquote(query); err == nil { + // Use standard Go unquoting for proper escape sequence handling + query = unquoted + } else { + // Fallback: simple quote removal + query = trimQuotes(query) + } + } else if first == '`' && last == '`' { + // Raw strings - simply remove backticks + query = trimQuotes(query) + } + + // 2. Process comments line by line before normalization + lines := strings.Split(query, "\n") + var processedParts []string + + for _, line := range lines { + // Remove comments from current line + if idx := strings.Index(line, "--"); idx != -1 { + line = line[:idx] + } + + // Add non-empty lines + if trimmed := strings.TrimSpace(line); trimmed != "" { + processedParts = append(processedParts, trimmed) + } + } + + // 3. Reassemble query and normalize + query = strings.Join(processedParts, " ") + query = strings.ToUpper(query) + query = strings.ReplaceAll(query, "\t", " ") + query = regexp.MustCompile(`\s+`).ReplaceAllString(query, " ") + + return strings.TrimSpace(query) +} + +// trimQuotes removes first and last character (quotes) +func trimQuotes(query string) string { + return query[1 : len(query)-1] +} + +// IsSelectStarQuery determines if query contains SELECT * with enhanced allowed patterns support. +// Exported for testing purposes. +func IsSelectStarQuery(query string, cfg *config.UnqueryvetSettings) bool { + return isSelectStarQuery(query, cfg) +} + +func isSelectStarQuery(query string, cfg *config.UnqueryvetSettings) bool { + // Check allowed patterns first - if query matches an allowed pattern, ignore it + for _, pattern := range cfg.AllowedPatterns { + if matched, _ := regexp.MatchString(pattern, query); matched { + return false + } + } + + // Check for SELECT * in query (case-insensitive) + upperQuery := strings.ToUpper(query) + if strings.Contains(upperQuery, "SELECT *") { //nolint:unqueryvet + // Ensure this is actually an SQL query by checking for SQL keywords + sqlKeywords := []string{"FROM", "WHERE", "JOIN", "GROUP", "ORDER", "HAVING", "UNION", "LIMIT"} + for _, keyword := range sqlKeywords { + if strings.Contains(upperQuery, keyword) { + return true + } + } + + // Also check if it's just "SELECT *" without other keywords (still problematic) + trimmed := strings.TrimSpace(upperQuery) + if trimmed == "SELECT *" { + return true + } + } + return false +} + +// getWarningMessage returns informative warning message +func getWarningMessage() string { + return defaultWarningMessage +} + +// getDetailedWarningMessage returns context-specific warning message +func getDetailedWarningMessage(context string) string { + switch context { + case "sql_builder": + return "avoid SELECT * in SQL builder - explicitly specify columns to prevent unnecessary data transfer and schema change issues" + case "nested": + return "avoid SELECT * in subquery - can cause performance issues and unexpected results when schema changes" + case "empty_select": + return "SQL builder Select() without columns defaults to SELECT * - add specific columns with .Columns() method" + default: + return defaultWarningMessage + } +} + +// isSQLBuilderSelectStar checks SQL builder method calls for SELECT * usage +func isSQLBuilderSelectStar(call *ast.CallExpr) bool { + fun, ok := call.Fun.(*ast.SelectorExpr) + if !ok { + return false + } + + // Check that this is a Select method call + if fun.Sel == nil || fun.Sel.Name != selectKeyword { + return false + } + + if len(call.Args) == 0 { + return false + } + + // Check Select method arguments for "*" or empty strings + for _, arg := range call.Args { + if lit, ok := arg.(*ast.BasicLit); ok && lit.Kind == token.STRING { + value := strings.Trim(lit.Value, "`\"") + // Consider both "*" and empty strings in Select() as problematic + if value == "*" || value == "" { + return true + } + } + } + + return false +} + +// analyzeSQLBuilders performs advanced SQL builder analysis +// Key logic for handling edge-cases like Select().Columns("*") +func analyzeSQLBuilders(pass *analysis.Pass, file *ast.File) { + // Track SQL builder variables and their state + builderVars := make(map[string]*ast.CallExpr) // Variables with empty Select() calls + hasColumns := make(map[string]bool) // Flag: were columns added for variable + + // First pass: find variables created with empty Select() calls + ast.Inspect(file, func(n ast.Node) bool { + switch node := n.(type) { + case *ast.AssignStmt: + // Analyze assignments like: query := builder.Select() + for i, expr := range node.Rhs { + if call, ok := expr.(*ast.CallExpr); ok { + if isEmptySelectCall(call) { + // Found empty Select() call, remember the variable + if i < len(node.Lhs) { + if ident, ok := node.Lhs[i].(*ast.Ident); ok { + builderVars[ident.Name] = call + hasColumns[ident.Name] = false + } + } + } + } + } + } + return true + }) + + // Second pass: check usage of Columns/Column methods + ast.Inspect(file, func(n ast.Node) bool { + switch node := n.(type) { + case *ast.CallExpr: + if sel, ok := node.Fun.(*ast.SelectorExpr); ok { + // Check calls to Columns() or Column() methods + if sel.Sel != nil && (sel.Sel.Name == columnsKeyword || sel.Sel.Name == columnKeyword) { + // Check for "*" in arguments + if hasStarInColumns(node) { + pass.Report(analysis.Diagnostic{ + Pos: node.Pos(), + Message: getDetailedWarningMessage("sql_builder"), + }) + } + + // Update variable state - columns were added + if ident, ok := sel.X.(*ast.Ident); ok { + if _, exists := builderVars[ident.Name]; exists { + if !hasStarInColumns(node) { + hasColumns[ident.Name] = true + } + } + } + } + } + + // Check call chains like builder.Select().Columns("*") + if isSelectWithColumns(node) { + if hasStarInColumns(node) { + if sel, ok := node.Fun.(*ast.SelectorExpr); ok && sel.Sel != nil { + pass.Report(analysis.Diagnostic{ + Pos: node.Pos(), + Message: getDetailedWarningMessage("sql_builder"), + }) + } + } + return true + } + } + return true + }) + + // Final check: warn about builders with empty Select() without subsequent columns + for varName, call := range builderVars { + if !hasColumns[varName] { + pass.Report(analysis.Diagnostic{ + Pos: call.Pos(), + Message: getDetailedWarningMessage("empty_select"), + }) + } + } +} + +// isEmptySelectCall checks if call is an empty Select() +func isEmptySelectCall(call *ast.CallExpr) bool { + if sel, ok := call.Fun.(*ast.SelectorExpr); ok { + if sel.Sel != nil && sel.Sel.Name == selectKeyword && len(call.Args) == 0 { + return true + } + } + return false +} + +// isSelectWithColumns checks call chains like Select().Columns() +func isSelectWithColumns(call *ast.CallExpr) bool { + if sel, ok := call.Fun.(*ast.SelectorExpr); ok { + if sel.Sel != nil && (sel.Sel.Name == columnsKeyword || sel.Sel.Name == columnKeyword) { + // Check that previous call in chain is Select() + if innerCall, ok := sel.X.(*ast.CallExpr); ok { + return isEmptySelectCall(innerCall) + } + } + } + return false +} + +// hasStarInColumns checks if call arguments contain "*" symbol +func hasStarInColumns(call *ast.CallExpr) bool { + for _, arg := range call.Args { + if lit, ok := arg.(*ast.BasicLit); ok && lit.Kind == token.STRING { + value := strings.Trim(lit.Value, "`\"") + if value == "*" { + return true + } + } + } + return false +} diff --git a/vendor/github.com/MirrexOne/unqueryvet/pkg/config/config.go b/vendor/github.com/MirrexOne/unqueryvet/pkg/config/config.go new file mode 100644 index 00000000..034c324d --- /dev/null +++ b/vendor/github.com/MirrexOne/unqueryvet/pkg/config/config.go @@ -0,0 +1,27 @@ +// Package config provides configuration structures for Unqueryvet analyzer. +package config + +// UnqueryvetSettings holds the configuration for the Unqueryvet analyzer. +type UnqueryvetSettings struct { + // CheckSQLBuilders enables checking SQL builders like Squirrel for SELECT * usage + CheckSQLBuilders bool `mapstructure:"check-sql-builders" json:"check-sql-builders" yaml:"check-sql-builders"` + + // AllowedPatterns is a list of regex patterns that are allowed to use SELECT * + // Example: ["SELECT \\* FROM temp_.*", "SELECT \\* FROM .*_backup"] + AllowedPatterns []string `mapstructure:"allowed-patterns" json:"allowed-patterns" yaml:"allowed-patterns"` +} + +// DefaultSettings returns the default configuration for unqueryvet +func DefaultSettings() UnqueryvetSettings { + return UnqueryvetSettings{ + CheckSQLBuilders: true, + AllowedPatterns: []string{ + `(?i)COUNT\(\s*\*\s*\)`, + `(?i)MAX\(\s*\*\s*\)`, + `(?i)MIN\(\s*\*\s*\)`, + `(?i)SELECT \* FROM information_schema\..*`, + `(?i)SELECT \* FROM pg_catalog\..*`, + `(?i)SELECT \* FROM sys\..*`, + }, + } +} diff --git a/vendor/github.com/godoc-lint/godoc-lint/LICENSE b/vendor/github.com/godoc-lint/godoc-lint/LICENSE new file mode 100644 index 00000000..f51a8f9b --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Babak K. Shandiz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/analysis/analyzer.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/analysis/analyzer.go new file mode 100644 index 00000000..05239005 --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/analysis/analyzer.go @@ -0,0 +1,108 @@ +// Package analysis provides the main analyzer implementation. +package analysis + +import ( + "errors" + "fmt" + "path/filepath" + + "golang.org/x/tools/go/analysis" + + "github.com/godoc-lint/godoc-lint/pkg/model" + "github.com/godoc-lint/godoc-lint/pkg/util" +) + +const ( + metaName = "godoclint" + metaDoc = "Checks Golang's documentation practice (godoc)" + metaURL = "https://github.com/godoc-lint/godoc-lint" +) + +// Analyzer implements the godoc-lint analyzer. +type Analyzer struct { + baseDir string + cb model.ConfigBuilder + inspector model.Inspector + reg model.Registry + exitFunc func(int, error) + + analyzer *analysis.Analyzer +} + +// NewAnalyzer returns a new instance of the corresponding analyzer. +func NewAnalyzer(baseDir string, cb model.ConfigBuilder, reg model.Registry, inspector model.Inspector, exitFunc func(int, error)) *Analyzer { + result := &Analyzer{ + baseDir: baseDir, + cb: cb, + reg: reg, + inspector: inspector, + exitFunc: exitFunc, + analyzer: &analysis.Analyzer{ + Name: metaName, + Doc: metaDoc, + URL: metaURL, + Requires: []*analysis.Analyzer{inspector.GetAnalyzer()}, + }, + } + + result.analyzer.Run = result.run + return result +} + +// GetAnalyzer returns the underlying analyzer. +func (a *Analyzer) GetAnalyzer() *analysis.Analyzer { + return a.analyzer +} + +func (a *Analyzer) run(pass *analysis.Pass) (any, error) { + if len(pass.Files) == 0 { + return nil, nil + } + + ft := util.GetPassFileToken(pass.Files[0], pass) + if ft == nil { + err := errors.New("cannot prepare config") + if a.exitFunc != nil { + a.exitFunc(2, err) + } + return nil, err + } + + if !util.IsPathUnderBaseDir(a.baseDir, ft.Name()) { + return nil, nil + } + + pkgDir := filepath.Dir(ft.Name()) + cfg, err := a.cb.GetConfig(pkgDir) + if err != nil { + err := fmt.Errorf("cannot prepare config: %w", err) + if a.exitFunc != nil { + a.exitFunc(2, err) + } + return nil, err + } + + ir := pass.ResultOf[a.inspector.GetAnalyzer()].(*model.InspectorResult) + if ir == nil || ir.Files == nil { + return nil, nil + } + + actx := &model.AnalysisContext{ + Config: cfg, + InspectorResult: ir, + Pass: pass, + } + + for _, checker := range a.reg.List() { + // TODO(babakks): This can be done once to improve performance. + ruleSet := checker.GetCoveredRules() + if !actx.Config.IsAnyRuleApplicable(ruleSet) { + continue + } + + if err := checker.Apply(actx); err != nil { + return nil, fmt.Errorf("checker error: %w", err) + } + } + return nil, nil +} diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/check/deprecated/deprecated.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/check/deprecated/deprecated.go new file mode 100644 index 00000000..08915f6c --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/check/deprecated/deprecated.go @@ -0,0 +1,114 @@ +// Package deprecated provides a checker for correct usage of deprecation +// markers. +package deprecated + +import ( + "go/ast" + "go/doc/comment" + "regexp" + + "github.com/godoc-lint/godoc-lint/pkg/model" + "github.com/godoc-lint/godoc-lint/pkg/util" +) + +const deprecatedRule = model.DeprecatedRule + +var ruleSet = model.RuleSet{}.Add(deprecatedRule) + +// DeprecatedChecker checks correct usage of "Deprecated:" markers. +type DeprecatedChecker struct{} + +// NewDeprecatedChecker returns a new instance of the corresponding checker. +func NewDeprecatedChecker() *DeprecatedChecker { + return &DeprecatedChecker{} +} + +// GetCoveredRules implements the corresponding interface method. +func (r *DeprecatedChecker) GetCoveredRules() model.RuleSet { + return ruleSet +} + +// Apply implements the corresponding interface method. +func (r *DeprecatedChecker) Apply(actx *model.AnalysisContext) error { + docs := make(map[*model.CommentGroup]struct{}, 10*len(actx.InspectorResult.Files)) + + for _, ir := range util.AnalysisApplicableFiles(actx, false, model.RuleSet{}.Add(deprecatedRule)) { + if ir.PackageDoc != nil { + docs[ir.PackageDoc] = struct{}{} + } + + for _, sd := range ir.SymbolDecl { + isExported := ast.IsExported(sd.Name) + if !isExported { + continue + } + + if sd.ParentDoc != nil { + docs[sd.ParentDoc] = struct{}{} + } + if sd.Doc == nil { + continue + } + docs[sd.Doc] = struct{}{} + } + } + + for doc := range docs { + checkDeprecations(actx, doc) + } + return nil +} + +// probableDeprecationRE finds probable deprecation markers, including the +// correct usage. +var probableDeprecationRE = regexp.MustCompile(`(?i)^deprecated:.?`) + +const correctDeprecationMarker = "Deprecated: " + +func checkDeprecations(actx *model.AnalysisContext, doc *model.CommentGroup) { + if doc.DisabledRules.All || doc.DisabledRules.Rules.Has(deprecatedRule) { + return + } + + for _, block := range doc.Parsed.Content { + // The correct usage of deprecation markers is to put them at the beginning + // of a paragraph (i.e. not a heading, code block, etc). Also the syntax is + // strict and must only be "Deprecated: " (case-sensitive and with the + // trailing whitespace). + // + // However, not all wrong usages are reliably discoverable. For example: + // + // // Foo is a symbol. + // // deprecated: use Bar. + // // func Foo() {} + // + // In this cases, the "deprecated: " marker is at the beginning of a new + // line, but as of godoc parser, it's just in the middle of the paragraph + // started at the top line. There might be some smart ways to detect these + // cases, but the problem is there will be false-positives to them. For + // instance, a valid case like this should never be detected as a wrong + // usage: + // + // // Foo is a symbol but here are the reasons why it's + // // deprecated: blah, blah, ... + // // func Foo() {} + // + // Needless to say we don't want to deal with human language complexities. + + par, ok := block.(*comment.Paragraph) + if !ok || len(par.Text) == 0 { + continue + } + text, ok := (par.Text[0]).(comment.Plain) + if !ok { + continue + } + + if match := probableDeprecationRE.FindString(string(text)); match == "" || match == correctDeprecationMarker { + continue + } + + actx.Pass.ReportRangef(&doc.CG, "deprecation note should be formatted as %q", correctDeprecationMarker) + break + } +} diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/check/max_len/max_len.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/check/max_len/max_len.go new file mode 100644 index 00000000..1d647c49 --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/check/max_len/max_len.go @@ -0,0 +1,96 @@ +// Package max_len provides a checker for maximum line length of godocs. +package max_len + +import ( + "fmt" + gdc "go/doc/comment" + "strings" + "unicode/utf8" + + "github.com/godoc-lint/godoc-lint/pkg/model" + "github.com/godoc-lint/godoc-lint/pkg/util" +) + +const maxLenRule = model.MaxLenRule + +var ruleSet = model.RuleSet{}.Add(maxLenRule) + +// MaxLenChecker checks maximum line length of godocs. +type MaxLenChecker struct{} + +// NewMaxLenChecker returns a new instance of the corresponding checker. +func NewMaxLenChecker() *MaxLenChecker { + return &MaxLenChecker{} +} + +// GetCoveredRules implements the corresponding interface method. +func (r *MaxLenChecker) GetCoveredRules() model.RuleSet { + return ruleSet +} + +// Apply implements the corresponding interface method. +func (r *MaxLenChecker) Apply(actx *model.AnalysisContext) error { + includeTests := actx.Config.GetRuleOptions().MaxLenIncludeTests + maxLen := int(actx.Config.GetRuleOptions().MaxLenLength) + + docs := make(map[*model.CommentGroup]struct{}, 10*len(actx.InspectorResult.Files)) + + for _, ir := range util.AnalysisApplicableFiles(actx, includeTests, model.RuleSet{}.Add(maxLenRule)) { + if ir.PackageDoc != nil { + docs[ir.PackageDoc] = struct{}{} + } + + for _, sd := range ir.SymbolDecl { + if sd.ParentDoc != nil { + docs[sd.ParentDoc] = struct{}{} + } + if sd.Doc == nil { + continue + } + docs[sd.Doc] = struct{}{} + } + } + + for doc := range docs { + checkMaxLen(actx, doc, maxLen) + } + return nil +} + +func checkMaxLen(actx *model.AnalysisContext, doc *model.CommentGroup, maxLen int) { + if doc.DisabledRules.All || doc.DisabledRules.Rules.Has(maxLenRule) { + return + } + + linkDefsMap := make(map[string]struct{}, len(doc.Parsed.Links)) + for _, linkDef := range doc.Parsed.Links { + linkDefLine := fmt.Sprintf("[%s]: %s", linkDef.Text, linkDef.URL) + linkDefsMap[linkDefLine] = struct{}{} + } + + nonCodeBlocks := make([]gdc.Block, 0, len(doc.Parsed.Content)) + for _, b := range doc.Parsed.Content { + if _, ok := b.(*gdc.Code); ok { + continue + } + nonCodeBlocks = append(nonCodeBlocks, b) + } + strippedCodeAndLinks := &gdc.Doc{ + Content: nonCodeBlocks, + } + text := string((&gdc.Printer{}).Comment(strippedCodeAndLinks)) + linesIter := strings.SplitSeq(removeCarriageReturn(text), "\n") + + for l := range linesIter { + lineLen := utf8.RuneCountInString(l) + if lineLen <= maxLen { + continue + } + actx.Pass.ReportRangef(&doc.CG, "godoc line is too long (%d > %d)", lineLen, maxLen) + break + } +} + +func removeCarriageReturn(s string) string { + return strings.ReplaceAll(s, "\r", "") +} diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/check/no_unused_link/no_unused_link.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/check/no_unused_link/no_unused_link.go new file mode 100644 index 00000000..8910204b --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/check/no_unused_link/no_unused_link.go @@ -0,0 +1,69 @@ +// Package no_unused_link provides a checker for unused links in godocs. +package no_unused_link + +import ( + "github.com/godoc-lint/godoc-lint/pkg/model" + "github.com/godoc-lint/godoc-lint/pkg/util" +) + +const noUnusedLinkRule = model.NoUnusedLinkRule + +var ruleSet = model.RuleSet{}.Add(noUnusedLinkRule) + +// NoUnusedLinkChecker checks for unused links. +type NoUnusedLinkChecker struct{} + +// NewNoUnusedLinkChecker returns a new instance of the corresponding checker. +func NewNoUnusedLinkChecker() *NoUnusedLinkChecker { + return &NoUnusedLinkChecker{} +} + +// GetCoveredRules implements the corresponding interface method. +func (r *NoUnusedLinkChecker) GetCoveredRules() model.RuleSet { + return ruleSet +} + +// Apply implements the corresponding interface method. +func (r *NoUnusedLinkChecker) Apply(actx *model.AnalysisContext) error { + includeTests := actx.Config.GetRuleOptions().NoUnusedLinkIncludeTests + + docs := make(map[*model.CommentGroup]struct{}, 10*len(actx.InspectorResult.Files)) + + for _, ir := range util.AnalysisApplicableFiles(actx, includeTests, model.RuleSet{}.Add(noUnusedLinkRule)) { + if ir.PackageDoc != nil { + docs[ir.PackageDoc] = struct{}{} + } + + for _, sd := range ir.SymbolDecl { + if sd.ParentDoc != nil { + docs[sd.ParentDoc] = struct{}{} + } + if sd.Doc == nil { + continue + } + docs[sd.Doc] = struct{}{} + } + } + + for doc := range docs { + checkNoUnusedLink(actx, doc) + } + return nil +} + +func checkNoUnusedLink(actx *model.AnalysisContext, doc *model.CommentGroup) { + if doc.DisabledRules.All || doc.DisabledRules.Rules.Has(noUnusedLinkRule) { + return + } + + if doc.Text == "" { + return + } + + for _, linkDef := range doc.Parsed.Links { + if linkDef.Used { + continue + } + actx.Pass.ReportRangef(&doc.CG, "godoc has unused link (%q)", linkDef.Text) + } +} diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/check/pkg_doc/pkg_doc.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/check/pkg_doc/pkg_doc.go new file mode 100644 index 00000000..199b1f54 --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/check/pkg_doc/pkg_doc.go @@ -0,0 +1,204 @@ +// Package pkg_doc provides a checker for package godocs. +package pkg_doc + +import ( + "go/ast" + "strings" + + "github.com/godoc-lint/godoc-lint/pkg/check/shared" + "github.com/godoc-lint/godoc-lint/pkg/model" + "github.com/godoc-lint/godoc-lint/pkg/util" +) + +const ( + pkgDocRule = model.PkgDocRule + singlePkgDocRule = model.SinglePkgDocRule + requirePkgDocRule = model.RequirePkgDocRule +) + +var ruleSet = model.RuleSet{}.Add( + pkgDocRule, + singlePkgDocRule, + requirePkgDocRule, +) + +// PkgDocChecker checks package godocs. +type PkgDocChecker struct{} + +// NewPkgDocChecker returns a new instance of the corresponding checker. +func NewPkgDocChecker() *PkgDocChecker { + return &PkgDocChecker{} +} + +// GetCoveredRules implements the corresponding interface method. +func (r *PkgDocChecker) GetCoveredRules() model.RuleSet { + return ruleSet +} + +// Apply implements the corresponding interface method. +func (r *PkgDocChecker) Apply(actx *model.AnalysisContext) error { + checkPkgDocRule(actx) + checkSinglePkgDocRule(actx) + checkRequirePkgDocRule(actx) + return nil +} + +const commandPkgName = "main" +const commandTestPkgName = "main_test" + +func checkPkgDocRule(actx *model.AnalysisContext) { + if !actx.Config.IsAnyRuleApplicable(model.RuleSet{}.Add(pkgDocRule)) { + return + } + + includeTests := actx.Config.GetRuleOptions().PkgDocIncludeTests + + for f, ir := range util.AnalysisApplicableFiles(actx, includeTests, model.RuleSet{}.Add(pkgDocRule)) { + if ir.PackageDoc == nil { + continue + } + + if ir.PackageDoc.DisabledRules.All || ir.PackageDoc.DisabledRules.Rules.Has(pkgDocRule) { + continue + } + + if f.Name.Name == commandPkgName || f.Name.Name == commandTestPkgName { + // Skip command packages, as they are not required to start with + // "Package main" or "Package main_test". + // + // See for more details: + // - https://github.com/godoc-lint/godoc-lint/issues/10 + // - https://go.dev/doc/comment#cmd + continue + } + + if ir.PackageDoc.Text == "" { + continue + } + + if shared.HasDeprecatedParagraph(ir.PackageDoc.Parsed.Content) { + // If there's a paragraph starting with "Deprecated:", we skip the + // entire godoc. The reason is a deprecated symbol will not appear + // when docs are rendered. + // + // Another reason is that we cannot just skip those paragraphs and + // look for the symbol in the remaining text. To do that, we need + // to iterate over all comment.Block nodes, and check if a block + // is a paragraph AND starts with the deprecation marker. This is + // simple, but the challenge appears when we get to the first block + // that does not have the marker and we want to check if it starts + // with the symbol name. We'd expect that to be a paragraph, but + // that is not always the case. For example, take this decl: + // + // // Deprecated: blah blah + // // + // // Foo is integer + // // + // // Deprecation: blah blah + // type Foo int + // + // The first block is a paragraph which we can easily skip due to + // the "Deprecated:" marker. However, the second block is actually + // parsed as a heading. One can verify this by copy/pasting it in + // a Go file and check the gopls hover. + // + // There might be a workaround for this, but this also means the + // godoc parser behaves in unexpected ways, and until we don't + // really know the extent of its quirks, it's safer to just skip + // further checks on such godocs. + continue + } + + if expectedPrefix, ok := checkPkgDocPrefix(ir.PackageDoc.Text, f.Name.Name); !ok { + actx.Pass.Reportf(ir.PackageDoc.CG.Pos(), "package godoc should start with %q", expectedPrefix+" ") + } + } +} + +func checkPkgDocPrefix(text string, packageName string) (string, bool) { + expectedPrefix := "Package " + packageName + if !strings.HasPrefix(text, expectedPrefix) { + return expectedPrefix, false + } + rest := text[len(expectedPrefix):] + return expectedPrefix, rest == "" || rest[0] == ' ' || rest[0] == '\t' || rest[0] == '\r' || rest[0] == '\n' +} + +func checkSinglePkgDocRule(actx *model.AnalysisContext) { + if !actx.Config.IsAnyRuleApplicable(model.RuleSet{}.Add(singlePkgDocRule)) { + return + } + + includeTests := actx.Config.GetRuleOptions().SinglePkgDocIncludeTests + + documentedPkgs := make(map[string][]*ast.File, 2) + + for f, ir := range util.AnalysisApplicableFiles(actx, includeTests, model.RuleSet{}.Add(singlePkgDocRule)) { + if ir.PackageDoc == nil || ir.PackageDoc.Text == "" { + continue + } + + if ir.PackageDoc.DisabledRules.All || ir.PackageDoc.DisabledRules.Rules.Has(singlePkgDocRule) { + continue + } + + pkg := f.Name.Name + if _, ok := documentedPkgs[pkg]; !ok { + documentedPkgs[pkg] = make([]*ast.File, 0, 2) + } + documentedPkgs[pkg] = append(documentedPkgs[pkg], f) + } + + for pkg, fs := range documentedPkgs { + if len(fs) < 2 { + continue + } + for _, f := range fs { + ir := actx.InspectorResult.Files[f] + actx.Pass.Reportf(ir.PackageDoc.CG.Pos(), "package has more than one godoc (%q)", pkg) + } + } +} + +func checkRequirePkgDocRule(actx *model.AnalysisContext) { + if !actx.Config.IsAnyRuleApplicable(model.RuleSet{}.Add(requirePkgDocRule)) { + return + } + + includeTests := actx.Config.GetRuleOptions().RequirePkgDocIncludeTests + + pkgFiles := make(map[string][]*ast.File, 2) + + for f := range util.AnalysisApplicableFiles(actx, includeTests, model.RuleSet{}.Add(requirePkgDocRule)) { + pkg := f.Name.Name + if _, ok := pkgFiles[pkg]; !ok { + pkgFiles[pkg] = make([]*ast.File, 0, len(actx.Pass.Files)) + } + pkgFiles[pkg] = append(pkgFiles[pkg], f) + } + + for pkg, fs := range pkgFiles { + pkgHasGodoc := false + for _, f := range fs { + ir := actx.InspectorResult.Files[f] + + if ir.PackageDoc == nil || ir.PackageDoc.Text == "" { + continue + } + + if ir.PackageDoc.DisabledRules.All || ir.PackageDoc.DisabledRules.Rules.Has(requirePkgDocRule) { + continue + } + + pkgHasGodoc = true + break + } + + if pkgHasGodoc { + continue + } + + // Add a diagnostic message to the first file of the package. + actx.Pass.Reportf(fs[0].Name.Pos(), "package should have a godoc (%q)", pkg) + } +} diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/check/registry.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/check/registry.go new file mode 100644 index 00000000..8452fa02 --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/check/registry.go @@ -0,0 +1,64 @@ +// Package check provides a registry of checkers. +package check + +import ( + "github.com/godoc-lint/godoc-lint/pkg/check/deprecated" + "github.com/godoc-lint/godoc-lint/pkg/check/max_len" + "github.com/godoc-lint/godoc-lint/pkg/check/no_unused_link" + "github.com/godoc-lint/godoc-lint/pkg/check/pkg_doc" + "github.com/godoc-lint/godoc-lint/pkg/check/require_doc" + "github.com/godoc-lint/godoc-lint/pkg/check/start_with_name" + "github.com/godoc-lint/godoc-lint/pkg/model" +) + +// Registry implements a registry of rules. +type Registry struct { + checkers map[model.Checker]struct{} + coveredRules model.RuleSet +} + +// NewRegistry returns a new rule registry instance. +func NewRegistry(checkers ...model.Checker) *Registry { + registry := Registry{ + checkers: make(map[model.Checker]struct{}, len(checkers)+10), + } + for _, c := range checkers { + registry.Add(c) + } + return ®istry +} + +// NewPopulatedRegistry returns a registry with all supported rules registered. +func NewPopulatedRegistry() *Registry { + return NewRegistry( + max_len.NewMaxLenChecker(), + pkg_doc.NewPkgDocChecker(), + require_doc.NewRequireDocChecker(), + start_with_name.NewStartWithNameChecker(), + no_unused_link.NewNoUnusedLinkChecker(), + deprecated.NewDeprecatedChecker(), + ) +} + +// Add implements the corresponding interface method. +func (r *Registry) Add(checker model.Checker) { + if _, ok := r.checkers[checker]; ok { + return + } + r.coveredRules = r.coveredRules.Merge(checker.GetCoveredRules()) + r.checkers[checker] = struct{}{} +} + +// List implements the corresponding interface method. +func (r *Registry) List() []model.Checker { + all := make([]model.Checker, 0, len(r.checkers)) + for c := range r.checkers { + all = append(all, c) + } + return all +} + +// GetCoveredRules implements the corresponding interface method. +func (r *Registry) GetCoveredRules() model.RuleSet { + return r.coveredRules +} diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/check/require_doc/require_doc.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/check/require_doc/require_doc.go new file mode 100644 index 00000000..afa16bd7 --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/check/require_doc/require_doc.go @@ -0,0 +1,173 @@ +// Package require_doc provides a checker that requires symbols to have godocs. +package require_doc + +import ( + "go/ast" + + "golang.org/x/tools/go/analysis" + + "github.com/godoc-lint/godoc-lint/pkg/model" + "github.com/godoc-lint/godoc-lint/pkg/util" +) + +const requireDocRule = model.RequireDocRule + +var ruleSet = model.RuleSet{}.Add(requireDocRule) + +// RequireDocChecker checks required godocs. +type RequireDocChecker struct{} + +// NewRequireDocChecker returns a new instance of the corresponding checker. +func NewRequireDocChecker() *RequireDocChecker { + return &RequireDocChecker{} +} + +// GetCoveredRules implements the corresponding interface method. +func (r *RequireDocChecker) GetCoveredRules() model.RuleSet { + return ruleSet +} + +// Apply implements the corresponding interface method. +func (r *RequireDocChecker) Apply(actx *model.AnalysisContext) error { + includeTests := actx.Config.GetRuleOptions().RequireDocIncludeTests + requirePublic := !actx.Config.GetRuleOptions().RequireDocIgnoreExported + requirePrivate := !actx.Config.GetRuleOptions().RequireDocIgnoreUnexported + + if !requirePublic && !requirePrivate { + return nil + } + + for _, ir := range util.AnalysisApplicableFiles(actx, includeTests, model.RuleSet{}.Add(requireDocRule)) { + for _, decl := range ir.SymbolDecl { + isExported := ast.IsExported(decl.Name) + if isExported && !requirePublic || !isExported && !requirePrivate { + continue + } + + if decl.Name == "_" { + // Blank identifiers should be ignored; e.g.: + // + // var _ = 0 + continue + } + + if decl.Doc != nil && (decl.Doc.DisabledRules.All || decl.Doc.DisabledRules.Rules.Has(requireDocRule)) { + continue + } + + if decl.Kind == model.SymbolDeclKindBad { + continue + } + + if decl.Kind == model.SymbolDeclKindFunc { + if decl.Doc == nil || decl.Doc.Text == "" { + reportRange(actx.Pass, decl.Ident) + } + continue + } + + // Now we only have const/var/type declarations. + + if decl.Doc != nil && decl.Doc.Text != "" { + // cases: + // + // // godoc + // const foo = 0 + // + // // godoc + // const foo, bar = 0, 0 + // + // const ( + // // godoc + // foo = 0 + // ) + // + // const ( + // // godoc + // foo, bar = 0, 0 + // ) + // + // // godoc + // type foo int + // + // type ( + // // godoc + // foo int + // ) + continue + } + + if decl.TrailingDoc != nil && decl.TrailingDoc.Text != "" { + // cases: + // + // const foo = 0 // godoc + // + // const foo, bar = 0, 0 // godoc + // + // const ( + // foo = 0 // godoc + // ) + // + // const ( + // foo, bar = 0, 0 // godoc + // ) + // + // type foo int // godoc + // + // type ( + // foo int // godoc + // ) + continue + } + + if decl.ParentDoc != nil && decl.ParentDoc.Text != "" { + // cases: + // + // // godoc + // const ( + // foo = 0 + // ) + // + // // godoc + // const ( + // foo, bar = 0, 0 + // ) + // + // // godoc + // type ( + // foo int + // ) + continue + } + + // At this point there is no godoc for the symbol. + // + // cases: + // + // const foo = 0 + // + // const foo, bar = 0, 0 + // + // const ( + // foo = 0 + // ) + // + // const ( + // foo, bar = 0, 0 + // ) + // + // type foo int + // + // type ( + // foo int + // ) + + reportRange(actx.Pass, decl.Ident) + } + } + return nil +} + +func reportRange(pass *analysis.Pass, ident *ast.Ident) { + pass.ReportRangef(ident, "symbol should have a godoc (%q)", ident.Name) +} diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/check/shared/deprecated.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/check/shared/deprecated.go new file mode 100644 index 00000000..8ebed0e2 --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/check/shared/deprecated.go @@ -0,0 +1,29 @@ +// Package shared provides shared utilities for checkers. +package shared + +import ( + "go/doc/comment" + "strings" +) + +// HasDeprecatedParagraph reports whether the given comment blocks contain a +// paragraph starting with deprecation marker. +func HasDeprecatedParagraph(blocks []comment.Block) bool { + for _, block := range blocks { + par, ok := block.(*comment.Paragraph) + if !ok || len(par.Text) == 0 { + continue + } + text, ok := (par.Text[0]).(comment.Plain) + if !ok { + continue + } + + // Only an exact match (casing and the trailing whitespace) is considered + // a valid deprecation marker. + if strings.HasPrefix(string(text), "Deprecated: ") { + return true + } + } + return false +} diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/check/start_with_name/start_with_name.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/check/start_with_name/start_with_name.go new file mode 100644 index 00000000..f8c905aa --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/check/start_with_name/start_with_name.go @@ -0,0 +1,132 @@ +// Package start_with_name provides a checker for godocs starting with the +// symbol name. +package start_with_name + +import ( + "go/ast" + "regexp" + "strings" + + "github.com/godoc-lint/godoc-lint/pkg/check/shared" + "github.com/godoc-lint/godoc-lint/pkg/model" + "github.com/godoc-lint/godoc-lint/pkg/util" +) + +const startWithNameRule = model.StartWithNameRule + +var ruleSet = model.RuleSet{}.Add(startWithNameRule) + +// StartWithNameChecker checks starter of godocs. +type StartWithNameChecker struct{} + +// NewStartWithNameChecker returns a new instance of the corresponding checker. +func NewStartWithNameChecker() *StartWithNameChecker { + return &StartWithNameChecker{} +} + +// GetCoveredRules implements the corresponding interface method. +func (r *StartWithNameChecker) GetCoveredRules() model.RuleSet { + return ruleSet +} + +// Apply implements the corresponding interface method. +func (r *StartWithNameChecker) Apply(actx *model.AnalysisContext) error { + if !actx.Config.IsAnyRuleApplicable(model.RuleSet{}.Add(startWithNameRule)) { + return nil + } + + includeTests := actx.Config.GetRuleOptions().StartWithNameIncludeTests + includePrivate := actx.Config.GetRuleOptions().StartWithNameIncludeUnexported + + for _, ir := range util.AnalysisApplicableFiles(actx, includeTests, model.RuleSet{}.Add(startWithNameRule)) { + for _, decl := range ir.SymbolDecl { + isExported := ast.IsExported(decl.Name) + if !isExported && !includePrivate { + continue + } + + if decl.Name == "_" { + // Blank identifiers should be ignored; e.g.: + // + // var _ = 0 + continue + } + + if decl.Kind == model.SymbolDeclKindBad { + continue + } + + if decl.Doc == nil || decl.Doc.Text == "" { + continue + } + + if decl.Doc.DisabledRules.All || decl.Doc.DisabledRules.Rules.Has(startWithNameRule) { + continue + } + + if decl.MultiNameDecl { + continue + } + + if shared.HasDeprecatedParagraph(decl.Doc.Parsed.Content) { + // If there's a paragraph starting with "Deprecated:", we skip the + // entire godoc. The reason is a deprecated symbol will not appear + // when docs are rendered. + // + // Another reason is that we cannot just skip those paragraphs and + // look for the symbol in the remaining text. To do that, we need + // to iterate over all comment.Block nodes, and check if a block + // is a paragraph AND starts with the deprecation marker. This is + // simple, but the challenge appears when we get to the first block + // that does not have the marker and we want to check if it starts + // with the symbol name. We'd expect that to be a paragraph, but + // that is not always the case. For example, take this decl: + // + // // Deprecated: blah blah + // // + // // Foo is integer + // // + // // Deprecation: blah blah + // type Foo int + // + // The first block is a paragraph which we can easily skip due to + // the "Deprecated:" marker. However, the second block is actually + // parsed as a heading. One can verify this by copy/pasting it in + // a Go file and check the gopls hover. + // + // There might be a workaround for this, but this also means the + // godoc parser behaves in unexpected ways, and until we don't + // really know the extent of its quirks, it's safer to just skip + // further checks on such godocs. + continue + } + + if matchSymbolName(decl.Doc.Text, decl.Name) { + continue + } + + actx.Pass.ReportRangef(&decl.Doc.CG, "godoc should start with symbol name (%q)", decl.Name) + } + } + return nil +} + +var startPattern = regexp.MustCompile(`^(?:(A|a|AN|An|an|THE|The|the) )?(?P.+?)\b`) +var startPatternSymbolNameIndex = startPattern.SubexpIndex("symbol_name") + +func matchSymbolName(text string, symbol string) bool { + head := strings.SplitN(text, "\n", 2)[0] + head, _ = strings.CutPrefix(head, "\r") + head = strings.SplitN(head, " ", 2)[0] + head = strings.SplitN(head, "\t", 2)[0] + + if head == symbol { + return true + } + + match := startPattern.FindStringSubmatch(text) + if match == nil { + return false + } + return match[startPatternSymbolNameIndex] == symbol +} diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/compose/compose.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/compose/compose.go new file mode 100644 index 00000000..7ec04e34 --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/compose/compose.go @@ -0,0 +1,53 @@ +// Package compose provides composition of the linter components. +package compose + +import ( + "os" + + "github.com/godoc-lint/godoc-lint/pkg/analysis" + "github.com/godoc-lint/godoc-lint/pkg/check" + "github.com/godoc-lint/godoc-lint/pkg/config" + "github.com/godoc-lint/godoc-lint/pkg/inspect" + "github.com/godoc-lint/godoc-lint/pkg/model" +) + +// Composition holds the composed components of the linter. +type Composition struct { + Registry model.Registry + ConfigBuilder model.ConfigBuilder + Inspector model.Inspector + Analyzer model.Analyzer +} + +// CompositionConfig holds the configuration for composing the linter. +type CompositionConfig struct { + BaseDir string + ExitFunc func(int, error) + + // BaseDirPlainConfig holds the plain configuration for the base directory. + // + // This is meant to be used for integrating with umbrella linters (e.g. + // golangci-lint) where the root config comes from a different source/format. + BaseDirPlainConfig *config.PlainConfig +} + +// Compose composes the linter components based on the given configuration. +func Compose(c CompositionConfig) *Composition { + if c.BaseDir == "" { + // It's a best effort to use the current working directory if not set. + c.BaseDir, _ = os.Getwd() + } + + reg := check.NewPopulatedRegistry() + cb := config.NewConfigBuilder(c.BaseDir).WithBaseDirPlainConfig(c.BaseDirPlainConfig) + ocb := config.NewOnceConfigBuilder(cb) + inspector := inspect.NewInspector(ocb, c.ExitFunc) + analyzer := analysis.NewAnalyzer(c.BaseDir, ocb, reg, inspector, c.ExitFunc) + + return &Composition{ + Registry: reg, + ConfigBuilder: cb, + Inspector: inspector, + Analyzer: analyzer, + } +} diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/config/builder.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/config/builder.go new file mode 100644 index 00000000..aa25cf18 --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/config/builder.go @@ -0,0 +1,289 @@ +package config + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "regexp" + "slices" + + "github.com/godoc-lint/godoc-lint/pkg/model" + "github.com/godoc-lint/godoc-lint/pkg/util" +) + +// ConfigBuilder implements a configuration builder. +type ConfigBuilder struct { + baseDir string + override *model.ConfigOverride + + // baseDirPlainConfig holds the plain config for the base directory. + // + // This is meant to be used for integrating with umbrella linters (e.g. + // golangci-lint) where the root config comes from a different + // source/format. + baseDirPlainConfig *PlainConfig +} + +// NewConfigBuilder crates a new instance of the corresponding struct. +func NewConfigBuilder(baseDir string) *ConfigBuilder { + return &ConfigBuilder{ + baseDir: baseDir, + } +} + +// WithBaseDirPlainConfig sets the plain config for the base directory. This is +// meant to be used for integrating with umbrella linters (e.g. golangci-lint) +// where the root config comes from a different source/format. +func (cb *ConfigBuilder) WithBaseDirPlainConfig(baseDirPlainConfig *PlainConfig) *ConfigBuilder { + cb.baseDirPlainConfig = baseDirPlainConfig + return cb +} + +// GetConfig implements the corresponding interface method. +func (cb *ConfigBuilder) GetConfig(cwd string) (model.Config, error) { + return cb.build(cwd) +} + +func (cb *ConfigBuilder) resolvePlainConfig(cwd string) (*PlainConfig, *PlainConfig, string, string, error) { + def := getDefaultPlainConfig() + + if !util.IsPathUnderBaseDir(cb.baseDir, cwd) { + if pcfg, filePath, err := cb.resolvePlainConfigAtBaseDir(); err != nil { + return nil, nil, "", "", err + } else if pcfg != nil { + return pcfg, def, cb.baseDir, filePath, nil + } + return def, def, cb.baseDir, "", nil + } + + path := cwd + for { + rel, err := filepath.Rel(cb.baseDir, path) + if err != nil { + return nil, nil, "", "", err + } + + if rel == "." { + if pcfg, filePath, err := cb.resolvePlainConfigAtBaseDir(); err != nil { + return nil, nil, "", "", err + } else if pcfg != nil { + return pcfg, def, cb.baseDir, filePath, nil + } + return def, def, cb.baseDir, "", nil + } + + if pcfg, filePath, err := findConventionalConfigFile(path); err != nil { + return nil, nil, "", "", err + } else if pcfg != nil { + return pcfg, def, path, filePath, nil + } + + path = filepath.Dir(path) + } +} + +func (cb *ConfigBuilder) resolvePlainConfigAtBaseDir() (*PlainConfig, string, error) { + // TODO(babakks): refactor this to a sync.OnceValue for performance + + if cb.override != nil && cb.override.ConfigFilePath != nil { + pcfg, err := FromYAMLFile(*cb.override.ConfigFilePath) + if err != nil { + return nil, "", err + } + return pcfg, *cb.override.ConfigFilePath, nil + } + + if pcfg, filePath, err := findConventionalConfigFile(cb.baseDir); err != nil { + return nil, "", err + } else if pcfg != nil { + return pcfg, filePath, nil + } + + if cb.baseDirPlainConfig != nil { + return cb.baseDirPlainConfig, "", nil + } + return nil, "", nil +} + +func findConventionalConfigFile(dir string) (*PlainConfig, string, error) { + for _, dcf := range defaultConfigFiles { + path := filepath.Join(dir, dcf) + if fi, err := os.Stat(path); err != nil || fi.IsDir() { + continue + } + pcfg, err := FromYAMLFile(path) + if err != nil { + return nil, "", err + } + return pcfg, path, nil + } + return nil, "", nil +} + +// build creates the configuration struct. +// +// It checks a sequence of sources: +// 1. Custom config file path +// 2. Default configuration files (e.g., `.godoc-lint.yaml`) +// +// If none was available, the default configuration will be returned. +// +// The method also does the following: +// - Applies override flags (e.g., enable, or disable). +// - Validates the final configuration. +func (cb *ConfigBuilder) build(cwd string) (*config, error) { + pcfg, def, configCWD, configFilePath, err := cb.resolvePlainConfig(cwd) + if err != nil { + return nil, err + } + + if err := pcfg.Validate(); err != nil { + return nil, fmt.Errorf("invalid config at %q: %w", configFilePath, err) + } + + toValidRuleSet := func(s []string) (*model.RuleSet, []string) { + if s == nil { + return nil, nil + } + invalids := make([]string, 0, len(s)) + rules := make([]model.Rule, 0, len(s)) + for _, v := range s { + if !model.AllRules.Has(model.Rule(v)) { + invalids = append(invalids, v) + continue + } + rules = append(rules, model.Rule(v)) + } + set := model.RuleSet{}.Add(rules...) + return &set, invalids + } + + toValidRegexpSlice := func(s []string) ([]*regexp.Regexp, []string) { + if s == nil { + return nil, nil + } + var invalids []string + var regexps []*regexp.Regexp + for _, v := range s { + re, err := regexp.Compile(v) + if err != nil { + invalids = append(invalids, v) + continue + } + regexps = append(regexps, re) + } + return regexps, invalids + } + + var errs []error + + result := &config{ + cwd: configCWD, + configFilePath: configFilePath, + } + + var enabledRules *model.RuleSet + if cb.override != nil && cb.override.Enable != nil { + enabledRules = cb.override.Enable + } else { + raw := pcfg.Enable + if raw == nil { + raw = def.Enable + } + rs, invalids := toValidRuleSet(raw) + if len(invalids) > 0 { + errs = append(errs, fmt.Errorf("invalid rule(s) name to enable: %q", invalids)) + } else { + enabledRules = rs + } + } + + var disabledRules *model.RuleSet + if cb.override != nil && cb.override.Disable != nil { + disabledRules = cb.override.Disable + } else { + raw := pcfg.Disable + if raw == nil { + raw = def.Disable + } + rs, invalids := toValidRuleSet(raw) + if len(invalids) > 0 { + errs = append(errs, fmt.Errorf("invalid rule(s) name to disable: %q", invalids)) + } else { + disabledRules = rs + } + } + + if cb.override != nil && cb.override.Include != nil { + result.includeAsRegexp = cb.override.Include + } else { + raw := pcfg.Include + if raw == nil { + raw = def.Include + } + rs, invalids := toValidRegexpSlice(raw) + if len(invalids) > 0 { + errs = append(errs, fmt.Errorf("invalid path pattern(s) to include: %q", invalids)) + } else { + result.includeAsRegexp = rs + } + } + + if cb.override != nil && cb.override.Exclude != nil { + result.excludeAsRegexp = cb.override.Exclude + } else { + raw := pcfg.Exclude + if raw == nil { + raw = def.Exclude + } + rs, invalids := toValidRegexpSlice(raw) + if len(invalids) > 0 { + errs = append(errs, fmt.Errorf("invalid path pattern(s) to exclude: %q", invalids)) + } else { + result.excludeAsRegexp = rs + } + } + + if cb.override != nil && cb.override.Default != nil { + result.rulesToApply = model.DefaultSetToRules[*cb.override.Default] + } else { + raw := pcfg.Default + if raw == nil { + raw = def.Default // never nil + } + + if !slices.Contains(model.DefaultSetValues, model.DefaultSet(*raw)) { + errs = append(errs, fmt.Errorf("invalid default set %q; must be one of %q", *raw, model.DefaultSetValues)) + } else { + result.rulesToApply = model.DefaultSetToRules[model.DefaultSet(*raw)] + } + } + + if errs != nil { + return nil, errors.Join(errs...) + } + + if enabledRules != nil { + result.rulesToApply = result.rulesToApply.Merge(*enabledRules) + } + if disabledRules != nil { + result.rulesToApply = result.rulesToApply.Remove(disabledRules.List()...) + } + + // To avoid being too strict, we don't complain if a rule is enabled and disabled at the same time. + + resolvedOptions := &model.RuleOptions{} + transferOptions(resolvedOptions, def.Options) // def.Options is never nil + if pcfg.Options != nil { + transferOptions(resolvedOptions, pcfg.Options) + } + result.options = resolvedOptions + + return result, nil +} + +// SetOverride implements the corresponding interface method. +func (cb *ConfigBuilder) SetOverride(override *model.ConfigOverride) { + cb.override = override +} diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/config/config.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/config/config.go new file mode 100644 index 00000000..9a2cdfc6 --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/config/config.go @@ -0,0 +1,73 @@ +package config + +import ( + "path/filepath" + "regexp" + + "github.com/godoc-lint/godoc-lint/pkg/model" +) + +// config represents the godoc-lint analyzer configuration. +type config struct { + // cwd holds the directory that the configuration is applied to. This is the + // way to find out relative paths to include/exclude based on the config + // file. + cwd string + + // configFilePath holds the path to the configuration file. If there is no + // configuration file, which is the case when the default is used, this will + // be an empty string. + configFilePath string + + includeAsRegexp []*regexp.Regexp + excludeAsRegexp []*regexp.Regexp + rulesToApply model.RuleSet + options *model.RuleOptions +} + +// GetConfigFilePath implements the corresponding interface method. +func (c *config) GetConfigFilePath() string { + return c.configFilePath +} + +// GetCWD implements the corresponding interface method. +func (c *config) GetCWD() string { + return c.cwd +} + +// IsAnyRuleApplicable implements the corresponding interface method. +func (c *config) IsAnyRuleApplicable(rs model.RuleSet) bool { + return c.rulesToApply.HasCommonsWith(rs) +} + +// IsPathApplicable implements the corresponding interface method. +func (c *config) IsPathApplicable(path string) bool { + p, err := filepath.Rel(c.cwd, path) + if err != nil { + p = path + } + + // To ensure a consistent behavior on different platform (with the same + // configuration), we convert the path to a Unix-style path. + asUnixPath := filepath.ToSlash(p) + + for _, re := range c.excludeAsRegexp { + if re.MatchString(asUnixPath) { + return false + } + } + if c.includeAsRegexp == nil { + return true + } + for _, re := range c.includeAsRegexp { + if re.MatchString(asUnixPath) { + return true + } + } + return false +} + +// GetRuleOptions implements the corresponding interface method. +func (c *config) GetRuleOptions() *model.RuleOptions { + return c.options +} diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/config/default.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/config/default.go new file mode 100644 index 00000000..79ba8b67 --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/config/default.go @@ -0,0 +1,28 @@ +package config + +import ( + _ "embed" + "sync" +) + +// defaultConfigFiles is the list of default configuration file names. +var defaultConfigFiles = []string{ + ".godoc-lint.yaml", + ".godoc-lint.yml", + ".godoc-lint.json", + ".godoclint.yaml", + ".godoclint.yml", + ".godoclint.json", +} + +// defaultConfigYAML is the default configuration (as YAML). +// +//go:embed default.yaml +var defaultConfigYAML []byte + +// getDefaultPlainConfig returns the parsed default configuration. +var getDefaultPlainConfig = sync.OnceValue(func() *PlainConfig { + // Error is nil due to tests. + pcfg, _ := FromYAML(defaultConfigYAML) + return pcfg +}) diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/config/default.yaml b/vendor/github.com/godoc-lint/godoc-lint/pkg/config/default.yaml new file mode 100644 index 00000000..eb244e77 --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/config/default.yaml @@ -0,0 +1,15 @@ +# Default configuration +version: "1.0" +default: basic +options: + max-len/length: 77 + max-len/include-tests: false + pkg-doc/include-tests: false + single-pkg-doc/include-tests: false + require-pkg-doc/include-tests: false + require-doc/include-tests: false + require-doc/ignore-exported: false + require-doc/ignore-unexported: true + start-with-name/include-tests: false + start-with-name/include-unexported: false + no-unused-link/include-tests: false \ No newline at end of file diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/config/doc.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/config/doc.go new file mode 100644 index 00000000..3a015d98 --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/config/doc.go @@ -0,0 +1,2 @@ +// Package config provides configuration building and management. +package config diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/config/once.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/config/once.go new file mode 100644 index 00000000..2d865d55 --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/config/once.go @@ -0,0 +1,59 @@ +package config + +import ( + "sync" + + "github.com/godoc-lint/godoc-lint/pkg/model" +) + +// OnceConfigBuilder wraps a config builder and make it a one-time builder, so +// that further attempts to build will return the same result. +// +// This type is concurrent-safe. +type OnceConfigBuilder struct { + builder model.ConfigBuilder + + mu sync.Mutex + m map[string]built +} + +type built struct { + value model.Config + err error +} + +// NewOnceConfigBuilder crates a new instance of the corresponding struct. +func NewOnceConfigBuilder(builder model.ConfigBuilder) *OnceConfigBuilder { + return &OnceConfigBuilder{ + builder: builder, + } +} + +// GetConfig implements the corresponding interface method. +func (ocb *OnceConfigBuilder) GetConfig(cwd string) (model.Config, error) { + ocb.mu.Lock() + defer ocb.mu.Unlock() + + if b, ok := ocb.m[cwd]; ok { + return b.value, b.err + } + + b := built{} + b.value, b.err = ocb.builder.GetConfig(cwd) + if ocb.m == nil { + ocb.m = make(map[string]built, 10) + } + ocb.m[cwd] = b + return b.value, b.err +} + +// SetOverride implements the corresponding interface method. +func (ocb *OnceConfigBuilder) SetOverride(override *model.ConfigOverride) { + ocb.mu.Lock() + defer ocb.mu.Unlock() + + if len(ocb.m) > 0 { + return + } + ocb.builder.SetOverride(override) +} diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/config/parser.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/config/parser.go new file mode 100644 index 00000000..71a04f04 --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/config/parser.go @@ -0,0 +1,42 @@ +package config + +import ( + "fmt" + "os" + "strings" + + "gopkg.in/yaml.v3" +) + +// FromYAML parses configuration from given YAML content. +func FromYAML(in []byte) (*PlainConfig, error) { + raw := PlainConfig{} + if err := yaml.Unmarshal(in, &raw); err != nil { + return nil, fmt.Errorf("cannot parse config from YAML file: %w", err) + } + + if raw.Version != nil && !strings.HasPrefix(*raw.Version, "1.") { + return nil, fmt.Errorf("unsupported config version: %s", *raw.Version) + } + + return &raw, nil +} + +// FromYAMLFile parses configuration from given file path. +func FromYAMLFile(path string) (*PlainConfig, error) { + in, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("cannot read file (%s): %w", path, err) + } + + raw := PlainConfig{} + if err := yaml.Unmarshal(in, &raw); err != nil { + return nil, fmt.Errorf("cannot parse config from YAML file: %w", err) + } + + if raw.Version != nil && !strings.HasPrefix(*raw.Version, "1.") { + return nil, fmt.Errorf("unsupported config version: %s", *raw.Version) + } + + return &raw, nil +} diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/config/plain.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/config/plain.go new file mode 100644 index 00000000..d19323f3 --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/config/plain.go @@ -0,0 +1,128 @@ +package config + +import ( + "errors" + "fmt" + "reflect" + "regexp" + "slices" + + "github.com/godoc-lint/godoc-lint/pkg/model" +) + +// PlainConfig represents the plain configuration type as users would provide +// via a config file (e.g., a YAML file). +type PlainConfig struct { + Version *string `yaml:"version" mapstructure:"version"` + Exclude []string `yaml:"exclude" mapstructure:"exclude"` + Include []string `yaml:"include" mapstructure:"include"` + Default *string `yaml:"default" mapstructure:"default"` + Enable []string `yaml:"enable" mapstructure:"enable"` + Disable []string `yaml:"disable" mapstructure:"disable"` + Options *PlainRuleOptions `yaml:"options" mapstructure:"options"` +} + +// PlainRuleOptions represents the plain rule options as users would provide via +// a config file (e.g., a YAML file). +type PlainRuleOptions struct { + MaxLenLength *uint `option:"max-len/length" yaml:"max-len/length" mapstructure:"max-len/length"` + MaxLenIncludeTests *bool `option:"max-len/include-tests" yaml:"max-len/include-tests" mapstructure:"max-len/include-tests"` + PkgDocIncludeTests *bool `option:"pkg-doc/include-tests" yaml:"pkg-doc/include-tests" mapstructure:"pkg-doc/include-tests"` + SinglePkgDocIncludeTests *bool `option:"single-pkg-doc/include-tests" yaml:"single-pkg-doc/include-tests" mapstructure:"single-pkg-doc/include-tests"` + RequirePkgDocIncludeTests *bool `option:"require-pkg-doc/include-tests" yaml:"require-pkg-doc/include-tests" mapstructure:"require-pkg-doc/include-tests"` + RequireDocIncludeTests *bool `option:"require-doc/include-tests" yaml:"require-doc/include-tests" mapstructure:"require-doc/include-tests"` + RequireDocIgnoreExported *bool `option:"require-doc/ignore-exported" yaml:"require-doc/ignore-exported" mapstructure:"require-doc/ignore-exported"` + RequireDocIgnoreUnexported *bool `option:"require-doc/ignore-unexported" yaml:"require-doc/ignore-unexported" mapstructure:"require-doc/ignore-unexported"` + StartWithNameIncludeTests *bool `option:"start-with-name/include-tests" yaml:"start-with-name/include-tests" mapstructure:"start-with-name/include-tests"` + StartWithNameIncludeUnexported *bool `option:"start-with-name/include-unexported" yaml:"start-with-name/include-unexported" mapstructure:"start-with-name/include-unexported"` + NoUnusedLinkIncludeTests *bool `option:"no-unused-link/include-tests" yaml:"no-unused-link/include-tests" mapstructure:"no-unused-link/include-tests"` +} + +func transferOptions(target *model.RuleOptions, source *PlainRuleOptions) { + resV := reflect.ValueOf(target).Elem() + resVT := resV.Type() + + resOptionMap := make(map[string]string, resVT.NumField()) + for i := range resVT.NumField() { + ft := resVT.Field(i) + key, ok := ft.Tag.Lookup("option") + if !ok { + continue + } + resOptionMap[key] = ft.Name + } + + v := reflect.ValueOf(source).Elem() + vt := v.Type() + for i := range vt.NumField() { + ft := vt.Field(i) + key, ok := ft.Tag.Lookup("option") + if !ok { + continue + } + if ft.Type.Kind() != reflect.Pointer { + continue + } + f := v.Field(i) + if f.IsNil() { + continue + } + resFieldName, ok := resOptionMap[key] + if !ok { + continue + } + resV.FieldByName(resFieldName).Set(f.Elem()) + } +} + +// Validate validates the plain configuration. +func (pcfg *PlainConfig) Validate() error { + var errs []error + + if pcfg.Default != nil && !slices.Contains(model.DefaultSetValues, model.DefaultSet(*pcfg.Default)) { + errs = append(errs, fmt.Errorf("invalid default set %q; must be one of %q", *pcfg.Default, model.DefaultSetValues)) + } + + if invalids := getInvalidRules(pcfg.Enable); len(invalids) > 0 { + errs = append(errs, fmt.Errorf("invalid rule name(s) to enable: %q", invalids)) + } + + if invalids := getInvalidRules(pcfg.Disable); len(invalids) > 0 { + errs = append(errs, fmt.Errorf("invalid rule name(s) to disable: %q", invalids)) + } + + // To avoid being too strict, we don't complain if a rule is enabled and disabled at the same time. + + if invalids := getInvalidRegexps(pcfg.Include); len(invalids) > 0 { + errs = append(errs, fmt.Errorf("invalid inclusion pattern(s): %q", invalids)) + } + + if invalids := getInvalidRegexps(pcfg.Exclude); len(invalids) > 0 { + errs = append(errs, fmt.Errorf("invalid exclusion pattern(s): %q", invalids)) + } + + if len(errs) > 0 { + return errors.Join(errs...) + } + return nil +} + +func getInvalidRules(names []string) []string { + invalids := make([]string, 0, len(names)) + for _, element := range names { + if !model.AllRules.Has(model.Rule(element)) { + invalids = append(invalids, element) + } + } + return invalids +} + +func getInvalidRegexps(values []string) []string { + invalids := make([]string, 0, len(values)) + for _, element := range values { + if _, err := regexp.Compile(element); err != nil { + invalids = append(invalids, element) + } + } + return invalids +} diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/inspect/inspector.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/inspect/inspector.go new file mode 100644 index 00000000..0040be0e --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/inspect/inspector.go @@ -0,0 +1,312 @@ +// Package inspect provides the pre-run inspection analyzer. +package inspect + +import ( + "errors" + "fmt" + "go/ast" + gdc "go/doc/comment" + "go/token" + "path/filepath" + "reflect" + "regexp" + "strings" + + "golang.org/x/tools/go/analysis" + + "github.com/godoc-lint/godoc-lint/pkg/model" + "github.com/godoc-lint/godoc-lint/pkg/util" +) + +const ( + metaName = "godoclint_inspect" + metaDoc = "Pre-run inspector for godoclint" + metaURL = "https://github.com/godoc-lint/godoc-lint" +) + +// Inspector implements the godoc-lint pre-run inspector. +type Inspector struct { + cb model.ConfigBuilder + exitFunc func(int, error) + + analyzer *analysis.Analyzer + parser gdc.Parser +} + +// NewInspector returns a new instance of the inspector. +func NewInspector(cb model.ConfigBuilder, exitFunc func(int, error)) *Inspector { + result := &Inspector{ + cb: cb, + exitFunc: exitFunc, + analyzer: &analysis.Analyzer{ + Name: metaName, + Doc: metaDoc, + URL: metaURL, + ResultType: reflect.TypeOf(new(model.InspectorResult)), + }, + } + result.analyzer.Run = result.run + return result +} + +// GetAnalyzer returns the underlying analyzer. +func (i *Inspector) GetAnalyzer() *analysis.Analyzer { + return i.analyzer +} + +var topLevelOrphanCommentGroupPattern = regexp.MustCompile(`(?m)(?:^//.*\r?\n)+(?:\r?\n|\z)`) +var disableDirectivePattern = regexp.MustCompile(`(?m)//godoclint:disable(?: *([^\r\n]+))?\r?$`) + +func (i *Inspector) run(pass *analysis.Pass) (any, error) { + if len(pass.Files) == 0 { + return &model.InspectorResult{}, nil + } + + ft := util.GetPassFileToken(pass.Files[0], pass) + if ft == nil { + err := errors.New("cannot prepare config") + if i.exitFunc != nil { + i.exitFunc(2, err) + } + return nil, err + } + + pkgDir := filepath.Dir(ft.Name()) + cfg, err := i.cb.GetConfig(pkgDir) + if err != nil { + if i.exitFunc != nil { + i.exitFunc(2, err) + } + return nil, err + } + + inspect := func(f *ast.File) (*model.FileInspection, error) { + ft := util.GetPassFileToken(f, pass) + if ft == nil { + return nil, nil + } + + raw, err := pass.ReadFile(ft.Name()) + if err != nil { + return nil, fmt.Errorf("cannot read file %q: %v", ft.Name(), err) + } + + // Extract package godoc, if any. + packageDoc := i.extractCommentGroup(f.Doc) + + // Extract top-level //godoclint:disable directives. + disabledRules := model.InspectorResultDisableRules{} + for _, match := range topLevelOrphanCommentGroupPattern.FindAll(raw, -1) { + d := extractDisableDirectivesInComment(string(match)) + disabledRules.All = disabledRules.All || d.All + disabledRules.Rules = disabledRules.Rules.Merge(d.Rules) + } + + // Extract top-level symbol declarations. + decls := make([]model.SymbolDecl, 0, len(f.Decls)) + for _, d := range f.Decls { + switch dt := d.(type) { + case *ast.FuncDecl: + decls = append(decls, model.SymbolDecl{ + Decl: d, + Kind: model.SymbolDeclKindFunc, + Name: dt.Name.Name, + Ident: dt.Name, + Doc: i.extractCommentGroup(dt.Doc), + }) + case *ast.BadDecl: + decls = append(decls, model.SymbolDecl{ + Decl: d, + Kind: model.SymbolDeclKindBad, + }) + case *ast.GenDecl: + switch dt.Tok { + case token.CONST, token.VAR: + kind := model.SymbolDeclKindConst + if dt.Tok == token.VAR { + kind = model.SymbolDeclKindVar + } + if dt.Lparen == token.NoPos { + // cases: + // const ... (single line) + // var ... (single line) + + spec := dt.Specs[0].(*ast.ValueSpec) + if len(spec.Names) == 1 { + // cases: + // const foo = 0 + // var foo = 0 + decls = append(decls, model.SymbolDecl{ + Decl: d, + Kind: kind, + Name: spec.Names[0].Name, + Ident: spec.Names[0], + Doc: i.extractCommentGroup(dt.Doc), + TrailingDoc: i.extractCommentGroup(spec.Comment), + }) + } else { + // cases: + // const foo, bar = 0, 0 + // var foo, bar = 0, 0 + doc := i.extractCommentGroup(dt.Doc) + trailingDoc := i.extractCommentGroup(spec.Comment) + for ix, n := range spec.Names { + decls = append(decls, model.SymbolDecl{ + Decl: d, + Kind: kind, + Name: n.Name, + Ident: n, + Doc: doc, + TrailingDoc: trailingDoc, + MultiNameDecl: true, + MultiNameIndex: ix, + }) + } + } + } else { + // cases: + // const ( + // foo = 0 + // ) + // var ( + // foo = 0 + // ) + // const ( + // foo, bar = 0, 0 + // ) + // var ( + // foo, bar = 0, 0 + // ) + + parentDoc := i.extractCommentGroup(dt.Doc) + for spix, s := range dt.Specs { + spec := s.(*ast.ValueSpec) + doc := i.extractCommentGroup(spec.Doc) + trailingDoc := i.extractCommentGroup(spec.Comment) + for ix, n := range spec.Names { + decls = append(decls, model.SymbolDecl{ + Decl: d, + Kind: kind, + Name: n.Name, + Ident: n, + Doc: doc, + TrailingDoc: trailingDoc, + ParentDoc: parentDoc, + MultiNameDecl: len(spec.Names) > 1, + MultiNameIndex: ix, + MultiSpecDecl: true, + MultiSpecIndex: spix, + }) + } + } + } + case token.TYPE: + if dt.Lparen == token.NoPos { + // case: + // type foo int + + spec := dt.Specs[0].(*ast.TypeSpec) + decls = append(decls, model.SymbolDecl{ + Decl: d, + Kind: model.SymbolDeclKindType, + IsTypeAlias: spec.Assign != token.NoPos, + Name: spec.Name.Name, + Ident: spec.Name, + Doc: i.extractCommentGroup(dt.Doc), + TrailingDoc: i.extractCommentGroup(spec.Comment), + }) + } else { + // case: + // type ( + // foo int + // ) + + parentDoc := i.extractCommentGroup(dt.Doc) + for spix, s := range dt.Specs { + spec := s.(*ast.TypeSpec) + decls = append(decls, model.SymbolDecl{ + Decl: d, + Kind: model.SymbolDeclKindType, + IsTypeAlias: spec.Assign != token.NoPos, + Name: spec.Name.Name, + Ident: spec.Name, + Doc: i.extractCommentGroup(spec.Doc), + TrailingDoc: i.extractCommentGroup(spec.Comment), + ParentDoc: parentDoc, + MultiSpecDecl: true, + MultiSpecIndex: spix, + }) + } + } + default: + continue + } + } + } + + return &model.FileInspection{ + DisabledRules: disabledRules, + PackageDoc: packageDoc, + SymbolDecl: decls, + }, nil + } + + result := &model.InspectorResult{ + Files: make(map[*ast.File]*model.FileInspection, len(pass.Files)), + } + + for _, f := range pass.Files { + ft := util.GetPassFileToken(f, pass) + if ft == nil { + continue + } + if !cfg.IsPathApplicable(ft.Name()) { + continue + } + + if fi, err := inspect(f); err != nil { + return nil, fmt.Errorf("inspector failed: %w", err) + } else { + result.Files[f] = fi + } + } + return result, nil +} + +func (i *Inspector) extractCommentGroup(cg *ast.CommentGroup) *model.CommentGroup { + if cg == nil { + return nil + } + + lines := make([]string, 0, len(cg.List)) + for _, l := range cg.List { + lines = append(lines, l.Text) + } + rawText := strings.Join(lines, "\n") + + text := cg.Text() + return &model.CommentGroup{ + CG: *cg, + Parsed: *i.parser.Parse(text), + Text: text, + DisabledRules: extractDisableDirectivesInComment(rawText), + } +} + +func extractDisableDirectivesInComment(s string) model.InspectorResultDisableRules { + result := model.InspectorResultDisableRules{} + for _, directive := range disableDirectivePattern.FindAllStringSubmatch(s, -1) { + args := directive[1] + if args == "" { + result.All = true + continue + } + + for name := range strings.SplitSeq(strings.TrimSpace(args), " ") { + if model.AllRules.Has(model.Rule(name)) { + result.Rules = result.Rules.Add(model.Rule(name)) + } + } + } + return result +} diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/model/analyzer.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/model/analyzer.go new file mode 100644 index 00000000..ded44c4e --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/model/analyzer.go @@ -0,0 +1,11 @@ +package model + +import ( + "golang.org/x/tools/go/analysis" +) + +// Analyzer defines an analyzer. +type Analyzer interface { + // GetAnalyzer returns the underlying analyzer. + GetAnalyzer() *analysis.Analyzer +} diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/model/checker.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/model/checker.go new file mode 100644 index 00000000..dca42e27 --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/model/checker.go @@ -0,0 +1,24 @@ +package model + +import "golang.org/x/tools/go/analysis" + +// AnalysisContext provides contextual information about the running analysis. +type AnalysisContext struct { + // Config provides analyzer configuration. + Config Config + + // InspectorResult is the analysis result of the pre-run inspector. + InspectorResult *InspectorResult + + // Pass is the analysis Pass instance. + Pass *analysis.Pass +} + +// Checker defines a rule checker. +type Checker interface { + // GetCoveredRules returns the set of rules applied by the checker. + GetCoveredRules() RuleSet + + // Apply checks for the rule(s). + Apply(actx *AnalysisContext) error +} diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/model/config.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/model/config.go new file mode 100644 index 00000000..78fa62a2 --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/model/config.go @@ -0,0 +1,123 @@ +package model + +import ( + "maps" + "regexp" + "slices" +) + +// ConfigBuilder defines a configuration builder. +type ConfigBuilder interface { + // SetOverride sets the configuration override. + SetOverride(override *ConfigOverride) + + // GetConfig builds and returns the configuration object for the given path. + GetConfig(cwd string) (Config, error) +} + +// DefaultSet defines the default set of rules to enable. +type DefaultSet string + +const ( + // DefaultSetAll enables all rules. + DefaultSetAll DefaultSet = "all" + // DefaultSetNone disables all rules. + DefaultSetNone DefaultSet = "none" + // DefaultSetBasic enables a basic set of rules. + DefaultSetBasic DefaultSet = "basic" + + // DefaultDefaultSet is the default set of rules to enable. + DefaultDefaultSet = DefaultSetBasic +) + +// DefaultSetToRules maps default sets to the corresponding rule sets. +var DefaultSetToRules = map[DefaultSet]RuleSet{ + DefaultSetAll: AllRules, + DefaultSetNone: {}, + DefaultSetBasic: func() RuleSet { + return RuleSet{}.Add( + PkgDocRule, + SinglePkgDocRule, + StartWithNameRule, + DeprecatedRule, + ) + }(), +} + +// DefaultSetValues holds the valid values for DefaultSet. +var DefaultSetValues = func() []DefaultSet { + values := slices.Collect(maps.Keys(DefaultSetToRules)) + slices.Sort(values) + return values +}() + +// ConfigOverride represents a configuration override. +// +// Non-nil values (including empty slices) indicate that the corresponding field +// is overridden. +type ConfigOverride struct { + // ConfigFilePath is the path to config file. + ConfigFilePath *string + + // Include is the overridden list of regexp patterns matching the files that + // the linter should include. + Include []*regexp.Regexp + + // Exclude is the overridden list of regexp patterns matching the files that + // the linter should exclude. + Exclude []*regexp.Regexp + + // Default is the default set of rules to enable. + Default *DefaultSet + + // Enable is the overridden list of rules to enable. + Enable *RuleSet + + // Disable is the overridden list of rules to disable. + Disable *RuleSet +} + +// NewConfigOverride returns a new config override instance. +func NewConfigOverride() *ConfigOverride { + return &ConfigOverride{} +} + +// Config defines an analyzer configuration. +type Config interface { + // GetCWD returns the directory that the configuration is applied to. This + // is the base to compute relative paths to include/exclude files. + GetCWD() string + + // GetConfigFilePath returns the path to the configuration file. If there is + // no configuration file, which is the case when the default is used, this + // will be an empty string. + GetConfigFilePath() string + + // IsAnyRuleEnabled determines if any of the given rule names is among + // enabled rules, or not among disabled rules. + IsAnyRuleApplicable(RuleSet) bool + + // IsPathApplicable determines if the given path matches the included path + // patterns, or does not match the excluded path patterns. + IsPathApplicable(path string) bool + + // Returns the rule-specific options. + // + // It never returns a nil pointer. + GetRuleOptions() *RuleOptions +} + +// RuleOptions represents individual linter rule configurations. +type RuleOptions struct { + MaxLenLength uint `option:"max-len/length"` + MaxLenIncludeTests bool `option:"max-len/include-tests"` + PkgDocIncludeTests bool `option:"pkg-doc/include-tests"` + SinglePkgDocIncludeTests bool `option:"single-pkg-doc/include-tests"` + RequirePkgDocIncludeTests bool `option:"require-pkg-doc/include-tests"` + RequireDocIncludeTests bool `option:"require-doc/include-tests"` + RequireDocIgnoreExported bool `option:"require-doc/ignore-exported"` + RequireDocIgnoreUnexported bool `option:"require-doc/ignore-unexported"` + StartWithNameIncludeTests bool `option:"start-with-name/include-tests"` + StartWithNameIncludeUnexported bool `option:"start-with-name/include-unexported"` + NoUnusedLinkIncludeTests bool `option:"no-unused-link/include-tests"` +} diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/model/doc.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/model/doc.go new file mode 100644 index 00000000..b4afa8d4 --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/model/doc.go @@ -0,0 +1,2 @@ +// Package model provides data models for the linter. +package model diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/model/inspector.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/model/inspector.go new file mode 100644 index 00000000..3166e17f --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/model/inspector.go @@ -0,0 +1,189 @@ +package model + +import ( + "go/ast" + "go/doc/comment" + + "golang.org/x/tools/go/analysis" +) + +// Inspector defines a pre-run inspector. +type Inspector interface { + // GetAnalyzer returns the underlying analyzer. + GetAnalyzer() *analysis.Analyzer +} + +// InspectorResult represents the result of the inspector analysis. +type InspectorResult struct { + // Files provides extracted information per AST file. + Files map[*ast.File]*FileInspection +} + +// FileInspection represents the inspection result for a single file. +type FileInspection struct { + // DisabledRules contains information about rules disabled at top level. + DisabledRules InspectorResultDisableRules + + // PackageDoc represents the package godoc, if any. + PackageDoc *CommentGroup + + // SymbolDecl represents symbols declared in the package file. + SymbolDecl []SymbolDecl +} + +// InspectorResultDisableRules contains the list of disabled rules. +type InspectorResultDisableRules struct { + // All indicates whether all rules are disabled. + All bool + + // Rules is the set of rules disabled. + Rules RuleSet +} + +// SymbolDeclKind is the enum type for the symbol declarations. +type SymbolDeclKind string + +const ( + // SymbolDeclKindBad represents an unknown declaration kind. + SymbolDeclKindBad SymbolDeclKind = "bad" + // SymbolDeclKindFunc represents a function declaration kind. + SymbolDeclKindFunc SymbolDeclKind = "func" + // SymbolDeclKindConst represents a const declaration kind. + SymbolDeclKindConst SymbolDeclKind = "const" + // SymbolDeclKindType represents a type declaration kind. + SymbolDeclKindType SymbolDeclKind = "type" + // SymbolDeclKindVar represents a var declaration kind. + SymbolDeclKindVar SymbolDeclKind = "var" +) + +// SymbolDecl represents a top level declaration. +type SymbolDecl struct { + // Decl is the underlying declaration node. + Decl ast.Decl + + // Kind is the declaration kind (e.g., func or type). + Kind SymbolDeclKind + + // IsTypeAlias indicates that the type symbol is an alias. For example: + // + // type Foo = int + // + // This is always false for non-type declaration (e.g., const or var). + IsTypeAlias bool + + // Name is the name of the declared symbol. + Name string + + // Ident is the symbol identifier node. + Ident *ast.Ident + + // MultiNameDecl determines whether the symbol is declared as part of a + // multi-name declaration spec; For example: + // + // const foo, bar = 0, 0 + // + // This field is only valid for const, var, or type declarations. + MultiNameDecl bool + + // MultiNameIndex is the index of the declared symbol within the spec. For + // example, in the below declaration, the index of "foo" and "bar" are 0 and + // 1, respectively: + // + // const foo, bar = 0, 0 + // + // In single-name specs, this will be 0. + MultiNameIndex int + + // MultiSpecDecl determines whether the symbol is declared as part of a + // multi-spec declaration. A multi spec declaration is const/var/type + // declaration with a pair of grouping brackets, even if there is only one + // spec between the brackets. For example, these are multi-spec + // declarations: + // + // const ( + // foo = 0 + // ) + // + // const ( + // foo, bar = 0, 0 + // ) + // + // const ( + // foo = 0 + // bar = 0 + // ) + // + // const ( + // foo, bar = 0, 0 + // baz = 0 + // ) + // + MultiSpecDecl bool + + // SpecIndex is the index of the spec where the symbol is declared. For + // example, in the below declaration, the index of "foo" and "bar" are 0 and + // 1, respectively: + // + // const ( + // foo = 0 + // bar = 0 + // ) + // + // In single-spec declarations, this will be 0. + MultiSpecIndex int + + // Doc is the comment group associated to the symbol. For example: + // + // // godoc + // const foo = 0 + // + // const ( + // // godoc + // foo = 0 + // ) + // + // Note that, as in the first example above, for single-spec declarations + // (i.e., single line declarations), the godoc above the const/var/type + // keyword is considered as the declaration doc, and the parent doc will be + // nil. + Doc *CommentGroup + + // TrailingDoc is the comment group that is following the symbol + // declaration. For example, this is a trailing comment group: + // + // const ( + // foo = 0 // trailing comment group. + // ) + // + TrailingDoc *CommentGroup + + // Doc is the comment group associated to the parent declaration. For + // instance: + // + // // parent godoc + // const ( + // // godoc + // Foo = 0 + // ) + // + // Note that for single-spec declarations (i.e., single line declarations), + // the godoc above the const/var/type keyword is considered as the + // declaration doc, and the parent doc will be nil. + ParentDoc *CommentGroup +} + +// CommentGroup represents an ast.CommentGroup and its parsed godoc instance. +type CommentGroup struct { + // CG represents the AST comment group. + CG ast.CommentGroup + + // Parsed represents the comment group parsed into a godoc. + Parsed comment.Doc + + // Test is the comment group text. + Text string + + // DisabledRules contains information about rules disabled in the comment + // group. + DisabledRules InspectorResultDisableRules +} diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/model/registry.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/model/registry.go new file mode 100644 index 00000000..64512291 --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/model/registry.go @@ -0,0 +1,14 @@ +package model + +// Registry defines a registry of checkers. +type Registry interface { + // Add registers a new checker. + Add(Checker) + + // List returns a slice of the registered checkers. + List() []Checker + + // GetCoveredRules returns the set of rules covered by the registered + // checkers. + GetCoveredRules() RuleSet +} diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/model/rule.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/model/rule.go new file mode 100644 index 00000000..c53fcf61 --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/model/rule.go @@ -0,0 +1,37 @@ +package model + +// Rule represents a rule. +type Rule string + +const ( + // PkgDocRule represents the "pkg-doc" rule. + PkgDocRule Rule = "pkg-doc" + // SinglePkgDocRule represents the "single-pkg-doc" rule. + SinglePkgDocRule Rule = "single-pkg-doc" + // RequirePkgDocRule represents the "require-pkg-doc" rule. + RequirePkgDocRule Rule = "require-pkg-doc" + // StartWithNameRule represents the "start-with-name" rule. + StartWithNameRule Rule = "start-with-name" + // RequireDocRule represents the "require-doc" rule. + RequireDocRule Rule = "require-doc" + // DeprecatedRule represents the "deprecated" rule. + DeprecatedRule Rule = "deprecated" + // MaxLenRule represents the "max-len" rule. + MaxLenRule Rule = "max-len" + // NoUnusedLinkRule represents the "no-unused-link" rule. + NoUnusedLinkRule Rule = "no-unused-link" +) + +// AllRules is the set of all supported rules. +var AllRules = func() RuleSet { + return RuleSet{}.Add( + PkgDocRule, + SinglePkgDocRule, + RequirePkgDocRule, + StartWithNameRule, + RequireDocRule, + DeprecatedRule, + MaxLenRule, + NoUnusedLinkRule, + ) +}() diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/model/ruleset.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/model/ruleset.go new file mode 100644 index 00000000..0cf19544 --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/model/ruleset.go @@ -0,0 +1,97 @@ +package model + +import "slices" + +// RuleSet represents an immutable set of rule names. +// +// A zero rule set represents an empty set. +type RuleSet struct { + m map[Rule]struct{} +} + +// Merge combines the rules in the current and the given rule sets into a new +// set. +func (rs RuleSet) Merge(another RuleSet) RuleSet { + result := RuleSet{ + m: make(map[Rule]struct{}, len(rs.m)+len(another.m)), + } + for r := range rs.m { + result.m[r] = struct{}{} + } + for r := range another.m { + result.m[r] = struct{}{} + } + return result +} + +// Add returns a new rule set containing the rules in the current set and the +// rules provided as arguments. +func (rs RuleSet) Add(rules ...Rule) RuleSet { + result := RuleSet{ + m: make(map[Rule]struct{}, len(rs.m)+len(rules)), + } + for r := range rs.m { + result.m[r] = struct{}{} + } + for _, r := range rules { + result.m[r] = struct{}{} + } + return result +} + +// Remove returns a new rule set containing the rules in the current set +// excluding those provided as arguments. +func (rs RuleSet) Remove(rules ...Rule) RuleSet { + result := RuleSet{ + m: make(map[Rule]struct{}, len(rs.m)), + } + for r := range rs.m { + result.m[r] = struct{}{} + } + for _, r := range rules { + delete(result.m, r) + } + return result +} + +// Has determines whether the given rule is in the set. +func (rs RuleSet) Has(rule Rule) bool { + _, ok := rs.m[rule] + return ok +} + +// HasCommonsWith indicates at least one rule is in common with the given rule +// set. +// +// If the given set is empty/zero, the method will return false. +func (rs RuleSet) HasCommonsWith(another RuleSet) bool { + for r := range another.m { + if _, ok := rs.m[r]; ok { + return true + } + } + return false +} + +// IsSupersetOf indicates that all rules in the given set are in the current +// set. +// +// If the given set is empty/zero, the method will return true. +func (rs RuleSet) IsSupersetOf(another RuleSet) bool { + for r := range another.m { + if _, ok := rs.m[r]; !ok { + return false + } + } + return true +} + +// List returns a slice of the rules in the set. +func (rs RuleSet) List() []Rule { + rules := make([]Rule, 0, len(rs.m)) + for r := range rs.m { + rules = append(rules, r) + } + slices.Sort(rules) + return rules +} diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/util/ast.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/util/ast.go new file mode 100644 index 00000000..1441001d --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/util/ast.go @@ -0,0 +1,66 @@ +package util + +import ( + "go/ast" + "go/token" + "iter" + "strings" + + "golang.org/x/tools/go/analysis" + + "github.com/godoc-lint/godoc-lint/pkg/model" +) + +// GetPassFileToken is a helper function to return the file token associated +// with the given AST file. +func GetPassFileToken(f *ast.File, pass *analysis.Pass) *token.File { + if f.Pos() == token.NoPos { + return nil + } + ft := pass.Fset.File(f.Pos()) + if ft == nil { + return nil + } + return ft +} + +// AnalysisApplicableFiles returns an iterator looping over files that are ready +// to be analyzed. +// +// The yield-ed arguments are never nil. +func AnalysisApplicableFiles(actx *model.AnalysisContext, includeTests bool, ruleSet model.RuleSet) iter.Seq2[*ast.File, *model.FileInspection] { + return func(yield func(*ast.File, *model.FileInspection) bool) { + if actx.InspectorResult == nil { + return + } + + for _, f := range actx.Pass.Files { + ir := actx.InspectorResult.Files[f] + + if ir == nil { + continue + } + + ft := GetPassFileToken(f, actx.Pass) + if ft == nil { + continue + } + + if !actx.Config.IsPathApplicable(ft.Name()) { + continue + } + + if !includeTests && strings.HasSuffix(ft.Name(), "_test.go") { + continue + } + + if ir.DisabledRules.All || ir.DisabledRules.Rules.IsSupersetOf(ruleSet) { + continue + } + + if !yield(f, ir) { + return + } + } + } +} diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/util/doc.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/util/doc.go new file mode 100644 index 00000000..1c40b746 --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/util/doc.go @@ -0,0 +1,2 @@ +// Package util provides utility functions for the linter. +package util diff --git a/vendor/github.com/godoc-lint/godoc-lint/pkg/util/path.go b/vendor/github.com/godoc-lint/godoc-lint/pkg/util/path.go new file mode 100644 index 00000000..b7dd451e --- /dev/null +++ b/vendor/github.com/godoc-lint/godoc-lint/pkg/util/path.go @@ -0,0 +1,16 @@ +package util + +import ( + "path/filepath" + "strings" +) + +// IsPathUnderBaseDir determines whether the given path is a sub-directory of +// the given base, lexicographically. +func IsPathUnderBaseDir(baseDir, path string) bool { + rel, err := filepath.Rel(baseDir, path) + if err != nil { + return false + } + return rel == "." || rel != ".." && !strings.HasPrefix(filepath.ToSlash(rel), "../") +} diff --git a/vendor/github.com/golangci/asciicheck/.gitignore b/vendor/github.com/golangci/asciicheck/.gitignore new file mode 100644 index 00000000..26938fd8 --- /dev/null +++ b/vendor/github.com/golangci/asciicheck/.gitignore @@ -0,0 +1,21 @@ +# IntelliJ project files +.idea +*.iml +out +gen + +# Go template +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +/asciicheck diff --git a/vendor/github.com/golangci/asciicheck/.golangci.yml b/vendor/github.com/golangci/asciicheck/.golangci.yml new file mode 100644 index 00000000..e28845eb --- /dev/null +++ b/vendor/github.com/golangci/asciicheck/.golangci.yml @@ -0,0 +1,87 @@ +version: "2" + +formatters: + enable: + - gci + - gofumpt + settings: + gofumpt: + extra-rules: true + +linters: + default: all + disable: + - cyclop # duplicate of gocyclo + - dupl + - errchkjson + - exhaustive + - exhaustruct + - lll + - mnd + - nilnil + - nlreturn + - nonamedreturns + - paralleltest + - prealloc + - rowserrcheck # not relevant (SQL) + - sqlclosecheck # not relevant (SQL) + - testpackage + - tparallel + - varnamelen + - wsl # Deprecated + - forcetypeassert # recheck in the future. + + settings: + depguard: + rules: + main: + deny: + - pkg: github.com/instana/testify + desc: not allowed + - pkg: github.com/pkg/errors + desc: Should be replaced by standard lib errors package + funlen: + lines: -1 + statements: 40 + goconst: + min-len: 5 + min-occurrences: 3 + gocritic: + disabled-checks: + - sloppyReassign + - rangeValCopy + - octalLiteral + - paramTypeCombine # already handle by gofumpt.extra-rules + enabled-tags: + - diagnostic + - style + - performance + settings: + hugeParam: + sizeThreshold: 100 + gocyclo: + min-complexity: 20 + godox: + keywords: + - FIXME + govet: + disable: + - fieldalignment + enable-all: true + misspell: + locale: US + mnd: + ignored-numbers: + - "124" + wsl: + force-case-trailing-whitespace: 1 + allow-trailing-comment: true + exclusions: + presets: + - comments + - std-error-handling + - common-false-positives + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 diff --git a/vendor/github.com/tdakkota/asciicheck/LICENSE b/vendor/github.com/golangci/asciicheck/LICENSE similarity index 100% rename from vendor/github.com/tdakkota/asciicheck/LICENSE rename to vendor/github.com/golangci/asciicheck/LICENSE diff --git a/vendor/github.com/golangci/asciicheck/Makefile b/vendor/github.com/golangci/asciicheck/Makefile new file mode 100644 index 00000000..cdfc2611 --- /dev/null +++ b/vendor/github.com/golangci/asciicheck/Makefile @@ -0,0 +1,15 @@ +.PHONY: clean check test build + +default: clean check test build + +clean: + rm -rf dist/ cover.out + +test: clean + go test -v -cover ./... + +check: + golangci-lint run + +build: + go build -ldflags "-s -w" -trimpath ./cmd/asciicheck/ diff --git a/vendor/github.com/tdakkota/asciicheck/README.md b/vendor/github.com/golangci/asciicheck/README.md similarity index 75% rename from vendor/github.com/tdakkota/asciicheck/README.md rename to vendor/github.com/golangci/asciicheck/README.md index a7ff5884..0d1da4b9 100644 --- a/vendor/github.com/tdakkota/asciicheck/README.md +++ b/vendor/github.com/golangci/asciicheck/README.md @@ -1,13 +1,19 @@ -# asciicheck [![Go Report Card](https://goreportcard.com/badge/github.com/tdakkota/asciicheck)](https://goreportcard.com/report/github.com/tdakkota/asciicheck) [![codecov](https://codecov.io/gh/tdakkota/asciicheck/branch/master/graph/badge.svg)](https://codecov.io/gh/tdakkota/asciicheck) ![Go](https://github.com/tdakkota/asciicheck/workflows/Go/badge.svg) +# asciicheck + +[![Go Report Card](https://goreportcard.com/badge/github.com/golangci/asciicheck)](https://goreportcard.com/report/github.com/golangci/asciicheck) + Simple linter to check that your code does not contain non-ASCII identifiers -# Install +The project has been moved to the golangci organization because the GitHub account of the original author (@tdakkota) is no longer available. + +## Install +```bash +go install github.com/golangci/asciicheck/cmd/asciicheck@latest ``` -go get -u github.com/tdakkota/asciicheck/cmd/asciicheck -``` -# Reason to use +## Reason to use + So, do you see this code? Looks correct, isn't it? ```go @@ -22,20 +28,24 @@ func main() { fmt.Println(s) } ``` + But if you try to run it, you will get an error: + ``` ./prog.go:8:7: undefined: TestStruct ``` What? `TestStruct` is defined above, but compiler thinks diffrent. Why? **Answer**: + Because `TestStruct` is not `TеstStruct`. ``` type TеstStruct struct{} ^ this 'e' (U+0435) is not 'e' (U+0065) ``` -# Usage +## Usage + asciicheck uses [`singlechecker`](https://pkg.go.dev/golang.org/x/tools/go/analysis/singlechecker) package to run: ``` diff --git a/vendor/github.com/tdakkota/asciicheck/ascii.go b/vendor/github.com/golangci/asciicheck/ascii.go similarity index 100% rename from vendor/github.com/tdakkota/asciicheck/ascii.go rename to vendor/github.com/golangci/asciicheck/ascii.go diff --git a/vendor/github.com/tdakkota/asciicheck/asciicheck.go b/vendor/github.com/golangci/asciicheck/asciicheck.go similarity index 90% rename from vendor/github.com/tdakkota/asciicheck/asciicheck.go rename to vendor/github.com/golangci/asciicheck/asciicheck.go index 2ec141ec..0983c727 100644 --- a/vendor/github.com/tdakkota/asciicheck/asciicheck.go +++ b/vendor/github.com/golangci/asciicheck/asciicheck.go @@ -20,7 +20,7 @@ func NewAnalyzer() *analysis.Analyzer { } func run(pass *analysis.Pass) (any, error) { - inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + insp := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{ (*ast.File)(nil), @@ -35,7 +35,7 @@ func run(pass *analysis.Pass) (any, error) { (*ast.AssignStmt)(nil), } - inspect.Preorder(nodeFilter, func(n ast.Node) { + insp.Preorder(nodeFilter, func(n ast.Node) { switch n := n.(type) { case *ast.File: checkIdent(pass, n.Name) @@ -71,6 +71,7 @@ func run(pass *analysis.Pass) (any, error) { } } }) + return nil, nil } @@ -84,7 +85,7 @@ func checkIdent(pass *analysis.Pass, v *ast.Ident) { pass.Report( analysis.Diagnostic{ Pos: v.Pos(), - Message: fmt.Sprintf("identifier \"%s\" contain non-ASCII character: %#U", v.Name, ch), + Message: fmt.Sprintf("identifier %q contain non-ASCII character: %#U", v.Name, ch), }, ) } @@ -94,6 +95,7 @@ func checkFieldList(pass *analysis.Pass, f *ast.FieldList) { if f == nil { return } + for _, f := range f.List { for _, name := range f.Names { checkIdent(pass, name) diff --git a/vendor/github.com/golangci/golangci-lint/v2/internal/cache/cache.go b/vendor/github.com/golangci/golangci-lint/v2/internal/cache/cache.go index 627993d2..138a3614 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/internal/cache/cache.go +++ b/vendor/github.com/golangci/golangci-lint/v2/internal/cache/cache.go @@ -166,7 +166,7 @@ func (c *Cache) computePkgHash(pkg *packages.Package) (hashResults, error) { fmt.Fprintf(key, "pkgpath %s\n", pkg.PkgPath) - for _, f := range pkg.CompiledGoFiles { + for _, f := range slices.Concat(pkg.CompiledGoFiles, pkg.IgnoredFiles) { h, fErr := c.fileHash(f) if fErr != nil { return nil, fmt.Errorf("failed to calculate file %s hash: %w", f, fErr) diff --git a/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/analysisflags/readme.md b/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/analysisflags/readme.md index 4d221d4c..6035c222 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/analysisflags/readme.md +++ b/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/analysisflags/readme.md @@ -5,4 +5,7 @@ This is just a copy of the code without any changes. ## History -- sync with https://github.com/golang/tools/blob/v0.28.0 +- https://github.com/golangci/golangci-lint/pull/6076 + - sync with https://github.com/golang/tools/blob/v0.37.0/go/analysis/internal/analysisflags +- https://github.com/golangci/golangci-lint/pull/5576 + - sync with https://github.com/golang/tools/blob/v0.28.0/go/analysis/internal/analysisflags diff --git a/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/analysisinternal/analysis.go b/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/analysisinternal/analysis.go index bb12600d..b613d167 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/analysisinternal/analysis.go +++ b/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/analysisinternal/analysis.go @@ -8,25 +8,30 @@ package analysisinternal import ( "fmt" - "os" + "slices" "golang.org/x/tools/go/analysis" ) -// MakeReadFile returns a simple implementation of the Pass.ReadFile function. -func MakeReadFile(pass *analysis.Pass) func(filename string) ([]byte, error) { +// A ReadFileFunc is a function that returns the +// contents of a file, such as [os.ReadFile]. +type ReadFileFunc = func(filename string) ([]byte, error) + +// CheckedReadFile returns a wrapper around a Pass.ReadFile +// function that performs the appropriate checks. +func CheckedReadFile(pass *analysis.Pass, readFile ReadFileFunc) ReadFileFunc { return func(filename string) ([]byte, error) { if err := CheckReadable(pass, filename); err != nil { return nil, err } - return os.ReadFile(filename) + return readFile(filename) } } // CheckReadable enforces the access policy defined by the ReadFile field of [analysis.Pass]. func CheckReadable(pass *analysis.Pass, filename string) error { - if slicesContains(pass.OtherFiles, filename) || - slicesContains(pass.IgnoredFiles, filename) { + if slices.Contains(pass.OtherFiles, filename) || + slices.Contains(pass.IgnoredFiles, filename) { return nil } for _, f := range pass.Files { @@ -36,13 +41,3 @@ func CheckReadable(pass *analysis.Pass, filename string) error { } return fmt.Errorf("Pass.ReadFile: %s is not among OtherFiles, IgnoredFiles, or names of Files", filename) } - -// TODO(adonovan): use go1.21 slices.Contains. -func slicesContains[S ~[]E, E comparable](slice S, x E) bool { - for _, elem := range slice { - if elem == x { - return true - } - } - return false -} diff --git a/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/analysisinternal/readme.md b/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/analysisinternal/readme.md index f301cdbe..6c54592d 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/analysisinternal/readme.md +++ b/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/analysisinternal/readme.md @@ -5,4 +5,7 @@ This is just a copy of the code without any changes. ## History -- sync with https://github.com/golang/tools/blob/v0.28.0 +- https://github.com/golangci/golangci-lint/pull/6076 + - sync with https://github.com/golang/tools/blob/v0.37.0/internal/analysisinternal/ +- https://github.com/golangci/golangci-lint/pull/5576 + - sync with https://github.com/golang/tools/blob/v0.28.0/internal/analysisinternal/ diff --git a/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/diff.go b/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/diff.go index a13547b7..c12bdfd2 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/diff.go +++ b/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/diff.go @@ -7,6 +7,7 @@ package diff import ( "fmt" + "slices" "sort" "strings" ) @@ -64,7 +65,7 @@ func ApplyBytes(src []byte, edits []Edit) ([]byte, error) { // It may return a different slice. func validate(src string, edits []Edit) ([]Edit, int, error) { if !sort.IsSorted(editsSort(edits)) { - edits = append([]Edit(nil), edits...) + edits = slices.Clone(edits) SortEdits(edits) } diff --git a/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/lcs/common.go b/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/lcs/common.go index c3e82dd2..27fa9ecb 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/lcs/common.go +++ b/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/lcs/common.go @@ -51,7 +51,7 @@ func (l lcs) fix() lcs { // from the set of diagonals in l, find a maximal non-conflicting set // this problem may be NP-complete, but we use a greedy heuristic, // which is quadratic, but with a better data structure, could be D log D. - // indepedent is not enough: {0,3,1} and {3,0,2} can't both occur in an lcs + // independent is not enough: {0,3,1} and {3,0,2} can't both occur in an lcs // which has to have monotone x and y if len(l) == 0 { return nil diff --git a/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/lcs/doc.go b/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/lcs/doc.go index 9029dd20..aa4b0fb5 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/lcs/doc.go +++ b/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/lcs/doc.go @@ -139,7 +139,7 @@ computed labels. That is the worst case. Had the code noticed (x,y)=(u,v)=(3,3) from the edgegraph. The implementation looks for a number of special cases to try to avoid computing an extra forward path. If the two-sided algorithm has stop early (because D has become too large) it will have found a forward LCS and a -backwards LCS. Ideally these go with disjoint prefixes and suffixes of A and B, but disjointness may fail and the two +backwards LCS. Ideally these go with disjoint prefixes and suffixes of A and B, but disjointedness may fail and the two computed LCS may conflict. (An easy example is where A is a suffix of B, and shares a short prefix. The backwards LCS is all of A, and the forward LCS is a prefix of A.) The algorithm combines the two to form a best-effort LCS. In the worst case the forward partial LCS may have to diff --git a/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/lcs/old.go b/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/lcs/old.go index 4353da15..4c346706 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/lcs/old.go +++ b/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/lcs/old.go @@ -105,7 +105,7 @@ func forward(e *editGraph) lcs { return ans } // from D to D+1 - for D := 0; D < e.limit; D++ { + for D := range e.limit { e.setForward(D+1, -(D + 1), e.getForward(D, -D)) if ok, ans := e.fdone(D+1, -(D + 1)); ok { return ans @@ -199,13 +199,14 @@ func (e *editGraph) bdone(D, k int) (bool, lcs) { } // run the backward algorithm, until success or up to the limit on D. +// (used only by tests) func backward(e *editGraph) lcs { e.setBackward(0, 0, e.ux) if ok, ans := e.bdone(0, 0); ok { return ans } // from D to D+1 - for D := 0; D < e.limit; D++ { + for D := range e.limit { e.setBackward(D+1, -(D + 1), e.getBackward(D, -D)-1) if ok, ans := e.bdone(D+1, -(D + 1)); ok { return ans @@ -299,7 +300,7 @@ func twosided(e *editGraph) lcs { e.setBackward(0, 0, e.ux) // from D to D+1 - for D := 0; D < e.limit; D++ { + for D := range e.limit { // just finished a backwards pass, so check if got, ok := e.twoDone(D, D); ok { return e.twolcs(D, D, got) @@ -376,10 +377,7 @@ func (e *editGraph) twoDone(df, db int) (int, bool) { if (df+db+e.delta)%2 != 0 { return 0, false // diagonals cannot overlap } - kmin := -db + e.delta - if -df > kmin { - kmin = -df - } + kmin := max(-df, -db+e.delta) kmax := db + e.delta if df < kmax { kmax = df diff --git a/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/ndiff.go b/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/ndiff.go index b429a69e..1c64d1ec 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/ndiff.go +++ b/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/ndiff.go @@ -72,7 +72,7 @@ func diffRunes(before, after []rune) []Edit { func runes(bytes []byte) []rune { n := utf8.RuneCount(bytes) runes := make([]rune, n) - for i := 0; i < n; i++ { + for i := range n { r, sz := utf8.DecodeRune(bytes) bytes = bytes[sz:] runes[i] = r diff --git a/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/readme.md b/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/readme.md index 4b979849..b28e41d9 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/readme.md +++ b/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/readme.md @@ -5,4 +5,7 @@ This is just a copy of the code without any changes. ## History -- sync with https://github.com/golang/tools/blob/v0.28.0 +- https://github.com/golangci/golangci-lint/pull/6076 + - sync with https://github.com/golang/tools/blob/v0.37.0/internal/diff/ +- https://github.com/golangci/golangci-lint/pull/5576 + - sync with https://github.com/golang/tools/blob/v0.28.0/internal/diff/ diff --git a/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/unified.go b/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/unified.go index cfbda610..9a786dbb 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/unified.go +++ b/vendor/github.com/golangci/golangci-lint/v2/internal/x/tools/diff/unified.go @@ -129,12 +129,12 @@ func toUnified(fromName, toName string, content string, edits []Edit, contextLin switch { case h != nil && start == last: - //direct extension + // direct extension case h != nil && start <= last+gap: - //within range of previous lines, add the joiners + // within range of previous lines, add the joiners addEqualLines(h, lines, last, start) default: - //need to start a new hunk + // need to start a new hunk if h != nil { // add the edge to the previous hunk addEqualLines(h, lines, last, last+contextLines) diff --git a/vendor/github.com/golangci/golangci-lint/v2/pkg/commands/custom.go b/vendor/github.com/golangci/golangci-lint/v2/pkg/commands/custom.go index 227df9be..e6a7f5ed 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/pkg/commands/custom.go +++ b/vendor/github.com/golangci/golangci-lint/v2/pkg/commands/custom.go @@ -5,6 +5,7 @@ import ( "log" "os" + "github.com/fatih/color" "github.com/spf13/cobra" "github.com/golangci/golangci-lint/v2/pkg/commands/internal" @@ -13,11 +14,19 @@ import ( const envKeepTempFiles = "CUSTOM_GCL_KEEP_TEMP_FILES" +type customOptions struct { + version string + name string + destination string +} + type customCommand struct { cmd *cobra.Command cfg *internal.Configuration + opts customOptions + log logutils.Log } @@ -33,6 +42,13 @@ func newCustomCommand(logger logutils.Log) *customCommand { SilenceUsage: true, } + flagSet := customCmd.PersistentFlags() + flagSet.SortFlags = false // sort them as they are defined here + + flagSet.StringVar(&c.opts.version, "version", "", color.GreenString("The golangci-lint version used to build the custom binary")) + flagSet.StringVar(&c.opts.name, "name", "", color.GreenString("The name of the custom binary")) + flagSet.StringVar(&c.opts.destination, "destination", "", color.GreenString("The directory path used to store the custom binary")) + c.cmd = customCmd return c @@ -44,6 +60,18 @@ func (c *customCommand) preRunE(_ *cobra.Command, _ []string) error { return err } + if c.opts.version != "" { + cfg.Version = c.opts.version + } + + if c.opts.name != "" { + cfg.Name = c.opts.name + } + + if c.opts.destination != "" { + cfg.Destination = c.opts.destination + } + err = cfg.Validate() if err != nil { return err diff --git a/vendor/github.com/golangci/golangci-lint/v2/pkg/commands/flagsets.go b/vendor/github.com/golangci/golangci-lint/v2/pkg/commands/flagsets.go index cc4b0043..2b61217c 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/pkg/commands/flagsets.go +++ b/vendor/github.com/golangci/golangci-lint/v2/pkg/commands/flagsets.go @@ -138,5 +138,5 @@ func setupIssuesFlagSet(v *viper.Viper, fs *pflag.FlagSet) { internal.AddFlagAndBind(v, fs, fs.Bool, "whole-files", "issues.whole-files", false, color.GreenString("Show issues in any part of update files (requires new-from-rev or new-from-patch)")) internal.AddFlagAndBind(v, fs, fs.Bool, "fix", "issues.fix", false, - color.GreenString("Fix found issues (if it's supported by the linter)")) + color.GreenString("Apply the fixes detected by the linters and formatters (if it's supported by the linter)")) } diff --git a/vendor/github.com/golangci/golangci-lint/v2/pkg/commands/fmt.go b/vendor/github.com/golangci/golangci-lint/v2/pkg/commands/fmt.go index 3292af3e..ab1aef45 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/pkg/commands/fmt.go +++ b/vendor/github.com/golangci/golangci-lint/v2/pkg/commands/fmt.go @@ -113,14 +113,11 @@ func (c *fmtCommand) preRunE(_ *cobra.Command, _ []string) error { } func (c *fmtCommand) execute(_ *cobra.Command, args []string) error { - paths, err := cleanArgs(args) - if err != nil { - return fmt.Errorf("failed to clean arguments: %w", err) - } + paths := cleanArgs(args) c.log.Infof("Formatting Go files...") - err = c.runner.Run(paths) + err := c.runner.Run(paths) if err != nil { return fmt.Errorf("failed to process files: %w", err) } @@ -134,25 +131,15 @@ func (c *fmtCommand) persistentPostRun(_ *cobra.Command, _ []string) { } } -func cleanArgs(args []string) ([]string, error) { +func cleanArgs(args []string) []string { if len(args) == 0 { - abs, err := filepath.Abs(".") - if err != nil { - return nil, err - } - - return []string{abs}, nil + return []string{"."} } var expanded []string for _, arg := range args { - abs, err := filepath.Abs(strings.ReplaceAll(arg, "...", "")) - if err != nil { - return nil, err - } - - expanded = append(expanded, abs) + expanded = append(expanded, filepath.Clean(strings.ReplaceAll(arg, "...", ""))) } - return expanded, nil + return expanded } diff --git a/vendor/github.com/golangci/golangci-lint/v2/pkg/commands/internal/builder.go b/vendor/github.com/golangci/golangci-lint/v2/pkg/commands/internal/builder.go index 5bb9b472..63f6f2f1 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/pkg/commands/internal/builder.go +++ b/vendor/github.com/golangci/golangci-lint/v2/pkg/commands/internal/builder.go @@ -92,7 +92,7 @@ func (b Builder) clone(ctx context.Context) error { //nolint:gosec // the variable is sanitized. cmd := exec.CommandContext(ctx, "git", "clone", "--branch", sanitizeVersion(b.cfg.Version), - "--single-branch", "--depth", "1", "-c advice.detachedHead=false", "-q", + "--single-branch", "--depth", "1", "-c", "advice.detachedHead=false", "-q", "https://github.com/golangci/golangci-lint.git", ) cmd.Dir = b.root @@ -257,7 +257,7 @@ func (b Builder) createVersion(orig string) (string, error) { continue } - dh, err := dirhash.HashDir(plugin.Path, "", dirhash.DefaultHash) + dh, err := hashDir(plugin.Path, "", dirhash.DefaultHash) if err != nil { return "", fmt.Errorf("hash plugin directory: %w", err) } diff --git a/vendor/github.com/golangci/golangci-lint/v2/pkg/commands/internal/dirhash.go b/vendor/github.com/golangci/golangci-lint/v2/pkg/commands/internal/dirhash.go new file mode 100644 index 00000000..16ea6a85 --- /dev/null +++ b/vendor/github.com/golangci/golangci-lint/v2/pkg/commands/internal/dirhash.go @@ -0,0 +1,93 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package internal + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "golang.org/x/mod/sumdb/dirhash" +) + +// Slightly modified copy of [dirhash.HashDir]. +// https://github.com/golang/mod/blob/v0.28.0/sumdb/dirhash/hash.go#L67-L79 +func hashDir(dir, prefix string, hash dirhash.Hash) (string, error) { + files, err := dirFiles(dir, prefix) + if err != nil { + return "", err + } + + osOpen := func(name string) (io.ReadCloser, error) { + return os.Open(filepath.Join(dir, strings.TrimPrefix(name, prefix))) + } + + return hash(files, osOpen) +} + +// Modified copy of [dirhash.DirFiles]. +// https://github.com/golang/mod/blob/v0.28.0/sumdb/dirhash/hash.go#L81-L109 +// And adapted to globally follows the rules from https://github.com/golang/mod/blob/v0.28.0/zip/zip.go +func dirFiles(dir, prefix string) ([]string, error) { + var files []string + + dir = filepath.Clean(dir) + + err := filepath.Walk(dir, func(file string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + if dir == file { + // Don't skip the top-level directory. + return nil + } + + switch info.Name() { + // Skip vendor and node directories. + case "vendor", "node_modules": + return filepath.SkipDir + + // Skip VCS directories. + case ".bzr", ".git", ".hg", ".svn": + return filepath.SkipDir + } + + // Skip submodules (directories containing go.mod files). + if goModInfo, err := os.Lstat(filepath.Join(dir, "go.mod")); err == nil && !goModInfo.IsDir() { + return filepath.SkipDir + } + + return nil + } + + if file == dir { + return fmt.Errorf("%s is not a directory", dir) + } + + if !info.Mode().IsRegular() { + return nil + } + + rel := file + + if dir != "." { + rel = file[len(dir)+1:] + } + + f := filepath.Join(prefix, rel) + + files = append(files, filepath.ToSlash(f)) + + return nil + }) + if err != nil { + return nil, err + } + return files, nil +} diff --git a/vendor/github.com/golangci/golangci-lint/v2/pkg/commands/root.go b/vendor/github.com/golangci/golangci-lint/v2/pkg/commands/root.go index 3a160bea..9fa36f25 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/pkg/commands/root.go +++ b/vendor/github.com/golangci/golangci-lint/v2/pkg/commands/root.go @@ -127,7 +127,7 @@ func forceRootParsePersistentFlags() (*rootOptions, error) { fs := pflag.NewFlagSet("config flag set", pflag.ContinueOnError) // Ignore unknown flags because we will parse the command flags later. - fs.ParseErrorsWhitelist = pflag.ParseErrorsWhitelist{UnknownFlags: true} + fs.ParseErrorsAllowlist = pflag.ParseErrorsAllowlist{UnknownFlags: true} opts := &rootOptions{} diff --git a/vendor/github.com/golangci/golangci-lint/v2/pkg/config/linters_settings.go b/vendor/github.com/golangci/golangci-lint/v2/pkg/config/linters_settings.go index 9394f89b..fefa94ca 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/pkg/config/linters_settings.go +++ b/vendor/github.com/golangci/golangci-lint/v2/pkg/config/linters_settings.go @@ -24,6 +24,9 @@ var defaultLintersSettings = LintersSettings{ Dupl: DuplSettings{ Threshold: 150, }, + EmbeddedStructFieldCheck: EmbeddedStructFieldCheckSettings{ + EmptyLine: true, + }, ErrorLint: ErrorLintSettings{ Errorf: true, ErrorfMulti: true, @@ -128,6 +131,7 @@ var defaultLintersSettings = LintersSettings{ StrConcat: true, BoolFormat: true, HexFormat: true, + ConcatLoop: true, }, Prealloc: PreallocSettings{ Simple: true, @@ -159,6 +163,9 @@ var defaultLintersSettings = LintersSettings{ SkipRegexp: `(export|internal)_test\.go`, AllowPackages: []string{"main"}, }, + Unqueryvet: UnqueryvetSettings{ + CheckSQLBuilders: true, + }, Unused: UnusedSettings{ FieldWritesAreUses: true, PostStatementsAreReads: false, @@ -239,6 +246,7 @@ type LintersSettings struct { Goconst GoConstSettings `mapstructure:"goconst"` Gocritic GoCriticSettings `mapstructure:"gocritic"` Gocyclo GoCycloSettings `mapstructure:"gocyclo"` + Godoclint GodoclintSettings `mapstructure:"godoclint"` Godot GodotSettings `mapstructure:"godot"` Godox GodoxSettings `mapstructure:"godox"` Goheader GoHeaderSettings `mapstructure:"goheader"` @@ -246,12 +254,15 @@ type LintersSettings struct { Gomodguard GoModGuardSettings `mapstructure:"gomodguard"` Gosec GoSecSettings `mapstructure:"gosec"` Gosmopolitan GosmopolitanSettings `mapstructure:"gosmopolitan"` + Unqueryvet UnqueryvetSettings `mapstructure:"unqueryvet"` Govet GovetSettings `mapstructure:"govet"` Grouper GrouperSettings `mapstructure:"grouper"` Iface IfaceSettings `mapstructure:"iface"` ImportAs ImportAsSettings `mapstructure:"importas"` Inamedparam INamedParamSettings `mapstructure:"inamedparam"` + Ineffassign IneffassignSettings `mapstructure:"ineffassign"` InterfaceBloat InterfaceBloatSettings `mapstructure:"interfacebloat"` + IotaMixing IotaMixingSettings `mapstructure:"iotamixing"` Ireturn IreturnSettings `mapstructure:"ireturn"` Lll LllSettings `mapstructure:"lll"` LoggerCheck LoggerCheckSettings `mapstructure:"loggercheck"` @@ -259,6 +270,7 @@ type LintersSettings struct { Makezero MakezeroSettings `mapstructure:"makezero"` Misspell MisspellSettings `mapstructure:"misspell"` Mnd MndSettings `mapstructure:"mnd"` + Modernize ModernizeSettings `mapstructure:"modernize"` MustTag MustTagSettings `mapstructure:"musttag"` Nakedret NakedretSettings `mapstructure:"nakedret"` Nestif NestifSettings `mapstructure:"nestif"` @@ -374,12 +386,14 @@ type DuplSettings struct { } type DupWordSettings struct { - Keywords []string `mapstructure:"keywords"` - Ignore []string `mapstructure:"ignore"` + Keywords []string `mapstructure:"keywords"` + Ignore []string `mapstructure:"ignore"` + CommentsOnly bool `mapstructure:"comments-only"` } type EmbeddedStructFieldCheckSettings struct { ForbidMutex bool `mapstructure:"forbid-mutex"` + EmptyLine bool `mapstructure:"empty-line"` } type ErrcheckSettings struct { @@ -471,6 +485,7 @@ type GinkgoLinterSettings struct { ForbidSpecPollution bool `mapstructure:"forbid-spec-pollution"` ForceSucceedForFuncs bool `mapstructure:"force-succeed"` ForceAssertionDescription bool `mapstructure:"force-assertion-description"` + ForeToNot bool `mapstructure:"force-tonot"` } type GoChecksumTypeSettings struct { @@ -515,6 +530,24 @@ type GoCycloSettings struct { MinComplexity int `mapstructure:"min-complexity"` } +type GodoclintSettings struct { + Default *string `mapstructure:"default"` + Enable []string `mapstructure:"enable"` + Disable []string `mapstructure:"disable"` + Options struct { + MaxLen struct { + Length *uint `mapstructure:"length"` + } `mapstructure:"max-len"` + RequireDoc struct { + IgnoreExported *bool `mapstructure:"ignore-exported"` + IgnoreUnexported *bool `mapstructure:"ignore-unexported"` + } `mapstructure:"require-doc"` + StartWithName struct { + IncludeUnexported *bool `mapstructure:"include-unexported"` + } `mapstructure:"start-with-name"` + } `mapstructure:"options"` +} + type GodotSettings struct { Scope string `mapstructure:"scope"` Exclude []string `mapstructure:"exclude"` @@ -640,10 +673,18 @@ type INamedParamSettings struct { SkipSingleParam bool `mapstructure:"skip-single-param"` } +type IneffassignSettings struct { + CheckEscapingErrors bool `mapstructure:"check-escaping-errors"` +} + type InterfaceBloatSettings struct { Max int `mapstructure:"max"` } +type IotaMixingSettings struct { + ReportIndividual bool `mapstructure:"report-individual"` +} + type IreturnSettings struct { Allow []string `mapstructure:"allow"` Reject []string `mapstructure:"reject"` @@ -720,6 +761,10 @@ type MndSettings struct { IgnoredFunctions []string `mapstructure:"ignored-functions"` } +type ModernizeSettings struct { + Disable []string `mapstructure:"disable"` +} + type NoLintLintSettings struct { RequireExplanation bool `mapstructure:"require-explanation"` RequireSpecific bool `mapstructure:"require-specific"` @@ -751,6 +796,9 @@ type PerfSprintSettings struct { BoolFormat bool `mapstructure:"bool-format"` HexFormat bool `mapstructure:"hex-format"` + + ConcatLoop bool `mapstructure:"concat-loop"` + LoopOtherOps bool `mapstructure:"loop-other-ops"` } type PreallocSettings struct { @@ -786,15 +834,16 @@ type RecvcheckSettings struct { } type ReviveSettings struct { - Go string `mapstructure:"-"` - MaxOpenFiles int `mapstructure:"max-open-files"` - Confidence float64 `mapstructure:"confidence"` - Severity string `mapstructure:"severity"` - EnableAllRules bool `mapstructure:"enable-all-rules"` - Rules []ReviveRule `mapstructure:"rules"` - ErrorCode int `mapstructure:"error-code"` - WarningCode int `mapstructure:"warning-code"` - Directives []ReviveDirective `mapstructure:"directives"` + Go string `mapstructure:"-"` + MaxOpenFiles int `mapstructure:"max-open-files"` + Confidence float64 `mapstructure:"confidence"` + Severity string `mapstructure:"severity"` + EnableAllRules bool `mapstructure:"enable-all-rules"` + EnableDefaultRules bool `mapstructure:"enable-default-rules"` + Rules []ReviveRule `mapstructure:"rules"` + ErrorCode int `mapstructure:"error-code"` + WarningCode int `mapstructure:"warning-code"` + Directives []ReviveDirective `mapstructure:"directives"` } type ReviveRule struct { @@ -971,6 +1020,11 @@ type UnparamSettings struct { CheckExported bool `mapstructure:"check-exported"` } +type UnqueryvetSettings struct { + CheckSQLBuilders bool `mapstructure:"check-sql-builders"` + AllowedPatterns []string `mapstructure:"allowed-patterns"` +} + type UnusedSettings struct { FieldWritesAreUses bool `mapstructure:"field-writes-are-uses"` PostStatementsAreReads bool `mapstructure:"post-statements-are-reads"` diff --git a/vendor/github.com/golangci/golangci-lint/v2/pkg/goanalysis/pkgerrors/extract.go b/vendor/github.com/golangci/golangci-lint/v2/pkg/goanalysis/pkgerrors/extract.go index d1257e66..76a4c902 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/pkg/goanalysis/pkgerrors/extract.go +++ b/vendor/github.com/golangci/golangci-lint/v2/pkg/goanalysis/pkgerrors/extract.go @@ -2,6 +2,7 @@ package pkgerrors import ( "fmt" + "maps" "regexp" "strings" @@ -18,7 +19,9 @@ func extractErrors(pkg *packages.Package) []packages.Error { return errors } + skippedErrors := map[string]packages.Error{} seenErrors := map[string]bool{} + var uniqErrors []packages.Error for _, err := range errors { msg := stackCrusher(err.Error()) @@ -26,15 +29,35 @@ func extractErrors(pkg *packages.Package) []packages.Error { continue } + // This `if` is important to avoid duplicate errors. + // The goal is to keep the most relevant error. if msg != err.Error() { + prev, alreadySkip := skippedErrors[msg] + if !alreadySkip { + skippedErrors[msg] = err + continue + } + + if len(err.Error()) < len(prev.Error()) { + skippedErrors[msg] = err + } + continue } + delete(skippedErrors, msg) + seenErrors[msg] = true uniqErrors = append(uniqErrors, err) } + // In some cases, the error stack doesn't contain the tip error. + // We must keep at least one of the original errors that contain the specific message. + for skippedError := range maps.Values(skippedErrors) { + uniqErrors = append(uniqErrors, skippedError) + } + if len(pkg.GoFiles) != 0 { // errors were extracted from deps and have at least one file in package for i := range uniqErrors { diff --git a/vendor/github.com/golangci/golangci-lint/v2/pkg/goanalysis/runner_checker.go b/vendor/github.com/golangci/golangci-lint/v2/pkg/goanalysis/runner_checker.go index 569002ed..e8fda994 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/pkg/goanalysis/runner_checker.go +++ b/vendor/github.com/golangci/golangci-lint/v2/pkg/goanalysis/runner_checker.go @@ -12,6 +12,7 @@ import ( "errors" "fmt" "go/types" + "os" "reflect" "time" @@ -160,7 +161,7 @@ func (act *action) analyze() { AllObjectFacts: act.AllObjectFacts, AllPackageFacts: act.AllPackageFacts, } - pass.ReadFile = analysisinternal.MakeReadFile(pass) + pass.ReadFile = analysisinternal.CheckedReadFile(pass, os.ReadFile) act.pass = pass act.runner.passToPkgGuard.Lock() diff --git a/vendor/github.com/golangci/golangci-lint/v2/pkg/goanalysis/runner_loadingpackage.go b/vendor/github.com/golangci/golangci-lint/v2/pkg/goanalysis/runner_loadingpackage.go index 29a27089..217803bb 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/pkg/goanalysis/runner_loadingpackage.go +++ b/vendor/github.com/golangci/golangci-lint/v2/pkg/goanalysis/runner_loadingpackage.go @@ -63,21 +63,22 @@ func (lp *loadingPackage) analyzeRecursive(ctx context.Context, cancel context.C } func (lp *loadingPackage) analyze(ctx context.Context, cancel context.CancelFunc, loadMode LoadMode, loadSem chan struct{}) { - loadSem <- struct{}{} - defer func() { - <-loadSem - }() - select { case <-ctx.Done(): return - default: + case loadSem <- struct{}{}: + defer func() { + <-loadSem + }() } // Save memory on unused more fields. defer lp.decUse(loadMode < LoadModeWholeProgram) if err := lp.loadWithFacts(loadMode); err != nil { + // Note: this error is ignored when there is no facts loading (e.g. with 98% of linters). + // But this is not a problem because the errors are added to the package.Errors. + // You through an error, try to add it to actions, but there is no action annnddd it's gone! werr := fmt.Errorf("failed to load package %s: %w", lp.pkg.Name, err) // Don't need to write error to errCh, it will be extracted and reported on another layer. @@ -88,6 +89,10 @@ func (lp *loadingPackage) analyze(ctx context.Context, cancel context.CancelFunc act.Err = werr } + if len(lp.actions) == 0 { + lp.log.Warnf("no action but there is an error: %v", err) + } + return } @@ -239,9 +244,11 @@ func (lp *loadingPackage) loadFromExportData() error { return fmt.Errorf("dependency %q hasn't been loaded yet", path) } } + if pkg.ExportFile == "" { return fmt.Errorf("no export data for %q", pkg.ID) } + f, err := os.Open(pkg.ExportFile) if err != nil { return err @@ -332,13 +339,15 @@ func (lp *loadingPackage) loadImportedPackageWithFacts(loadMode LoadMode) error if srcErr := lp.loadFromSource(loadMode); srcErr != nil { return srcErr } + // Make sure this package can't be imported successfully pkg.Errors = append(pkg.Errors, packages.Error{ Pos: "-", Msg: fmt.Sprintf("could not load export data: %s", err), Kind: packages.ParseError, }) - return fmt.Errorf("could not load export data: %w", err) + + return nil } } diff --git a/vendor/github.com/golangci/golangci-lint/v2/pkg/goformat/runner.go b/vendor/github.com/golangci/golangci-lint/v2/pkg/goformat/runner.go index f8762615..650fb8f5 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/pkg/goformat/runner.go +++ b/vendor/github.com/golangci/golangci-lint/v2/pkg/goformat/runner.go @@ -223,8 +223,14 @@ func NewRunnerOptions(cfg *config.Config, diff, diffColored, stdin bool) (Runner return RunnerOptions{}, fmt.Errorf("get base path: %w", err) } + // Required to be consistent with `RunnerOptions.MatchAnyPattern`. + absBasePath, err := filepath.Abs(basePath) + if err != nil { + return RunnerOptions{}, err + } + opts := RunnerOptions{ - basePath: basePath, + basePath: absBasePath, generated: cfg.Formatters.Exclusions.Generated, diff: diff || diffColored, colors: diffColored, @@ -251,7 +257,12 @@ func (o RunnerOptions) MatchAnyPattern(path string) (bool, error) { return false, nil } - rel, err := filepath.Rel(o.basePath, path) + abs, err := filepath.Abs(path) + if err != nil { + return false, err + } + + rel, err := filepath.Rel(o.basePath, abs) if err != nil { return false, err } @@ -272,7 +283,7 @@ func skipDir(name string) bool { return true default: - return strings.HasPrefix(name, ".") + return strings.HasPrefix(name, ".") && name != "." } } diff --git a/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/asciicheck/asciicheck.go b/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/asciicheck/asciicheck.go index 48727703..6a34b256 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/asciicheck/asciicheck.go +++ b/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/asciicheck/asciicheck.go @@ -1,7 +1,7 @@ package asciicheck import ( - "github.com/tdakkota/asciicheck" + "github.com/golangci/asciicheck" "github.com/golangci/golangci-lint/v2/pkg/goanalysis" ) diff --git a/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/contextcheck/contextcheck.go b/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/contextcheck/contextcheck.go index 88c71d2d..b01df7d9 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/contextcheck/contextcheck.go +++ b/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/contextcheck/contextcheck.go @@ -2,6 +2,8 @@ package contextcheck import ( "github.com/kkHAIKE/contextcheck" + "golang.org/x/tools/go/analysis/passes/ctrlflow" + "golang.org/x/tools/go/analysis/passes/inspect" "github.com/golangci/golangci-lint/v2/pkg/goanalysis" "github.com/golangci/golangci-lint/v2/pkg/lint/linter" @@ -9,6 +11,11 @@ import ( func New() *goanalysis.Linter { analyzer := contextcheck.NewAnalyzer(contextcheck.Configuration{}) + // TODO(ldez) there is a problem with this linter: + // I think the problem related to facts. + // The BuildSSA pass has been changed inside (0.39.0): + // https://github.com/golang/tools/commit/b74c09864920a69a4d2f6ef0ecb4f9cff226893a + analyzer.Requires = append(analyzer.Requires, ctrlflow.Analyzer, inspect.Analyzer) return goanalysis. NewLinterFromAnalyzer(analyzer). diff --git a/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/dupword/dupword.go b/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/dupword/dupword.go index 7b989bc2..0308534e 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/dupword/dupword.go +++ b/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/dupword/dupword.go @@ -14,8 +14,9 @@ func New(settings *config.DupWordSettings) *goanalysis.Linter { if settings != nil { cfg = map[string]any{ - "keyword": strings.Join(settings.Keywords, ","), - "ignore": strings.Join(settings.Ignore, ","), + "keyword": strings.Join(settings.Keywords, ","), + "ignore": strings.Join(settings.Ignore, ","), + "comments-only": settings.CommentsOnly, } } diff --git a/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/embeddedstructfieldcheck/embeddedstructfieldcheck.go b/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/embeddedstructfieldcheck/embeddedstructfieldcheck.go index c9df5038..ba4c06eb 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/embeddedstructfieldcheck/embeddedstructfieldcheck.go +++ b/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/embeddedstructfieldcheck/embeddedstructfieldcheck.go @@ -12,7 +12,8 @@ func New(settings *config.EmbeddedStructFieldCheckSettings) *goanalysis.Linter { if settings != nil { cfg = map[string]any{ - analyzer.ForbidMutexName: settings.ForbidMutex, + analyzer.ForbidMutexCheck: settings.ForbidMutex, + analyzer.EmptyLineCheck: settings.EmptyLine, } } diff --git a/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/ginkgolinter/ginkgolinter.go b/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/ginkgolinter/ginkgolinter.go index 71bb2409..99b9eb47 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/ginkgolinter/ginkgolinter.go +++ b/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/ginkgolinter/ginkgolinter.go @@ -26,6 +26,7 @@ func New(settings *config.GinkgoLinterSettings) *goanalysis.Linter { ForbidSpecPollution: settings.ForbidSpecPollution, ForceSucceedForFuncs: settings.ForceSucceedForFuncs, ForceAssertionDescription: settings.ForceAssertionDescription, + ForeToNot: settings.ForeToNot, } } diff --git a/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/godoclint/godoclint.go b/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/godoclint/godoclint.go new file mode 100644 index 00000000..23590921 --- /dev/null +++ b/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/godoclint/godoclint.go @@ -0,0 +1,109 @@ +package godoclint + +import ( + "errors" + "fmt" + "slices" + + glcompose "github.com/godoc-lint/godoc-lint/pkg/compose" + glconfig "github.com/godoc-lint/godoc-lint/pkg/config" + "github.com/godoc-lint/godoc-lint/pkg/model" + + "github.com/golangci/golangci-lint/v2/pkg/config" + "github.com/golangci/golangci-lint/v2/pkg/goanalysis" + "github.com/golangci/golangci-lint/v2/pkg/golinters/internal" +) + +func New(settings *config.GodoclintSettings) *goanalysis.Linter { + var pcfg glconfig.PlainConfig + + if settings != nil { + err := checkSettings(settings) + if err != nil { + internal.LinterLogger.Fatalf("godoclint: %v", err) + } + + // The following options are explicitly ignored: they must be handled globally with exclusions or nolint directives. + // - Include + // - Exclude + + // The following options are explicitly ignored: these options cannot work as expected because the global configuration about tests. + // - Options.MaxLenIncludeTests + // - Options.PkgDocIncludeTests + // - Options.SinglePkgDocIncludeTests + // - Options.RequirePkgDocIncludeTests + // - Options.RequireDocIncludeTests + // - Options.StartWithNameIncludeTests + // - Options.NoUnusedLinkIncludeTests + + pcfg = glconfig.PlainConfig{ + Default: settings.Default, + Enable: settings.Enable, + Disable: settings.Disable, + Options: &glconfig.PlainRuleOptions{ + MaxLenLength: settings.Options.MaxLen.Length, + MaxLenIncludeTests: pointer(true), + PkgDocIncludeTests: pointer(false), + SinglePkgDocIncludeTests: pointer(true), + RequirePkgDocIncludeTests: pointer(false), + RequireDocIncludeTests: pointer(true), + RequireDocIgnoreExported: settings.Options.RequireDoc.IgnoreExported, + RequireDocIgnoreUnexported: settings.Options.RequireDoc.IgnoreUnexported, + StartWithNameIncludeTests: pointer(false), + StartWithNameIncludeUnexported: settings.Options.StartWithName.IncludeUnexported, + NoUnusedLinkIncludeTests: pointer(true), + }, + } + } + + composition := glcompose.Compose(glcompose.CompositionConfig{ + BaseDirPlainConfig: &pcfg, + ExitFunc: func(_ int, err error) { + internal.LinterLogger.Errorf("godoclint: %v", err) + }, + }) + + return goanalysis. + NewLinterFromAnalyzer(composition.Analyzer.GetAnalyzer()). + WithLoadMode(goanalysis.LoadModeSyntax) +} + +func checkSettings(settings *config.GodoclintSettings) error { + switch deref(settings.Default) { + case string(model.DefaultSetAll): + if len(settings.Enable) > 0 { + return errors.New("cannot use 'enable' with 'default=all'") + } + + case string(model.DefaultSetNone): + if len(settings.Disable) > 0 { + return errors.New("cannot use 'disable' with 'default=none'") + } + + default: + for _, rule := range settings.Enable { + if slices.Contains(settings.Disable, rule) { + return fmt.Errorf("a rule cannot be enabled and disabled at the same time: '%s'", rule) + } + } + + for _, rule := range settings.Disable { + if slices.Contains(settings.Enable, rule) { + return fmt.Errorf("a rule cannot be enabled and disabled at the same time: '%s'", rule) + } + } + } + + return nil +} + +func pointer[T any](v T) *T { return &v } + +func deref[T any](v *T) T { + if v == nil { + var zero T + return zero + } + + return *v +} diff --git a/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/govet/govet.go b/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/govet/govet.go index e4de5b0f..7755e4ec 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/govet/govet.go +++ b/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/govet/govet.go @@ -109,7 +109,7 @@ var ( waitgroup.Analyzer, } - // https://github.com/golang/go/blob/go1.23.0/src/cmd/vet/main.go#L55-L87 + // https://github.com/golang/go/blob/go1.25.2/src/cmd/vet/main.go#L57-L91 defaultAnalyzers = []*analysis.Analyzer{ appends.Analyzer, asmdecl.Analyzer, @@ -124,6 +124,7 @@ var ( directive.Analyzer, errorsas.Analyzer, framepointer.Analyzer, + hostport.Analyzer, httpresponse.Analyzer, ifaceassert.Analyzer, loopclosure.Analyzer, @@ -144,6 +145,7 @@ var ( unreachable.Analyzer, unsafeptr.Analyzer, unusedresult.Analyzer, + waitgroup.Analyzer, } ) diff --git a/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/ineffassign/ineffassign.go b/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/ineffassign/ineffassign.go index 2c0119f1..1df737cb 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/ineffassign/ineffassign.go +++ b/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/ineffassign/ineffassign.go @@ -3,12 +3,21 @@ package ineffassign import ( "github.com/gordonklaus/ineffassign/pkg/ineffassign" + "github.com/golangci/golangci-lint/v2/pkg/config" "github.com/golangci/golangci-lint/v2/pkg/goanalysis" ) -func New() *goanalysis.Linter { +func New(settings *config.IneffassignSettings) *goanalysis.Linter { + var cfg map[string]any + + if settings != nil { + cfg = map[string]any{ + "check-escaping-errors": settings.CheckEscapingErrors, + } + } + return goanalysis. NewLinterFromAnalyzer(ineffassign.Analyzer). - WithDesc("Detects when assignments to existing variables are not used"). + WithConfig(cfg). WithLoadMode(goanalysis.LoadModeSyntax) } diff --git a/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/iotamixing/iotamixing.go b/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/iotamixing/iotamixing.go new file mode 100644 index 00000000..dee0c3c7 --- /dev/null +++ b/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/iotamixing/iotamixing.go @@ -0,0 +1,26 @@ +package iotamixing + +import ( + im "github.com/AdminBenni/iota-mixing/pkg/analyzer" + "github.com/AdminBenni/iota-mixing/pkg/analyzer/flags" + + "github.com/golangci/golangci-lint/v2/pkg/config" + "github.com/golangci/golangci-lint/v2/pkg/goanalysis" +) + +func New(settings *config.IotaMixingSettings) *goanalysis.Linter { + cfg := map[string]any{} + + if settings != nil { + cfg[flags.ReportIndividualFlagName] = settings.ReportIndividual + } + + analyzer := im.GetIotaMixingAnalyzer() + + flags.SetupFlags(&analyzer.Flags) + + return goanalysis. + NewLinterFromAnalyzer(analyzer). + WithConfig(cfg). + WithLoadMode(goanalysis.LoadModeSyntax) +} diff --git a/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/modernize/modernize.go b/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/modernize/modernize.go new file mode 100644 index 00000000..97825c07 --- /dev/null +++ b/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/modernize/modernize.go @@ -0,0 +1,34 @@ +package modernize + +import ( + "slices" + + "github.com/golangci/golangci-lint/v2/pkg/config" + "github.com/golangci/golangci-lint/v2/pkg/goanalysis" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/modernize" +) + +func New(settings *config.ModernizeSettings) *goanalysis.Linter { + var analyzers []*analysis.Analyzer + + if settings == nil { + analyzers = modernize.Suite + } else { + for _, analyzer := range modernize.Suite { + if slices.Contains(settings.Disable, analyzer.Name) { + continue + } + + analyzers = append(analyzers, analyzer) + } + } + + return goanalysis.NewLinter( + "modernize", + "A suite of analyzers that suggest simplifications to Go code, using modern language and library features.", + analyzers, + nil). + WithLoadMode(goanalysis.LoadModeTypesInfo) +} diff --git a/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/perfsprint/perfsprint.go b/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/perfsprint/perfsprint.go index 9684c27c..e06b5c2a 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/perfsprint/perfsprint.go +++ b/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/perfsprint/perfsprint.go @@ -28,6 +28,9 @@ func New(settings *config.PerfSprintSettings) *goanalysis.Linter { cfg["bool-format"] = settings.BoolFormat cfg["hex-format"] = settings.HexFormat + + cfg["concat-loop"] = settings.ConcatLoop + cfg["loop-other-ops"] = settings.LoopOtherOps } return goanalysis. diff --git a/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/revive/revive.go b/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/revive/revive.go index fc80e569..6799e1a4 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/revive/revive.go +++ b/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/revive/revive.go @@ -169,8 +169,8 @@ func (w *wrapper) toIssue(pass *analysis.Pass, failure *lint.Failure) *goanalysi // This function mimics the GetConfig function of revive. // This allows to get default values and right types. // https://github.com/golangci/golangci-lint/issues/1745 -// https://github.com/mgechev/revive/blob/v1.6.0/config/config.go#L230 -// https://github.com/mgechev/revive/blob/v1.6.0/config/config.go#L182-L188 +// https://github.com/mgechev/revive/blob/v1.13.0/config/config.go#L249 +// https://github.com/mgechev/revive/blob/v1.13.0/config/config.go#L198-L204 func getConfig(cfg *config.ReviveSettings) (*lint.Config, error) { conf := defaultConfig() @@ -269,7 +269,7 @@ func safeTomlSlice(r []any) []any { } // This element is not exported by revive, so we need copy the code. -// Extracted from https://github.com/mgechev/revive/blob/v1.11.0/config/config.go#L166 +// Extracted from https://github.com/mgechev/revive/blob/v1.13.0/config/config.go#L16 var defaultRules = []lint.Rule{ &rule.VarDeclarationsRule{}, &rule.PackageCommentsRule{}, @@ -325,14 +325,20 @@ var allRules = append([]lint.Rule{ &rule.FileLengthLimitRule{}, &rule.FilenameFormatRule{}, &rule.FlagParamRule{}, + &rule.ForbiddenCallInWgGoRule{}, &rule.FunctionLength{}, &rule.FunctionResultsLimitRule{}, &rule.GetReturnRule{}, &rule.IdenticalBranchesRule{}, + &rule.IdenticalIfElseIfBranchesRule{}, + &rule.IdenticalIfElseIfConditionsRule{}, + &rule.IdenticalSwitchBranchesRule{}, + &rule.IdenticalSwitchConditionsRule{}, &rule.IfReturnRule{}, &rule.ImportAliasNamingRule{}, &rule.ImportsBlocklistRule{}, &rule.ImportShadowingRule{}, + &rule.InefficientMapLookupRule{}, &rule.LineLengthLimitRule{}, &rule.MaxControlNestingRule{}, &rule.MaxPublicStructsRule{}, @@ -340,6 +346,7 @@ var allRules = append([]lint.Rule{ &rule.ModifiesValRecRule{}, &rule.NestedStructs{}, &rule.OptimizeOperandsOrderRule{}, + &rule.PackageDirectoryMismatchRule{}, &rule.RangeValAddress{}, &rule.RangeValInClosureRule{}, &rule.RedundantBuildTagRule{}, @@ -355,19 +362,23 @@ var allRules = append([]lint.Rule{ &rule.UnexportedNamingRule{}, &rule.UnhandledErrorRule{}, &rule.UnnecessaryFormatRule{}, + &rule.UnnecessaryIfRule{}, &rule.UnnecessaryStmtRule{}, + &rule.UnsecureURLSchemeRule{}, &rule.UnusedReceiverRule{}, &rule.UseAnyRule{}, &rule.UseErrorsNewRule{}, &rule.UseFmtPrintRule{}, &rule.UselessBreak{}, + &rule.UselessFallthroughRule{}, + &rule.UseWaitGroupGoRule{}, &rule.WaitGroupByValueRule{}, }, defaultRules...) const defaultConfidence = 0.8 // This element is not exported by revive, so we need copy the code. -// Extracted from https://github.com/mgechev/revive/blob/v1.11.0/config/config.go#L198 +// Extracted from https://github.com/mgechev/revive/blob/v1.13.0/config/config.go#L209 func normalizeConfig(cfg *lint.Config) { // NOTE(ldez): this custom section for golangci-lint should be kept. // --- @@ -378,19 +389,22 @@ func normalizeConfig(cfg *lint.Config) { if len(cfg.Rules) == 0 { cfg.Rules = map[string]lint.RuleConfig{} } - if cfg.EnableAllRules { - // Add to the configuration all rules not yet present in it - for _, r := range allRules { + + addRules := func(config *lint.Config, rules []lint.Rule) { + for _, r := range rules { ruleName := r.Name() - _, alreadyInConf := cfg.Rules[ruleName] - if alreadyInConf { - continue + if _, ok := config.Rules[ruleName]; !ok { + config.Rules[ruleName] = lint.RuleConfig{} } - // Add the rule with an empty conf for - cfg.Rules[ruleName] = lint.RuleConfig{} } } + if cfg.EnableAllRules { + addRules(cfg, allRules) + } else if cfg.EnableDefaultRules { + addRules(cfg, defaultRules) + } + severity := cfg.Severity if severity != "" { for k, v := range cfg.Rules { @@ -409,7 +423,7 @@ func normalizeConfig(cfg *lint.Config) { } // This element is not exported by revive, so we need copy the code. -// Extracted from https://github.com/mgechev/revive/blob/v1.11.0/config/config.go#L266 +// Extracted from https://github.com/mgechev/revive/blob/v1.13.0/config/config.go#L280 func defaultConfig() *lint.Config { defaultConfig := lint.Config{ Confidence: defaultConfidence, @@ -455,7 +469,7 @@ func extractRulesName(rules []lint.Rule) []string { return names } -// Extracted from https://github.com/mgechev/revive/blob/v1.11.0/formatter/severity.go +// Extracted from https://github.com/mgechev/revive/blob/v1.13.0/formatter/severity.go // Modified to use pointers (related to hugeParam rule). func severity(cfg *lint.Config, failure *lint.Failure) lint.Severity { if cfg, ok := cfg.Rules[failure.RuleName]; ok && cfg.Severity == lint.SeverityError { diff --git a/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/unqueryvet/unqueryvet.go b/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/unqueryvet/unqueryvet.go new file mode 100644 index 00000000..db4a4d75 --- /dev/null +++ b/vendor/github.com/golangci/golangci-lint/v2/pkg/golinters/unqueryvet/unqueryvet.go @@ -0,0 +1,24 @@ +package unqueryvet + +import ( + "github.com/MirrexOne/unqueryvet" + pkgconfig "github.com/MirrexOne/unqueryvet/pkg/config" + + "github.com/golangci/golangci-lint/v2/pkg/config" + "github.com/golangci/golangci-lint/v2/pkg/goanalysis" +) + +func New(settings *config.UnqueryvetSettings) *goanalysis.Linter { + cfg := pkgconfig.DefaultSettings() + + if settings != nil { + cfg.CheckSQLBuilders = settings.CheckSQLBuilders + if len(settings.AllowedPatterns) > 0 { + cfg.AllowedPatterns = settings.AllowedPatterns + } + } + + return goanalysis. + NewLinterFromAnalyzer(unqueryvet.NewWithConfig(&cfg)). + WithLoadMode(goanalysis.LoadModeSyntax) +} diff --git a/vendor/github.com/golangci/golangci-lint/v2/pkg/lint/lintersdb/builder_linter.go b/vendor/github.com/golangci/golangci-lint/v2/pkg/lint/lintersdb/builder_linter.go index 4c954156..6e9cef1d 100644 --- a/vendor/github.com/golangci/golangci-lint/v2/pkg/lint/lintersdb/builder_linter.go +++ b/vendor/github.com/golangci/golangci-lint/v2/pkg/lint/lintersdb/builder_linter.go @@ -43,6 +43,7 @@ import ( "github.com/golangci/golangci-lint/v2/pkg/golinters/goconst" "github.com/golangci/golangci-lint/v2/pkg/golinters/gocritic" "github.com/golangci/golangci-lint/v2/pkg/golinters/gocyclo" + "github.com/golangci/golangci-lint/v2/pkg/golinters/godoclint" "github.com/golangci/golangci-lint/v2/pkg/golinters/godot" "github.com/golangci/golangci-lint/v2/pkg/golinters/godox" "github.com/golangci/golangci-lint/v2/pkg/golinters/gofmt" @@ -63,6 +64,7 @@ import ( "github.com/golangci/golangci-lint/v2/pkg/golinters/ineffassign" "github.com/golangci/golangci-lint/v2/pkg/golinters/interfacebloat" "github.com/golangci/golangci-lint/v2/pkg/golinters/intrange" + "github.com/golangci/golangci-lint/v2/pkg/golinters/iotamixing" "github.com/golangci/golangci-lint/v2/pkg/golinters/ireturn" "github.com/golangci/golangci-lint/v2/pkg/golinters/lll" "github.com/golangci/golangci-lint/v2/pkg/golinters/loggercheck" @@ -71,6 +73,7 @@ import ( "github.com/golangci/golangci-lint/v2/pkg/golinters/mirror" "github.com/golangci/golangci-lint/v2/pkg/golinters/misspell" "github.com/golangci/golangci-lint/v2/pkg/golinters/mnd" + "github.com/golangci/golangci-lint/v2/pkg/golinters/modernize" "github.com/golangci/golangci-lint/v2/pkg/golinters/musttag" "github.com/golangci/golangci-lint/v2/pkg/golinters/nakedret" "github.com/golangci/golangci-lint/v2/pkg/golinters/nestif" @@ -107,6 +110,7 @@ import ( "github.com/golangci/golangci-lint/v2/pkg/golinters/tparallel" "github.com/golangci/golangci-lint/v2/pkg/golinters/unconvert" "github.com/golangci/golangci-lint/v2/pkg/golinters/unparam" + "github.com/golangci/golangci-lint/v2/pkg/golinters/unqueryvet" "github.com/golangci/golangci-lint/v2/pkg/golinters/unused" "github.com/golangci/golangci-lint/v2/pkg/golinters/usestdlibvars" "github.com/golangci/golangci-lint/v2/pkg/golinters/usetesting" @@ -151,7 +155,7 @@ func (LinterBuilder) Build(cfg *config.Config) ([]*linter.Config, error) { linter.NewConfig(asciicheck.New()). WithSince("v1.26.0"). - WithURL("https://github.com/tdakkota/asciicheck"), + WithURL("https://github.com/golangci/asciicheck"), linter.NewConfig(bidichk.New(&cfg.Linters.Settings.BiDiChk)). WithSince("v1.43.0"). @@ -331,6 +335,10 @@ func (LinterBuilder) Build(cfg *config.Config) ([]*linter.Config, error) { WithSince("v1.0.0"). WithURL("https://github.com/fzipp/gocyclo"), + linter.NewConfig(godoclint.New(&cfg.Linters.Settings.Godoclint)). + WithSince("v2.5.0"). + WithURL("https://github.com/godoc-lint/godoc-lint"), + linter.NewConfig(godot.New(&cfg.Linters.Settings.Godot)). WithSince("v1.25.0"). WithAutoFix(). @@ -375,6 +383,11 @@ func (LinterBuilder) Build(cfg *config.Config) ([]*linter.Config, error) { WithSince("v1.22.0"). WithURL("https://github.com/tommy-muehle/go-mnd"), + linter.NewConfig(modernize.New(&cfg.Linters.Settings.Modernize)). + WithSince("v2.6.0"). + WithLoadForGoAnalysis(). + WithURL("https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize"), + linter.NewConfig(gomoddirectives.New(&cfg.Linters.Settings.GoModDirectives)). WithSince("v1.39.0"). WithURL("https://github.com/ldez/gomoddirectives"), @@ -424,7 +437,7 @@ func (LinterBuilder) Build(cfg *config.Config) ([]*linter.Config, error) { WithSince("v1.55.0"). WithURL("https://github.com/macabu/inamedparam"), - linter.NewConfig(ineffassign.New()). + linter.NewConfig(ineffassign.New(&cfg.Linters.Settings.Ineffassign)). WithGroups(config.GroupStandard). WithSince("v1.0.0"). WithURL("https://github.com/gordonklaus/ineffassign"), @@ -440,6 +453,10 @@ func (LinterBuilder) Build(cfg *config.Config) ([]*linter.Config, error) { WithURL("https://github.com/ckaznocha/intrange"). WithNoopFallback(cfg, linter.IsGoLowerThanGo122()), + linter.NewConfig(iotamixing.New(&cfg.Linters.Settings.IotaMixing)). + WithSince("v2.5.0"). + WithURL("https://github.com/AdminBenni/iota-mixing"), + linter.NewConfig(ireturn.New(&cfg.Linters.Settings.Ireturn)). WithSince("v1.43.0"). WithLoadForGoAnalysis(). @@ -597,7 +614,7 @@ func (LinterBuilder) Build(cfg *config.Config) ([]*linter.Config, error) { WithSince("v1.0.0"). WithLoadForGoAnalysis(). WithAutoFix(). - WithURL("https://staticcheck.dev/"), + WithURL("https://github.com/dominikh/go-tools"), linter.NewConfig(swaggo.New()). WithSince("v2.2.0"). @@ -652,6 +669,10 @@ func (LinterBuilder) Build(cfg *config.Config) ([]*linter.Config, error) { WithLoadForGoAnalysis(). WithURL("https://github.com/mvdan/unparam"), + linter.NewConfig(unqueryvet.New(&cfg.Linters.Settings.Unqueryvet)). + WithSince("v2.5.0"). + WithURL("https://github.com/MirrexOne/unqueryvet"), + linter.NewConfig(unused.New(&cfg.Linters.Settings.Unused)). WithGroups(config.GroupStandard). WithSince("v1.20.0"). diff --git a/vendor/github.com/manuelarte/embeddedstructfieldcheck/analyzer/analyzer.go b/vendor/github.com/manuelarte/embeddedstructfieldcheck/analyzer/analyzer.go index d1b56c47..dc137d71 100644 --- a/vendor/github.com/manuelarte/embeddedstructfieldcheck/analyzer/analyzer.go +++ b/vendor/github.com/manuelarte/embeddedstructfieldcheck/analyzer/analyzer.go @@ -10,10 +10,16 @@ import ( "github.com/manuelarte/embeddedstructfieldcheck/internal" ) -const ForbidMutexName = "forbid-mutex" +const ( + EmptyLineCheck = "empty-line" + ForbidMutexCheck = "forbid-mutex" +) func NewAnalyzer() *analysis.Analyzer { - var forbidMutex bool + var ( + emptyLine bool + forbidMutex bool + ) a := &analysis.Analyzer{ Name: "embeddedstructfieldcheck", @@ -21,7 +27,7 @@ func NewAnalyzer() *analysis.Analyzer { "and there must be an empty line separating embedded fields from regular fields.", URL: "https://github.com/manuelarte/embeddedstructfieldcheck", Run: func(pass *analysis.Pass) (any, error) { - run(pass, forbidMutex) + run(pass, emptyLine, forbidMutex) //nolint:nilnil // impossible case. return nil, nil @@ -29,12 +35,15 @@ func NewAnalyzer() *analysis.Analyzer { Requires: []*analysis.Analyzer{inspect.Analyzer}, } - a.Flags.BoolVar(&forbidMutex, ForbidMutexName, false, "Checks that sync.Mutex is not used as an embedded field.") + a.Flags.BoolVar(&emptyLine, EmptyLineCheck, true, + "Checks that there is an empty space between the embedded fields and regular fields.") + a.Flags.BoolVar(&forbidMutex, ForbidMutexCheck, false, + "Checks that sync.Mutex and sync.RWMutex are not used as an embedded fields.") return a } -func run(pass *analysis.Pass, forbidMutex bool) { +func run(pass *analysis.Pass, emptyLine, forbidMutex bool) { insp, found := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) if !found { return @@ -50,6 +59,6 @@ func run(pass *analysis.Pass, forbidMutex bool) { return } - internal.Analyze(pass, node, forbidMutex) + internal.Analyze(pass, node, emptyLine, forbidMutex) }) } diff --git a/vendor/github.com/manuelarte/embeddedstructfieldcheck/internal/structanalyzer.go b/vendor/github.com/manuelarte/embeddedstructfieldcheck/internal/structanalyzer.go index 834da5de..2df53692 100644 --- a/vendor/github.com/manuelarte/embeddedstructfieldcheck/internal/structanalyzer.go +++ b/vendor/github.com/manuelarte/embeddedstructfieldcheck/internal/structanalyzer.go @@ -6,7 +6,7 @@ import ( "golang.org/x/tools/go/analysis" ) -func Analyze(pass *analysis.Pass, st *ast.StructType, forbidMutex bool) { +func Analyze(pass *analysis.Pass, st *ast.StructType, emptyLine, forbidMutex bool) { var firstEmbeddedField *ast.Field var lastEmbeddedField *ast.Field @@ -34,7 +34,9 @@ func Analyze(pass *analysis.Pass, st *ast.StructType, forbidMutex bool) { } } - checkMissingSpace(pass, lastEmbeddedField, firstNotEmbeddedField) + if emptyLine { + checkMissingSpace(pass, lastEmbeddedField, firstNotEmbeddedField) + } } func checkForbiddenEmbeddedField(pass *analysis.Pass, field *ast.Field, forbidMutex bool) { diff --git a/vendor/github.com/tdakkota/asciicheck/.gitignore b/vendor/github.com/tdakkota/asciicheck/.gitignore deleted file mode 100644 index dfa562d3..00000000 --- a/vendor/github.com/tdakkota/asciicheck/.gitignore +++ /dev/null @@ -1,32 +0,0 @@ -# IntelliJ project files -.idea -*.iml -out -gen - -# Go template -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ -.idea/$CACHE_FILE$ -.idea/$PRODUCT_WORKSPACE_FILE$ -.idea/.gitignore -.idea/codeStyles -.idea/deployment.xml -.idea/inspectionProfiles/ -.idea/kotlinc.xml -.idea/misc.xml -.idea/modules.xml -asciicheck.iml diff --git a/vendor/golang.org/x/tools/go/analysis/passes/modernize/any.go b/vendor/golang.org/x/tools/go/analysis/passes/modernize/any.go new file mode 100644 index 00000000..579ab865 --- /dev/null +++ b/vendor/golang.org/x/tools/go/analysis/passes/modernize/any.go @@ -0,0 +1,53 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modernize + +import ( + "go/ast" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/internal/analysis/analyzerutil" + "golang.org/x/tools/internal/versions" +) + +var AnyAnalyzer = &analysis.Analyzer{ + Name: "any", + Doc: analyzerutil.MustExtractDoc(doc, "any"), + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: runAny, + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any", +} + +// The any pass replaces interface{} with go1.18's 'any'. +func runAny(pass *analysis.Pass) (any, error) { + for curFile := range filesUsingGoVersion(pass, versions.Go1_18) { + for curIface := range curFile.Preorder((*ast.InterfaceType)(nil)) { + iface := curIface.Node().(*ast.InterfaceType) + + if iface.Methods.NumFields() == 0 { + // Check that 'any' is not shadowed. + if lookup(pass.TypesInfo, curIface, "any") == builtinAny { + pass.Report(analysis.Diagnostic{ + Pos: iface.Pos(), + End: iface.End(), + Message: "interface{} can be replaced by any", + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Replace interface{} by any", + TextEdits: []analysis.TextEdit{ + { + Pos: iface.Pos(), + End: iface.End(), + NewText: []byte("any"), + }, + }, + }}, + }) + } + } + } + } + return nil, nil +} diff --git a/vendor/golang.org/x/tools/go/analysis/passes/modernize/bloop.go b/vendor/golang.org/x/tools/go/analysis/passes/modernize/bloop.go new file mode 100644 index 00000000..ad45d744 --- /dev/null +++ b/vendor/golang.org/x/tools/go/analysis/passes/modernize/bloop.go @@ -0,0 +1,244 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modernize + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/analysis/analyzerutil" + typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex" + "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/moreiters" + "golang.org/x/tools/internal/typesinternal" + "golang.org/x/tools/internal/typesinternal/typeindex" + "golang.org/x/tools/internal/versions" +) + +var BLoopAnalyzer = &analysis.Analyzer{ + Name: "bloop", + Doc: analyzerutil.MustExtractDoc(doc, "bloop"), + Requires: []*analysis.Analyzer{ + inspect.Analyzer, + typeindexanalyzer.Analyzer, + }, + Run: bloop, + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#bloop", +} + +// bloop updates benchmarks that use "for range b.N", replacing it +// with go1.24's b.Loop() and eliminating any preceding +// b.{Start,Stop,Reset}Timer calls. +// +// Variants: +// +// for i := 0; i < b.N; i++ {} => for b.Loop() {} +// for range b.N {} +func bloop(pass *analysis.Pass) (any, error) { + if !typesinternal.Imports(pass.Pkg, "testing") { + return nil, nil + } + + var ( + index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) + info = pass.TypesInfo + ) + + // edits computes the text edits for a matched for/range loop + // at the specified cursor. b is the *testing.B value, and + // (start, end) is the portion using b.N to delete. + edits := func(curLoop inspector.Cursor, b ast.Expr, start, end token.Pos) (edits []analysis.TextEdit) { + curFn, _ := enclosingFunc(curLoop) + // Within the same function, delete all calls to + // b.{Start,Stop,Timer} that precede the loop. + filter := []ast.Node{(*ast.ExprStmt)(nil), (*ast.FuncLit)(nil)} + curFn.Inspect(filter, func(cur inspector.Cursor) (descend bool) { + node := cur.Node() + if is[*ast.FuncLit](node) { + return false // don't descend into FuncLits (e.g. sub-benchmarks) + } + stmt := node.(*ast.ExprStmt) + if stmt.Pos() > start { + return false // not preceding: stop + } + if call, ok := stmt.X.(*ast.CallExpr); ok { + obj := typeutil.Callee(info, call) + if typesinternal.IsMethodNamed(obj, "testing", "B", "StopTimer", "StartTimer", "ResetTimer") { + // Delete call statement. + // TODO(adonovan): delete following newline, or + // up to start of next stmt? (May delete a comment.) + edits = append(edits, analysis.TextEdit{ + Pos: stmt.Pos(), + End: stmt.End(), + }) + } + } + return true + }) + + // Replace ...b.N... with b.Loop(). + return append(edits, analysis.TextEdit{ + Pos: start, + End: end, + NewText: fmt.Appendf(nil, "%s.Loop()", astutil.Format(pass.Fset, b)), + }) + } + + // Find all for/range statements. + loops := []ast.Node{ + (*ast.ForStmt)(nil), + (*ast.RangeStmt)(nil), + } + for curFile := range filesUsingGoVersion(pass, versions.Go1_24) { + for curLoop := range curFile.Preorder(loops...) { + switch n := curLoop.Node().(type) { + case *ast.ForStmt: + // for _; i < b.N; _ {} + if cmp, ok := n.Cond.(*ast.BinaryExpr); ok && cmp.Op == token.LSS { + if sel, ok := cmp.Y.(*ast.SelectorExpr); ok && + sel.Sel.Name == "N" && + typesinternal.IsPointerToNamed(info.TypeOf(sel.X), "testing", "B") && usesBenchmarkNOnce(curLoop, info) { + + delStart, delEnd := n.Cond.Pos(), n.Cond.End() + + // Eliminate variable i if no longer needed: + // for i := 0; i < b.N; i++ { + // ...no references to i... + // } + body, _ := curLoop.LastChild() + if v := isIncrementLoop(info, n); v != nil && + !uses(index, body, v) { + delStart, delEnd = n.Init.Pos(), n.Post.End() + } + + pass.Report(analysis.Diagnostic{ + // Highlight "i < b.N". + Pos: n.Cond.Pos(), + End: n.Cond.End(), + Message: "b.N can be modernized using b.Loop()", + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Replace b.N with b.Loop()", + TextEdits: edits(curLoop, sel.X, delStart, delEnd), + }}, + }) + } + } + + case *ast.RangeStmt: + // for range b.N {} -> for b.Loop() {} + // + // TODO(adonovan): handle "for i := range b.N". + if sel, ok := n.X.(*ast.SelectorExpr); ok && + n.Key == nil && + n.Value == nil && + sel.Sel.Name == "N" && + typesinternal.IsPointerToNamed(info.TypeOf(sel.X), "testing", "B") && usesBenchmarkNOnce(curLoop, info) { + + pass.Report(analysis.Diagnostic{ + // Highlight "range b.N". + Pos: n.Range, + End: n.X.End(), + Message: "b.N can be modernized using b.Loop()", + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Replace b.N with b.Loop()", + TextEdits: edits(curLoop, sel.X, n.Range, n.X.End()), + }}, + }) + } + } + } + } + return nil, nil +} + +// uses reports whether the subtree cur contains a use of obj. +func uses(index *typeindex.Index, cur inspector.Cursor, obj types.Object) bool { + for use := range index.Uses(obj) { + if cur.Contains(use) { + return true + } + } + return false +} + +// enclosingFunc returns the cursor for the innermost Func{Decl,Lit} +// that encloses c, if any. +func enclosingFunc(c inspector.Cursor) (inspector.Cursor, bool) { + return moreiters.First(c.Enclosing((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil))) +} + +// usesBenchmarkNOnce reports whether a b.N loop should be modernized to b.Loop(). +// Only modernize loops that are: +// 1. Directly in a benchmark function (not in nested functions) +// - b.Loop() must be called in the same goroutine as the benchmark function +// - Function literals are often used with goroutines (go func(){...}) +// +// 2. The only b.N loop in that benchmark function +// - b.Loop() can only be called once per benchmark execution +// - Multiple calls result in "B.Loop called with timer stopped" error +// - Multiple loops may have complex interdependencies that are hard to analyze +func usesBenchmarkNOnce(c inspector.Cursor, info *types.Info) bool { + // Find the enclosing benchmark function + curFunc, ok := enclosingFunc(c) + if !ok { + return false + } + + // Check if this is actually a benchmark function + fdecl, ok := curFunc.Node().(*ast.FuncDecl) + if !ok { + return false // not in a function; or, inside a FuncLit + } + if !isBenchmarkFunc(fdecl) { + return false + } + + // Count all b.N references in this benchmark function (including nested functions) + bnRefCount := 0 + filter := []ast.Node{(*ast.SelectorExpr)(nil)} + curFunc.Inspect(filter, func(cur inspector.Cursor) bool { + sel := cur.Node().(*ast.SelectorExpr) + if sel.Sel.Name == "N" && + typesinternal.IsPointerToNamed(info.TypeOf(sel.X), "testing", "B") { + bnRefCount++ + } + return true + }) + + // Only modernize if there's exactly one b.N reference + return bnRefCount == 1 +} + +// isBenchmarkFunc reports whether f is a benchmark function. +func isBenchmarkFunc(f *ast.FuncDecl) bool { + return f.Recv == nil && + f.Name != nil && + f.Name.IsExported() && + strings.HasPrefix(f.Name.Name, "Benchmark") && + f.Type.Params != nil && + len(f.Type.Params.List) == 1 +} + +// isIncrementLoop reports whether loop has the form "for i := 0; ...; i++ { ... }", +// and if so, it returns the symbol for the index variable. +func isIncrementLoop(info *types.Info, loop *ast.ForStmt) *types.Var { + if assign, ok := loop.Init.(*ast.AssignStmt); ok && + assign.Tok == token.DEFINE && + len(assign.Rhs) == 1 && + isZeroIntConst(info, assign.Rhs[0]) && + is[*ast.IncDecStmt](loop.Post) && + loop.Post.(*ast.IncDecStmt).Tok == token.INC && + astutil.EqualSyntax(loop.Post.(*ast.IncDecStmt).X, assign.Lhs[0]) { + return info.Defs[assign.Lhs[0].(*ast.Ident)].(*types.Var) + } + return nil +} diff --git a/vendor/golang.org/x/tools/go/analysis/passes/modernize/doc.go b/vendor/golang.org/x/tools/go/analysis/passes/modernize/doc.go new file mode 100644 index 00000000..7469002f --- /dev/null +++ b/vendor/golang.org/x/tools/go/analysis/passes/modernize/doc.go @@ -0,0 +1,497 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package modernize provides a suite of analyzers that suggest +simplifications to Go code, using modern language and library +features. + +Each diagnostic provides a fix. Our intent is that these fixes may +be safely applied en masse without changing the behavior of your +program. In some cases the suggested fixes are imperfect and may +lead to (for example) unused imports or unused local variables, +causing build breakage. However, these problems are generally +trivial to fix. We regard any modernizer whose fix changes program +behavior to have a serious bug and will endeavor to fix it. + +To apply all modernization fixes en masse, you can use the +following command: + + $ go run golang.org/x/tools/go/analysis/passes/modernize/cmd/modernize@latest -fix ./... + +(Do not use "go get -tool" to add gopls as a dependency of your +module; gopls commands must be built from their release branch.) + +If the tool warns of conflicting fixes, you may need to run it more +than once until it has applied all fixes cleanly. This command is +not an officially supported interface and may change in the future. + +Changes produced by this tool should be reviewed as usual before +being merged. In some cases, a loop may be replaced by a simple +function call, causing comments within the loop to be discarded. +Human judgment may be required to avoid losing comments of value. + +The modernize suite contains many analyzers. Diagnostics from some, +such as "any" (which replaces "interface{}" with "any" where it +is safe to do so), are particularly numerous. It may ease the burden of +code review to apply fixes in two steps, the first consisting only of +fixes from the "any" analyzer, the second consisting of all +other analyzers. This can be achieved using flags, as in this example: + + $ modernize -any=true -fix ./... + $ modernize -any=false -fix ./... + +# Analyzer appendclipped + +appendclipped: simplify append chains using slices.Concat + +The appendclipped analyzer suggests replacing chains of append calls with a +single call to slices.Concat, which was added in Go 1.21. For example, +append(append(s, s1...), s2...) would be simplified to slices.Concat(s, s1, s2). + +In the simple case of appending to a newly allocated slice, such as +append([]T(nil), s...), the analyzer suggests the more concise slices.Clone(s). +For byte slices, it will prefer bytes.Clone if the "bytes" package is +already imported. + +This fix is only applied when the base of the append tower is a +"clipped" slice, meaning its length and capacity are equal (e.g. +x[:0:0] or []T{}). This is to avoid changing program behavior by +eliminating intended side effects on the base slice's underlying +array. + +This analyzer is currently disabled by default as the +transformation does not preserve the nilness of the base slice in +all cases; see https://go.dev/issue/73557. + +# Analyzer bloop + +bloop: replace for-range over b.N with b.Loop + +The bloop analyzer suggests replacing benchmark loops of the form +`for i := 0; i < b.N; i++` or `for range b.N` with the more modern +`for b.Loop()`, which was added in Go 1.24. + +This change makes benchmark code more readable and also removes the need for +manual timer control, so any preceding calls to b.StartTimer, b.StopTimer, +or b.ResetTimer within the same function will also be removed. + +Caveats: The b.Loop() method is designed to prevent the compiler from +optimizing away the benchmark loop, which can occasionally result in +slower execution due to increased allocations in some specific cases. + +# Analyzer any + +any: replace interface{} with any + +The any analyzer suggests replacing uses of the empty interface type, +`interface{}`, with the `any` alias, which was introduced in Go 1.18. +This is a purely stylistic change that makes code more readable. + +# Analyzer errorsastype + +errorsastype: replace errors.As with errors.AsType[T] + +This analyzer suggests fixes to simplify uses of [errors.As] of +this form: + + var myerr *MyErr + if errors.As(err, &myerr) { + handle(myerr) + } + +by using the less error-prone generic [errors.AsType] function, +introduced in Go 1.26: + + if myerr, ok := errors.AsType[*MyErr](err); ok { + handle(myerr) + } + +The fix is only offered if the var declaration has the form shown and +there are no uses of myerr outside the if statement. + +# Analyzer fmtappendf + +fmtappendf: replace []byte(fmt.Sprintf) with fmt.Appendf + +The fmtappendf analyzer suggests replacing `[]byte(fmt.Sprintf(...))` with +`fmt.Appendf(nil, ...)`. This avoids the intermediate allocation of a string +by Sprintf, making the code more efficient. The suggestion also applies to +fmt.Sprint and fmt.Sprintln. + +# Analyzer forvar + +forvar: remove redundant re-declaration of loop variables + +The forvar analyzer removes unnecessary shadowing of loop variables. +Before Go 1.22, it was common to write `for _, x := range s { x := x ... }` +to create a fresh variable for each iteration. Go 1.22 changed the semantics +of `for` loops, making this pattern redundant. This analyzer removes the +unnecessary `x := x` statement. + +This fix only applies to `range` loops. + +# Analyzer mapsloop + +mapsloop: replace explicit loops over maps with calls to maps package + +The mapsloop analyzer replaces loops of the form + + for k, v := range x { m[k] = v } + +with a single call to a function from the `maps` package, added in Go 1.23. +Depending on the context, this could be `maps.Copy`, `maps.Insert`, +`maps.Clone`, or `maps.Collect`. + +The transformation to `maps.Clone` is applied conservatively, as it +preserves the nilness of the source map, which may be a subtle change in +behavior if the original code did not handle a nil map in the same way. + +# Analyzer minmax + +minmax: replace if/else statements with calls to min or max + +The minmax analyzer simplifies conditional assignments by suggesting the use +of the built-in `min` and `max` functions, introduced in Go 1.21. For example, + + if a < b { x = a } else { x = b } + +is replaced by + + x = min(a, b). + +This analyzer avoids making suggestions for floating-point types, +as the behavior of `min` and `max` with NaN values can differ from +the original if/else statement. + +# Analyzer newexpr + +newexpr: simplify code by using go1.26's new(expr) + +This analyzer finds declarations of functions of this form: + + func varOf(x int) *int { return &x } + +and suggests a fix to turn them into inlinable wrappers around +go1.26's built-in new(expr) function: + + //go:fix inline + func varOf(x int) *int { return new(x) } + +(The directive comment causes the 'inline' analyzer to suggest +that calls to such functions are inlined.) + +In addition, this analyzer suggests a fix for each call +to one of the functions before it is transformed, so that + + use(varOf(123)) + +is replaced by: + + use(new(123)) + +Wrapper functions such as varOf are common when working with Go +serialization packages such as for JSON or protobuf, where pointers +are often used to express optionality. + +# Analyzer omitzero + +omitzero: suggest replacing omitempty with omitzero for struct fields + +The omitzero analyzer identifies uses of the `omitempty` JSON struct tag on +fields that are themselves structs. The `omitempty` tag has no effect on +struct-typed fields. The analyzer offers two suggestions: either remove the +tag, or replace it with `omitzero` (added in Go 1.24), which correctly +omits the field if the struct value is zero. + +Replacing `omitempty` with `omitzero` is a change in behavior. The +original code would always encode the struct field, whereas the +modified code will omit it if it is a zero-value. + +# Analyzer plusbuild + +plusbuild: remove obsolete //+build comments + +The plusbuild analyzer suggests a fix to remove obsolete build tags +of the form: + + //+build linux,amd64 + +in files that also contain a Go 1.18-style tag such as: + + //go:build linux && amd64 + +(It does not check that the old and new tags are consistent; +that is the job of the 'buildtag' analyzer in the vet suite.) + +# Analyzer rangeint + +rangeint: replace 3-clause for loops with for-range over integers + +The rangeint analyzer suggests replacing traditional for loops such +as + + for i := 0; i < n; i++ { ... } + +with the more idiomatic Go 1.22 style: + + for i := range n { ... } + +This transformation is applied only if (a) the loop variable is not +modified within the loop body and (b) the loop's limit expression +is not modified within the loop, as `for range` evaluates its +operand only once. + +# Analyzer reflecttypefor + +reflecttypefor: replace reflect.TypeOf(x) with TypeFor[T]() + +This analyzer suggests fixes to replace uses of reflect.TypeOf(x) with +reflect.TypeFor, introduced in go1.22, when the desired runtime type +is known at compile time, for example: + + reflect.TypeOf(uint32(0)) -> reflect.TypeFor[uint32]() + reflect.TypeOf((*ast.File)(nil)) -> reflect.TypeFor[*ast.File]() + +It also offers a fix to simplify the construction below, which uses +reflect.TypeOf to return the runtime type for an interface type, + + reflect.TypeOf((*io.Reader)(nil)).Elem() + +to: + + reflect.TypeFor[io.Reader]() + +No fix is offered in cases when the runtime type is dynamic, such as: + + var r io.Reader = ... + reflect.TypeOf(r) + +or when the operand has potential side effects. + +# Analyzer slicescontains + +slicescontains: replace loops with slices.Contains or slices.ContainsFunc + +The slicescontains analyzer simplifies loops that check for the existence of +an element in a slice. It replaces them with calls to `slices.Contains` or +`slices.ContainsFunc`, which were added in Go 1.21. + +If the expression for the target element has side effects, this +transformation will cause those effects to occur only once, not +once per tested slice element. + +# Analyzer slicesdelete + +slicesdelete: replace append-based slice deletion with slices.Delete + +The slicesdelete analyzer suggests replacing the idiom + + s = append(s[:i], s[j:]...) + +with the more explicit + + s = slices.Delete(s, i, j) + +introduced in Go 1.21. + +This analyzer is disabled by default. The `slices.Delete` function +zeros the elements between the new length and the old length of the +slice to prevent memory leaks, which is a subtle difference in +behavior compared to the append-based idiom; see https://go.dev/issue/73686. + +# Analyzer slicessort + +slicessort: replace sort.Slice with slices.Sort for basic types + +The slicessort analyzer simplifies sorting slices of basic ordered +types. It replaces + + sort.Slice(s, func(i, j int) bool { return s[i] < s[j] }) + +with the simpler `slices.Sort(s)`, which was added in Go 1.21. + +# Analyzer stditerators + +stditerators: use iterators instead of Len/At-style APIs + +This analyzer suggests a fix to replace each loop of the form: + + for i := 0; i < x.Len(); i++ { + use(x.At(i)) + } + +or its "for elem := range x.Len()" equivalent by a range loop over an +iterator offered by the same data type: + + for elem := range x.All() { + use(x.At(i) + } + +where x is one of various well-known types in the standard library. + +# Analyzer stringscut + +stringscut: replace strings.Index etc. with strings.Cut + +This analyzer replaces certain patterns of use of [strings.Index] and string slicing by [strings.Cut], added in go1.18. + +For example: + + idx := strings.Index(s, substr) + if idx >= 0 { + return s[:idx] + } + +is replaced by: + + before, _, ok := strings.Cut(s, substr) + if ok { + return before + } + +And: + + idx := strings.Index(s, substr) + if idx >= 0 { + return + } + +is replaced by: + + found := strings.Contains(s, substr) + if found { + return + } + +It also handles variants using [strings.IndexByte] instead of Index, or the bytes package instead of strings. + +Fixes are offered only in cases in which there are no potential modifications of the idx, s, or substr expressions between their definition and use. + +# Analyzer stringscutprefix + +stringscutprefix: replace HasPrefix/TrimPrefix with CutPrefix + +The stringscutprefix analyzer simplifies a common pattern where code first +checks for a prefix with `strings.HasPrefix` and then removes it with +`strings.TrimPrefix`. It replaces this two-step process with a single call +to `strings.CutPrefix`, introduced in Go 1.20. The analyzer also handles +the equivalent functions in the `bytes` package. + +For example, this input: + + if strings.HasPrefix(s, prefix) { + use(strings.TrimPrefix(s, prefix)) + } + +is fixed to: + + if after, ok := strings.CutPrefix(s, prefix); ok { + use(after) + } + +The analyzer also offers fixes to use CutSuffix in a similar way. +This input: + + if strings.HasSuffix(s, suffix) { + use(strings.TrimSuffix(s, suffix)) + } + +is fixed to: + + if before, ok := strings.CutSuffix(s, suffix); ok { + use(before) + } + +# Analyzer stringsseq + +stringsseq: replace ranging over Split/Fields with SplitSeq/FieldsSeq + +The stringsseq analyzer improves the efficiency of iterating over substrings. +It replaces + + for range strings.Split(...) + +with the more efficient + + for range strings.SplitSeq(...) + +which was added in Go 1.24 and avoids allocating a slice for the +substrings. The analyzer also handles strings.Fields and the +equivalent functions in the bytes package. + +# Analyzer stringsbuilder + +stringsbuilder: replace += with strings.Builder + +This analyzer replaces repeated string += string concatenation +operations with calls to Go 1.10's strings.Builder. + +For example: + + var s = "[" + for x := range seq { + s += x + s += "." + } + s += "]" + use(s) + +is replaced by: + + var s strings.Builder + s.WriteString("[") + for x := range seq { + s.WriteString(x) + s.WriteString(".") + } + s.WriteString("]") + use(s.String()) + +This avoids quadratic memory allocation and improves performance. + +The analyzer requires that all references to s except the final one +are += operations. To avoid warning about trivial cases, at least one +must appear within a loop. The variable s must be a local +variable, not a global or parameter. + +The sole use of the finished string must be the last reference to the +variable s. (It may appear within an intervening loop or function literal, +since even s.String() is called repeatedly, it does not allocate memory.) + +# Analyzer testingcontext + +testingcontext: replace context.WithCancel with t.Context in tests + +The testingcontext analyzer simplifies context management in tests. It +replaces the manual creation of a cancellable context, + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + +with a single call to t.Context(), which was added in Go 1.24. + +This change is only suggested if the `cancel` function is not used +for any other purpose. + +# Analyzer waitgroup + +waitgroup: replace wg.Add(1)/go/wg.Done() with wg.Go + +The waitgroup analyzer simplifies goroutine management with `sync.WaitGroup`. +It replaces the common pattern + + wg.Add(1) + go func() { + defer wg.Done() + ... + }() + +with a single call to + + wg.Go(func(){ ... }) + +which was added in Go 1.25. +*/ +package modernize diff --git a/vendor/golang.org/x/tools/go/analysis/passes/modernize/errorsastype.go b/vendor/golang.org/x/tools/go/analysis/passes/modernize/errorsastype.go new file mode 100644 index 00000000..d9a922f8 --- /dev/null +++ b/vendor/golang.org/x/tools/go/analysis/passes/modernize/errorsastype.go @@ -0,0 +1,243 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modernize + +import ( + "go/ast" + "go/token" + "go/types" + + "fmt" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/ast/edge" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/analysis/analyzerutil" + typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex" + "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/goplsexport" + "golang.org/x/tools/internal/refactor" + "golang.org/x/tools/internal/typesinternal" + "golang.org/x/tools/internal/typesinternal/typeindex" + "golang.org/x/tools/internal/versions" +) + +var errorsastypeAnalyzer = &analysis.Analyzer{ + Name: "errorsastype", + Doc: analyzerutil.MustExtractDoc(doc, "errorsastype"), + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#errorsastype", + Requires: []*analysis.Analyzer{typeindexanalyzer.Analyzer}, + Run: errorsastype, +} + +func init() { + // Export to gopls until this is a published modernizer. + goplsexport.ErrorsAsTypeModernizer = errorsastypeAnalyzer +} + +// errorsastype offers a fix to replace error.As with the newer +// errors.AsType[T] following this pattern: +// +// var myerr *MyErr +// if errors.As(err, &myerr) { ... } +// +// => +// +// if myerr, ok := errors.AsType[*MyErr](err); ok { ... } +// +// (In principle several of these can then be chained using if/else, +// but we don't attempt that.) +// +// We offer the fix only within an if statement, but not within a +// switch case such as: +// +// var myerr *MyErr +// switch { +// case errors.As(err, &myerr): +// } +// +// because the transformation in that case would be ungainly. +// +// Note that the cmd/vet suite includes the "errorsas" analyzer, which +// detects actual mistakes in the use of errors.As. This logic does +// not belong in errorsas because the problems it fixes are merely +// stylistic. +// +// TODO(adonovan): support more cases: +// +// - Negative cases +// var myerr E +// if !errors.As(err, &myerr) { ... } +// => +// myerr, ok := errors.AsType[E](err) +// if !ok { ... } +// +// - if myerr := new(E); errors.As(err, myerr); { ... } +// +// - if errors.As(err, myerr) && othercond { ... } +func errorsastype(pass *analysis.Pass) (any, error) { + var ( + index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) + info = pass.TypesInfo + ) + + for curCall := range index.Calls(index.Object("errors", "As")) { + call := curCall.Node().(*ast.CallExpr) + if len(call.Args) < 2 { + continue // spread call: errors.As(pair()) + } + + v, curDeclStmt := canUseErrorsAsType(info, index, curCall) + if v == nil { + continue + } + + file := astutil.EnclosingFile(curDeclStmt) + if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_26) { + continue // errors.AsType is too new + } + + // Locate identifier "As" in errors.As. + var asIdent *ast.Ident + switch n := ast.Unparen(call.Fun).(type) { + case *ast.Ident: + asIdent = n // "errors" was dot-imported + case *ast.SelectorExpr: + asIdent = n.Sel + default: + panic("no Ident for errors.As") + } + + // Format the type as valid Go syntax. + // TODO(adonovan): fix: FileQualifier needs to respect + // visibility at the current point, and either fail + // or edit the imports as needed. + // TODO(adonovan): fix: TypeString is not a sound way + // to print types as Go syntax as it does not respect + // symbol visibility, etc. We need something loosely + // integrated with FileQualifier that accumulates + // import edits, and may fail (e.g. for unexported + // type or field names from other packages). + // See https://go.dev/issues/75604. + qual := typesinternal.FileQualifier(file, pass.Pkg) + errtype := types.TypeString(v.Type(), qual) + + // Choose a name for the "ok" variable. + // TODO(adonovan): this pattern also appears in stditerators, + // and is wanted elsewhere; factor. + okName := "ok" + if okVar := lookup(info, curCall, "ok"); okVar != nil { + // The name 'ok' is already declared, but + // don't choose a fresh name unless okVar + // is also used within the if-statement. + curIf := curCall.Parent() + for curUse := range index.Uses(okVar) { + if curIf.Contains(curUse) { + scope := info.Scopes[curIf.Node().(*ast.IfStmt)] + okName = refactor.FreshName(scope, v.Pos(), "ok") + break + } + } + } + + pass.Report(analysis.Diagnostic{ + Pos: call.Fun.Pos(), + End: call.Fun.End(), + Message: fmt.Sprintf("errors.As can be simplified using AsType[%s]", errtype), + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("Replace errors.As with AsType[%s]", errtype), + TextEdits: append( + // delete "var myerr *MyErr" + refactor.DeleteStmt(pass.Fset.File(call.Fun.Pos()), curDeclStmt), + // if errors.As (err, &myerr) { ... } + // ------------- -------------- -------- ---- + // if myerr, ok := errors.AsType[*MyErr](err ); ok { ... } + analysis.TextEdit{ + // insert "myerr, ok := " + Pos: call.Pos(), + End: call.Pos(), + NewText: fmt.Appendf(nil, "%s, %s := ", v.Name(), okName), + }, + analysis.TextEdit{ + // replace As with AsType[T] + Pos: asIdent.Pos(), + End: asIdent.End(), + NewText: fmt.Appendf(nil, "AsType[%s]", errtype), + }, + analysis.TextEdit{ + // delete ", &myerr" + Pos: call.Args[0].End(), + End: call.Args[1].End(), + }, + analysis.TextEdit{ + // insert "; ok" + Pos: call.End(), + End: call.End(), + NewText: fmt.Appendf(nil, "; %s", okName), + }, + ), + }}, + }) + } + return nil, nil +} + +// canUseErrorsAsType reports whether curCall is a call to +// errors.As beneath an if statement, preceded by a +// declaration of the typed error var. The var must not be +// used outside the if statement. +func canUseErrorsAsType(info *types.Info, index *typeindex.Index, curCall inspector.Cursor) (_ *types.Var, _ inspector.Cursor) { + if !astutil.IsChildOf(curCall, edge.IfStmt_Cond) { + return // not beneath if statement + } + var ( + curIfStmt = curCall.Parent() + ifStmt = curIfStmt.Node().(*ast.IfStmt) + ) + if ifStmt.Init != nil { + return // if statement already has an init part + } + unary, ok := curCall.Node().(*ast.CallExpr).Args[1].(*ast.UnaryExpr) + if !ok || unary.Op != token.AND { + return // 2nd arg is not &var + } + id, ok := unary.X.(*ast.Ident) + if !ok { + return // not a simple ident (local var) + } + v := info.Uses[id].(*types.Var) + curDef, ok := index.Def(v) + if !ok { + return // var is not local (e.g. dot-imported) + } + // Have: if errors.As(err, &v) { ... } + + // Reject if v is used outside (before or after) the + // IfStmt, since that will become its new scope. + for curUse := range index.Uses(v) { + if !curIfStmt.Contains(curUse) { + return // v used before/after if statement + } + } + if !astutil.IsChildOf(curDef, edge.ValueSpec_Names) { + return // v not declared by "var v T" + } + var ( + curSpec = curDef.Parent() // ValueSpec + curDecl = curSpec.Parent() // GenDecl + spec = curSpec.Node().(*ast.ValueSpec) + ) + if len(spec.Names) != 1 || len(spec.Values) != 0 || + len(curDecl.Node().(*ast.GenDecl).Specs) != 1 { + return // not a simple "var v T" decl + } + + // Have: + // var v *MyErr + // ... + // if errors.As(err, &v) { ... } + // with no uses of v outside the IfStmt. + return v, curDecl.Parent() // DeclStmt +} diff --git a/vendor/golang.org/x/tools/go/analysis/passes/modernize/fmtappendf.go b/vendor/golang.org/x/tools/go/analysis/passes/modernize/fmtappendf.go new file mode 100644 index 00000000..389f7034 --- /dev/null +++ b/vendor/golang.org/x/tools/go/analysis/passes/modernize/fmtappendf.go @@ -0,0 +1,112 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modernize + +import ( + "fmt" + "go/ast" + "go/types" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/edge" + "golang.org/x/tools/internal/analysis/analyzerutil" + typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex" + "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/typesinternal/typeindex" + "golang.org/x/tools/internal/versions" +) + +var FmtAppendfAnalyzer = &analysis.Analyzer{ + Name: "fmtappendf", + Doc: analyzerutil.MustExtractDoc(doc, "fmtappendf"), + Requires: []*analysis.Analyzer{ + inspect.Analyzer, + typeindexanalyzer.Analyzer, + }, + Run: fmtappendf, + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#fmtappendf", +} + +// The fmtappend function replaces []byte(fmt.Sprintf(...)) by +// fmt.Appendf(nil, ...), and similarly for Sprint, Sprintln. +func fmtappendf(pass *analysis.Pass) (any, error) { + index := pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) + for _, fn := range []types.Object{ + index.Object("fmt", "Sprintf"), + index.Object("fmt", "Sprintln"), + index.Object("fmt", "Sprint"), + } { + for curCall := range index.Calls(fn) { + call := curCall.Node().(*ast.CallExpr) + if ek, idx := curCall.ParentEdge(); ek == edge.CallExpr_Args && idx == 0 { + // Is parent a T(fmt.SprintX(...)) conversion? + conv := curCall.Parent().Node().(*ast.CallExpr) + tv := pass.TypesInfo.Types[conv.Fun] + if tv.IsType() && types.Identical(tv.Type, byteSliceType) && + analyzerutil.FileUsesGoVersion(pass, astutil.EnclosingFile(curCall), versions.Go1_19) { + // Have: []byte(fmt.SprintX(...)) + + // Find "Sprint" identifier. + var id *ast.Ident + switch e := ast.Unparen(call.Fun).(type) { + case *ast.SelectorExpr: + id = e.Sel // "fmt.Sprint" + case *ast.Ident: + id = e // "Sprint" after `import . "fmt"` + } + + old, new := fn.Name(), strings.Replace(fn.Name(), "Sprint", "Append", 1) + edits := []analysis.TextEdit{ + { + // delete "[]byte(" + Pos: conv.Pos(), + End: conv.Lparen + 1, + }, + { + // remove ")" + Pos: conv.Rparen, + End: conv.Rparen + 1, + }, + { + Pos: id.Pos(), + End: id.End(), + NewText: []byte(new), + }, + { + Pos: call.Lparen + 1, + NewText: []byte("nil, "), + }, + } + if len(conv.Args) == 1 { + arg := conv.Args[0] + // Determine if we have T(fmt.SprintX(...)). If so, delete the non-args + // that come before the right parenthesis. Leaving an + // extra comma here produces invalid code. (See + // golang/go#74709) + if arg.End() < conv.Rparen { + edits = append(edits, analysis.TextEdit{ + Pos: arg.End(), + End: conv.Rparen, + }) + } + } + pass.Report(analysis.Diagnostic{ + Pos: conv.Pos(), + End: conv.End(), + Message: fmt.Sprintf("Replace []byte(fmt.%s...) with fmt.%s", old, new), + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("Replace []byte(fmt.%s...) with fmt.%s", old, new), + TextEdits: edits, + }}, + }) + } + } + } + } + return nil, nil +} diff --git a/vendor/golang.org/x/tools/go/analysis/passes/modernize/forvar.go b/vendor/golang.org/x/tools/go/analysis/passes/modernize/forvar.go new file mode 100644 index 00000000..67f60aca --- /dev/null +++ b/vendor/golang.org/x/tools/go/analysis/passes/modernize/forvar.go @@ -0,0 +1,113 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modernize + +import ( + "go/ast" + "go/token" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/internal/analysis/analyzerutil" + "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/refactor" + "golang.org/x/tools/internal/versions" +) + +var ForVarAnalyzer = &analysis.Analyzer{ + Name: "forvar", + Doc: analyzerutil.MustExtractDoc(doc, "forvar"), + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: forvar, + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#forvar", +} + +// forvar offers to fix unnecessary copying of a for variable +// +// for _, x := range foo { +// x := x // offer to remove this superfluous assignment +// } +// +// Prerequisites: +// First statement in a range loop has to be := +// where the two idents are the same, +// and the ident is defined (:=) as a variable in the for statement. +// (Note that this 'fix' does not work for three clause loops +// because the Go specfilesUsingGoVersionsays "The variable used by each subsequent iteration +// is declared implicitly before executing the post statement and initialized to the +// value of the previous iteration's variable at that moment.") +// +// Variant: same thing in an IfStmt.Init, when the IfStmt is the sole +// loop body statement: +// +// for _, x := range foo { +// if x := x; cond { ... } +// } +// +// (The restriction is necessary to avoid potential problems arising +// from merging two distinct variables.) +// +// This analyzer is synergistic with stditerators, +// which may create redundant "x := x" statements. +func forvar(pass *analysis.Pass) (any, error) { + for curFile := range filesUsingGoVersion(pass, versions.Go1_22) { + for curLoop := range curFile.Preorder((*ast.RangeStmt)(nil)) { + loop := curLoop.Node().(*ast.RangeStmt) + if loop.Tok != token.DEFINE { + continue + } + isLoopVarRedecl := func(stmt ast.Stmt) bool { + if assign, ok := stmt.(*ast.AssignStmt); ok && + assign.Tok == token.DEFINE && + len(assign.Lhs) == len(assign.Rhs) { + + for i, lhs := range assign.Lhs { + if !(astutil.EqualSyntax(lhs, assign.Rhs[i]) && + (astutil.EqualSyntax(lhs, loop.Key) || + astutil.EqualSyntax(lhs, loop.Value))) { + return false + } + } + return true + } + return false + } + // Have: for k, v := range x { stmts } + // + // Delete the prefix of stmts that are + // of the form k := k; v := v; k, v := k, v; v, k := v, k. + for _, stmt := range loop.Body.List { + if isLoopVarRedecl(stmt) { + // { x := x; ... } + // ------ + } else if ifstmt, ok := stmt.(*ast.IfStmt); ok && + ifstmt.Init != nil && + len(loop.Body.List) == 1 && // must be sole statement in loop body + isLoopVarRedecl(ifstmt.Init) { + // if x := x; cond { + // ------ + stmt = ifstmt.Init + } else { + break // stop at first other statement + } + + curStmt, _ := curLoop.FindNode(stmt) + edits := refactor.DeleteStmt(pass.Fset.File(stmt.Pos()), curStmt) + if len(edits) > 0 { + pass.Report(analysis.Diagnostic{ + Pos: stmt.Pos(), + End: stmt.End(), + Message: "copying variable is unneeded", + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Remove unneeded redeclaration", + TextEdits: edits, + }}, + }) + } + } + } + } + return nil, nil +} diff --git a/vendor/golang.org/x/tools/go/analysis/passes/modernize/maps.go b/vendor/golang.org/x/tools/go/analysis/passes/modernize/maps.go new file mode 100644 index 00000000..2352c8b6 --- /dev/null +++ b/vendor/golang.org/x/tools/go/analysis/passes/modernize/maps.go @@ -0,0 +1,274 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modernize + +// This file defines modernizers that use the "maps" package. + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/analysis/analyzerutil" + "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/refactor" + "golang.org/x/tools/internal/typeparams" + "golang.org/x/tools/internal/typesinternal" + "golang.org/x/tools/internal/versions" +) + +var MapsLoopAnalyzer = &analysis.Analyzer{ + Name: "mapsloop", + Doc: analyzerutil.MustExtractDoc(doc, "mapsloop"), + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: mapsloop, + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#mapsloop", +} + +// The mapsloop pass offers to simplify a loop of map insertions: +// +// for k, v := range x { +// m[k] = v +// } +// +// by a call to go1.23's maps package. There are four variants, the +// product of two axes: whether the source x is a map or an iter.Seq2, +// and whether the destination m is a newly created map: +// +// maps.Copy(m, x) (x is map) +// maps.Insert(m, x) (x is iter.Seq2) +// m = maps.Clone(x) (x is a non-nil map, m is a new map) +// m = maps.Collect(x) (x is iter.Seq2, m is a new map) +// +// A map is newly created if the preceding statement has one of these +// forms, where M is a map type: +// +// m = make(M) +// m = M{} +func mapsloop(pass *analysis.Pass) (any, error) { + // Skip the analyzer in packages where its + // fixes would create an import cycle. + if within(pass, "maps", "bytes", "runtime") { + return nil, nil + } + + info := pass.TypesInfo + + // check is called for each statement of this form: + // for k, v := range x { m[k] = v } + check := func(file *ast.File, curRange inspector.Cursor, assign *ast.AssignStmt, m, x ast.Expr) { + + // Is x a map or iter.Seq2? + tx := types.Unalias(info.TypeOf(x)) + var xmap bool + switch typeparams.CoreType(tx).(type) { + case *types.Map: + xmap = true + + case *types.Signature: + k, v, ok := assignableToIterSeq2(tx) + if !ok { + return // a named isomer of Seq2 + } + xmap = false + + // Record in tx the unnamed map[K]V type + // derived from the yield function. + // This is the type of maps.Collect(x). + tx = types.NewMap(k, v) + + default: + return // e.g. slice, channel (or no core type!) + } + + // Is the preceding statement of the form + // m = make(M) or M{} + // and can we replace its RHS with slices.{Clone,Collect}? + // + // Beware: if x may be nil, we cannot use Clone as it preserves nilness. + var mrhs ast.Expr // make(M) or M{}, or nil + if curPrev, ok := curRange.PrevSibling(); ok { + if assign, ok := curPrev.Node().(*ast.AssignStmt); ok && + len(assign.Lhs) == 1 && + len(assign.Rhs) == 1 && + astutil.EqualSyntax(assign.Lhs[0], m) { + + // Have: m = rhs; for k, v := range x { m[k] = v } + var newMap bool + rhs := assign.Rhs[0] + switch rhs := ast.Unparen(rhs).(type) { + case *ast.CallExpr: + if id, ok := ast.Unparen(rhs.Fun).(*ast.Ident); ok && + info.Uses[id] == builtinMake { + // Have: m = make(...) + newMap = true + } + case *ast.CompositeLit: + if len(rhs.Elts) == 0 { + // Have m = M{} + newMap = true + } + } + + // Take care not to change type of m's RHS expression. + if newMap { + trhs := info.TypeOf(rhs) + + // Inv: tx is the type of maps.F(x) + // - maps.Clone(x) has the same type as x. + // - maps.Collect(x) returns an unnamed map type. + + if assign.Tok == token.DEFINE { + // DEFINE (:=): we must not + // change the type of RHS. + if types.Identical(tx, trhs) { + mrhs = rhs + } + } else { + // ASSIGN (=): the types of LHS + // and RHS may differ in namedness. + if types.AssignableTo(tx, trhs) { + mrhs = rhs + } + } + + // Temporarily disable the transformation to the + // (nil-preserving) maps.Clone until we can prove + // that x is non-nil. This is rarely possible, + // and may require control flow analysis + // (e.g. a dominating "if len(x)" check). + // See #71844. + if xmap { + mrhs = nil + } + } + } + } + + // Choose function. + var funcName string + if mrhs != nil { + funcName = cond(xmap, "Clone", "Collect") + } else { + funcName = cond(xmap, "Copy", "Insert") + } + + // Report diagnostic, and suggest fix. + rng := curRange.Node() + prefix, importEdits := refactor.AddImport(info, file, "maps", "maps", funcName, rng.Pos()) + var ( + newText []byte + start, end token.Pos + ) + if mrhs != nil { + // Replace assignment and loop with expression. + // + // m = make(...) + // for k, v := range x { /* comments */ m[k] = v } + // + // -> + // + // /* comments */ + // m = maps.Copy(x) + curPrev, _ := curRange.PrevSibling() + start, end = curPrev.Node().Pos(), rng.End() + newText = fmt.Appendf(nil, "%s%s = %s%s(%s)", + allComments(file, start, end), + astutil.Format(pass.Fset, m), + prefix, + funcName, + astutil.Format(pass.Fset, x)) + } else { + // Replace loop with call statement. + // + // for k, v := range x { /* comments */ m[k] = v } + // + // -> + // + // /* comments */ + // maps.Copy(m, x) + start, end = rng.Pos(), rng.End() + newText = fmt.Appendf(nil, "%s%s%s(%s, %s)", + allComments(file, start, end), + prefix, + funcName, + astutil.Format(pass.Fset, m), + astutil.Format(pass.Fset, x)) + } + pass.Report(analysis.Diagnostic{ + Pos: assign.Lhs[0].Pos(), + End: assign.Lhs[0].End(), + Message: "Replace m[k]=v loop with maps." + funcName, + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Replace m[k]=v loop with maps." + funcName, + TextEdits: append(importEdits, []analysis.TextEdit{{ + Pos: start, + End: end, + NewText: newText, + }}...), + }}, + }) + + } + + // Find all range loops around m[k] = v. + for curFile := range filesUsingGoVersion(pass, versions.Go1_23) { + file := curFile.Node().(*ast.File) + + for curRange := range curFile.Preorder((*ast.RangeStmt)(nil)) { + rng := curRange.Node().(*ast.RangeStmt) + + if rng.Tok == token.DEFINE && + rng.Key != nil && + rng.Value != nil && + isAssignBlock(rng.Body) { + // Have: for k, v := range x { lhs = rhs } + + assign := rng.Body.List[0].(*ast.AssignStmt) + if index, ok := assign.Lhs[0].(*ast.IndexExpr); ok && + astutil.EqualSyntax(rng.Key, index.Index) && + astutil.EqualSyntax(rng.Value, assign.Rhs[0]) && + is[*types.Map](typeparams.CoreType(info.TypeOf(index.X))) && + types.Identical(info.TypeOf(index), info.TypeOf(rng.Value)) { // m[k], v + + // Have: for k, v := range x { m[k] = v } + // where there is no implicit conversion. + check(file, curRange, assign, index.X, rng.X) + } + } + } + } + return nil, nil +} + +// assignableToIterSeq2 reports whether t is assignable to +// iter.Seq[K, V] and returns K and V if so. +func assignableToIterSeq2(t types.Type) (k, v types.Type, ok bool) { + // The only named type assignable to iter.Seq2 is iter.Seq2. + if is[*types.Named](t) { + if !typesinternal.IsTypeNamed(t, "iter", "Seq2") { + return + } + t = t.Underlying() + } + + if t, ok := t.(*types.Signature); ok { + // func(yield func(K, V) bool)? + if t.Params().Len() == 1 && t.Results().Len() == 0 { + if yield, ok := t.Params().At(0).Type().(*types.Signature); ok { // sic, no Underlying/CoreType + if yield.Params().Len() == 2 && + yield.Results().Len() == 1 && + types.Identical(yield.Results().At(0).Type(), builtinBool.Type()) { + return yield.Params().At(0).Type(), yield.Params().At(1).Type(), true + } + } + } + } + return +} diff --git a/vendor/golang.org/x/tools/go/analysis/passes/modernize/minmax.go b/vendor/golang.org/x/tools/go/analysis/passes/modernize/minmax.go new file mode 100644 index 00000000..23a0977f --- /dev/null +++ b/vendor/golang.org/x/tools/go/analysis/passes/modernize/minmax.go @@ -0,0 +1,436 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modernize + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/edge" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/analysis/analyzerutil" + typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex" + "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/typeparams" + "golang.org/x/tools/internal/typesinternal/typeindex" + "golang.org/x/tools/internal/versions" +) + +var MinMaxAnalyzer = &analysis.Analyzer{ + Name: "minmax", + Doc: analyzerutil.MustExtractDoc(doc, "minmax"), + Requires: []*analysis.Analyzer{ + inspect.Analyzer, + typeindexanalyzer.Analyzer, + }, + Run: minmax, + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#minmax", +} + +// The minmax pass replaces if/else statements with calls to min or max, +// and removes user-defined min/max functions that are equivalent to built-ins. +// +// If/else replacement patterns: +// +// 1. if a < b { x = a } else { x = b } => x = min(a, b) +// 2. x = a; if a < b { x = b } => x = max(a, b) +// +// Pattern 1 requires that a is not NaN, and pattern 2 requires that b +// is not Nan. Since this is hard to prove, we reject floating-point +// numbers. +// +// Function removal: +// User-defined min/max functions are suggested for removal if they may +// be safely replaced by their built-in namesake. +// +// Variants: +// - all four ordered comparisons +// - "x := a" or "x = a" or "var x = a" in pattern 2 +// - "x < b" or "a < b" in pattern 2 +func minmax(pass *analysis.Pass) (any, error) { + // Check for user-defined min/max functions that can be removed + checkUserDefinedMinMax(pass) + + // check is called for all statements of this form: + // if a < b { lhs = rhs } + check := func(file *ast.File, curIfStmt inspector.Cursor, compare *ast.BinaryExpr) { + var ( + ifStmt = curIfStmt.Node().(*ast.IfStmt) + tassign = ifStmt.Body.List[0].(*ast.AssignStmt) + a = compare.X + b = compare.Y + lhs = tassign.Lhs[0] + rhs = tassign.Rhs[0] + sign = isInequality(compare.Op) + + // callArg formats a call argument, preserving comments from [start-end). + callArg = func(arg ast.Expr, start, end token.Pos) string { + comments := allComments(file, start, end) + return cond(arg == b, ", ", "") + // second argument needs a comma + cond(comments != "", "\n", "") + // comments need their own line + comments + + astutil.Format(pass.Fset, arg) + } + ) + + if fblock, ok := ifStmt.Else.(*ast.BlockStmt); ok && isAssignBlock(fblock) { + fassign := fblock.List[0].(*ast.AssignStmt) + + // Have: if a < b { lhs = rhs } else { lhs2 = rhs2 } + lhs2 := fassign.Lhs[0] + rhs2 := fassign.Rhs[0] + + // For pattern 1, check that: + // - lhs = lhs2 + // - {rhs,rhs2} = {a,b} + if astutil.EqualSyntax(lhs, lhs2) { + if astutil.EqualSyntax(rhs, a) && astutil.EqualSyntax(rhs2, b) { + sign = +sign + } else if astutil.EqualSyntax(rhs2, a) && astutil.EqualSyntax(rhs, b) { + sign = -sign + } else { + return + } + + sym := cond(sign < 0, "min", "max") + + if !is[*types.Builtin](lookup(pass.TypesInfo, curIfStmt, sym)) { + return // min/max function is shadowed + } + + // pattern 1 + // + // TODO(adonovan): if lhs is declared "var lhs T" on preceding line, + // simplify the whole thing to "lhs := min(a, b)". + pass.Report(analysis.Diagnostic{ + // Highlight the condition a < b. + Pos: compare.Pos(), + End: compare.End(), + Message: fmt.Sprintf("if/else statement can be modernized using %s", sym), + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("Replace if statement with %s", sym), + TextEdits: []analysis.TextEdit{{ + // Replace IfStmt with lhs = min(a, b). + Pos: ifStmt.Pos(), + End: ifStmt.End(), + NewText: fmt.Appendf(nil, "%s = %s(%s%s)", + astutil.Format(pass.Fset, lhs), + sym, + callArg(a, ifStmt.Pos(), ifStmt.Else.Pos()), + callArg(b, ifStmt.Else.Pos(), ifStmt.End()), + ), + }}, + }}, + }) + } + + } else if prev, ok := curIfStmt.PrevSibling(); ok && isSimpleAssign(prev.Node()) && ifStmt.Else == nil { + fassign := prev.Node().(*ast.AssignStmt) + + // Have: lhs0 = rhs0; if a < b { lhs = rhs } + // + // For pattern 2, check that + // - lhs = lhs0 + // - {a,b} = {rhs,rhs0} or {rhs,lhs0} + // The replacement must use rhs0 not lhs0 though. + // For example, we accept this variant: + // lhs = x; if lhs < y { lhs = y } => lhs = min(x, y), not min(lhs, y) + // + // TODO(adonovan): accept "var lhs0 = rhs0" form too. + lhs0 := fassign.Lhs[0] + rhs0 := fassign.Rhs[0] + + if astutil.EqualSyntax(lhs, lhs0) { + if astutil.EqualSyntax(rhs, a) && (astutil.EqualSyntax(rhs0, b) || astutil.EqualSyntax(lhs0, b)) { + sign = +sign + } else if (astutil.EqualSyntax(rhs0, a) || astutil.EqualSyntax(lhs0, a)) && astutil.EqualSyntax(rhs, b) { + sign = -sign + } else { + return + } + sym := cond(sign < 0, "min", "max") + + if !is[*types.Builtin](lookup(pass.TypesInfo, curIfStmt, sym)) { + return // min/max function is shadowed + } + + // Permit lhs0 to stand for rhs0 in the matching, + // but don't actually reduce to lhs0 = min(lhs0, rhs) + // since the "=" could be a ":=". Use min(rhs0, rhs). + if astutil.EqualSyntax(lhs0, a) { + a = rhs0 + } else if astutil.EqualSyntax(lhs0, b) { + b = rhs0 + } + + // pattern 2 + pass.Report(analysis.Diagnostic{ + // Highlight the condition a < b. + Pos: compare.Pos(), + End: compare.End(), + Message: fmt.Sprintf("if statement can be modernized using %s", sym), + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("Replace if/else with %s", sym), + TextEdits: []analysis.TextEdit{{ + Pos: fassign.Pos(), + End: ifStmt.End(), + // Replace "x := a; if ... {}" with "x = min(...)", preserving comments. + NewText: fmt.Appendf(nil, "%s %s %s(%s%s)", + astutil.Format(pass.Fset, lhs), + fassign.Tok.String(), + sym, + callArg(a, fassign.Pos(), ifStmt.Pos()), + callArg(b, ifStmt.Pos(), ifStmt.End()), + ), + }}, + }}, + }) + } + } + } + + // Find all "if a < b { lhs = rhs }" statements. + info := pass.TypesInfo + for curFile := range filesUsingGoVersion(pass, versions.Go1_21) { + astFile := curFile.Node().(*ast.File) + for curIfStmt := range curFile.Preorder((*ast.IfStmt)(nil)) { + ifStmt := curIfStmt.Node().(*ast.IfStmt) + + // Don't bother handling "if a < b { lhs = rhs }" when it appears + // as the "else" branch of another if-statement. + // if cond { ... } else if a < b { lhs = rhs } + // (This case would require introducing another block + // if cond { ... } else { if a < b { lhs = rhs } } + // and checking that there is no following "else".) + if astutil.IsChildOf(curIfStmt, edge.IfStmt_Else) { + continue + } + + if compare, ok := ifStmt.Cond.(*ast.BinaryExpr); ok && + ifStmt.Init == nil && + isInequality(compare.Op) != 0 && + isAssignBlock(ifStmt.Body) { + // a blank var has no type. + if tLHS := info.TypeOf(ifStmt.Body.List[0].(*ast.AssignStmt).Lhs[0]); tLHS != nil && !maybeNaN(tLHS) { + // Have: if a < b { lhs = rhs } + check(astFile, curIfStmt, compare) + } + } + } + } + return nil, nil +} + +// allComments collects all the comments from start to end. +func allComments(file *ast.File, start, end token.Pos) string { + var buf strings.Builder + for co := range astutil.Comments(file, start, end) { + _, _ = fmt.Fprintf(&buf, "%s\n", co.Text) + } + return buf.String() +} + +// isInequality reports non-zero if tok is one of < <= => >: +// +1 for > and -1 for <. +func isInequality(tok token.Token) int { + switch tok { + case token.LEQ, token.LSS: + return -1 + case token.GEQ, token.GTR: + return +1 + } + return 0 +} + +// isAssignBlock reports whether b is a block of the form { lhs = rhs }. +func isAssignBlock(b *ast.BlockStmt) bool { + if len(b.List) != 1 { + return false + } + // Inv: the sole statement cannot be { lhs := rhs }. + return isSimpleAssign(b.List[0]) +} + +// isSimpleAssign reports whether n has the form "lhs = rhs" or "lhs := rhs". +func isSimpleAssign(n ast.Node) bool { + assign, ok := n.(*ast.AssignStmt) + return ok && + (assign.Tok == token.ASSIGN || assign.Tok == token.DEFINE) && + len(assign.Lhs) == 1 && + len(assign.Rhs) == 1 +} + +// maybeNaN reports whether t is (or may be) a floating-point type. +func maybeNaN(t types.Type) bool { + // For now, we rely on core types. + // TODO(adonovan): In the post-core-types future, + // follow the approach of types.Checker.applyTypeFunc. + t = typeparams.CoreType(t) + if t == nil { + return true // fail safe + } + if basic, ok := t.(*types.Basic); ok && basic.Info()&types.IsFloat != 0 { + return true + } + return false +} + +// checkUserDefinedMinMax looks for user-defined min/max functions that are +// equivalent to the built-in functions and suggests removing them. +func checkUserDefinedMinMax(pass *analysis.Pass) { + index := pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) + + // Look up min and max functions by name in package scope + for _, funcName := range []string{"min", "max"} { + if fn, ok := pass.Pkg.Scope().Lookup(funcName).(*types.Func); ok { + // Use typeindex to get the FuncDecl directly + if def, ok := index.Def(fn); ok { + decl := def.Parent().Node().(*ast.FuncDecl) + // Check if this function matches the built-in min/max signature and behavior + if canUseBuiltinMinMax(fn, decl.Body) { + // Expand to include leading doc comment + pos := decl.Pos() + if docs := astutil.DocComment(decl); docs != nil { + pos = docs.Pos() + } + + pass.Report(analysis.Diagnostic{ + Pos: decl.Pos(), + End: decl.End(), + Message: fmt.Sprintf("user-defined %s function is equivalent to built-in %s and can be removed", funcName, funcName), + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("Remove user-defined %s function", funcName), + TextEdits: []analysis.TextEdit{{ + Pos: pos, + End: decl.End(), + }}, + }}, + }) + } + } + } + } +} + +// canUseBuiltinMinMax reports whether it is safe to replace a call +// to this min or max function by its built-in namesake. +func canUseBuiltinMinMax(fn *types.Func, body *ast.BlockStmt) bool { + sig := fn.Type().(*types.Signature) + + // Only consider the most common case: exactly 2 parameters + if sig.Params().Len() != 2 { + return false + } + + // Check if any parameter might be floating-point + for param := range sig.Params().Variables() { + if maybeNaN(param.Type()) { + return false // Don't suggest removal for float types due to NaN handling + } + } + + // Must have exactly one return value + if sig.Results().Len() != 1 { + return false + } + + // Check that the function body implements the expected min/max logic + if body == nil { + return false + } + + return hasMinMaxLogic(body, fn.Name()) +} + +// hasMinMaxLogic checks if the function body implements simple min/max logic. +func hasMinMaxLogic(body *ast.BlockStmt, funcName string) bool { + // Pattern 1: Single if/else statement + if len(body.List) == 1 { + if ifStmt, ok := body.List[0].(*ast.IfStmt); ok { + // Get the "false" result from the else block + if elseBlock, ok := ifStmt.Else.(*ast.BlockStmt); ok && len(elseBlock.List) == 1 { + if elseRet, ok := elseBlock.List[0].(*ast.ReturnStmt); ok && len(elseRet.Results) == 1 { + return checkMinMaxPattern(ifStmt, elseRet.Results[0], funcName) + } + } + } + } + + // Pattern 2: if statement followed by return + if len(body.List) == 2 { + if ifStmt, ok := body.List[0].(*ast.IfStmt); ok && ifStmt.Else == nil { + if retStmt, ok := body.List[1].(*ast.ReturnStmt); ok && len(retStmt.Results) == 1 { + return checkMinMaxPattern(ifStmt, retStmt.Results[0], funcName) + } + } + } + + return false +} + +// checkMinMaxPattern checks if an if statement implements min/max logic. +// ifStmt: the if statement to check +// falseResult: the expression returned when the condition is false +// funcName: "min" or "max" +func checkMinMaxPattern(ifStmt *ast.IfStmt, falseResult ast.Expr, funcName string) bool { + // Must have condition with comparison + cmp, ok := ifStmt.Cond.(*ast.BinaryExpr) + if !ok { + return false + } + + // Check if then branch returns one of the compared values + if len(ifStmt.Body.List) != 1 { + return false + } + + thenRet, ok := ifStmt.Body.List[0].(*ast.ReturnStmt) + if !ok || len(thenRet.Results) != 1 { + return false + } + + // Use the same logic as the existing minmax analyzer + sign := isInequality(cmp.Op) + if sign == 0 { + return false // Not a comparison operator + } + + t := thenRet.Results[0] // "true" result + f := falseResult // "false" result + x := cmp.X // left operand + y := cmp.Y // right operand + + // Check operand order and adjust sign accordingly + if astutil.EqualSyntax(t, x) && astutil.EqualSyntax(f, y) { + sign = +sign + } else if astutil.EqualSyntax(t, y) && astutil.EqualSyntax(f, x) { + sign = -sign + } else { + return false + } + + // Check if the sign matches the function name + return cond(sign < 0, "min", "max") == funcName +} + +// -- utils -- + +func is[T any](x any) bool { + _, ok := x.(T) + return ok +} + +func cond[T any](cond bool, t, f T) T { + if cond { + return t + } else { + return f + } +} diff --git a/vendor/golang.org/x/tools/go/analysis/passes/modernize/modernize.go b/vendor/golang.org/x/tools/go/analysis/passes/modernize/modernize.go new file mode 100644 index 00000000..013ce79d --- /dev/null +++ b/vendor/golang.org/x/tools/go/analysis/passes/modernize/modernize.go @@ -0,0 +1,143 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modernize + +import ( + _ "embed" + "go/ast" + "go/constant" + "go/format" + "go/token" + "go/types" + "iter" + "regexp" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/edge" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/analysis/analyzerutil" + "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/moreiters" + "golang.org/x/tools/internal/packagepath" + "golang.org/x/tools/internal/stdlib" + "golang.org/x/tools/internal/typesinternal" +) + +//go:embed doc.go +var doc string + +// Suite lists all modernize analyzers. +var Suite = []*analysis.Analyzer{ + AnyAnalyzer, + // AppendClippedAnalyzer, // not nil-preserving! + BLoopAnalyzer, + FmtAppendfAnalyzer, + ForVarAnalyzer, + MapsLoopAnalyzer, + MinMaxAnalyzer, + NewExprAnalyzer, + OmitZeroAnalyzer, + plusBuildAnalyzer, + RangeIntAnalyzer, + ReflectTypeForAnalyzer, + SlicesContainsAnalyzer, + // SlicesDeleteAnalyzer, // not nil-preserving! + SlicesSortAnalyzer, + stditeratorsAnalyzer, + stringscutAnalyzer, + StringsCutPrefixAnalyzer, + StringsSeqAnalyzer, + StringsBuilderAnalyzer, + TestingContextAnalyzer, + WaitGroupAnalyzer, +} + +// -- helpers -- + +// formatExprs formats a comma-separated list of expressions. +func formatExprs(fset *token.FileSet, exprs []ast.Expr) string { + var buf strings.Builder + for i, e := range exprs { + if i > 0 { + buf.WriteString(", ") + } + format.Node(&buf, fset, e) // ignore errors + } + return buf.String() +} + +// isZeroIntConst reports whether e is an integer whose value is 0. +func isZeroIntConst(info *types.Info, e ast.Expr) bool { + return isIntLiteral(info, e, 0) +} + +// isIntLiteral reports whether e is an integer with given value. +func isIntLiteral(info *types.Info, e ast.Expr, n int64) bool { + return info.Types[e].Value == constant.MakeInt64(n) +} + +// filesUsingGoVersion returns a cursor for each *ast.File in the inspector +// that uses at least the specified version of Go (e.g. "go1.24"). +// +// The pass's analyzer must require [inspect.Analyzer]. +// +// TODO(adonovan): opt: eliminate this function, instead following the +// approach of [fmtappendf], which uses typeindex and +// [analyzerutil.FileUsesGoVersion]; see "Tip" documented at the +// latter function for motivation. +func filesUsingGoVersion(pass *analysis.Pass, version string) iter.Seq[inspector.Cursor] { + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + + return func(yield func(inspector.Cursor) bool) { + for curFile := range inspect.Root().Children() { + file := curFile.Node().(*ast.File) + if analyzerutil.FileUsesGoVersion(pass, file, version) && !yield(curFile) { + break + } + } + } +} + +// within reports whether the current pass is analyzing one of the +// specified standard packages or their dependencies. +func within(pass *analysis.Pass, pkgs ...string) bool { + path := pass.Pkg.Path() + return packagepath.IsStdPackage(path) && + moreiters.Contains(stdlib.Dependencies(pkgs...), path) +} + +// unparenEnclosing removes enclosing parens from cur in +// preparation for a call to [Cursor.ParentEdge]. +func unparenEnclosing(cur inspector.Cursor) inspector.Cursor { + for astutil.IsChildOf(cur, edge.ParenExpr_X) { + cur = cur.Parent() + } + return cur +} + +var ( + builtinAny = types.Universe.Lookup("any") + builtinAppend = types.Universe.Lookup("append") + builtinBool = types.Universe.Lookup("bool") + builtinInt = types.Universe.Lookup("int") + builtinFalse = types.Universe.Lookup("false") + builtinLen = types.Universe.Lookup("len") + builtinMake = types.Universe.Lookup("make") + builtinNew = types.Universe.Lookup("new") + builtinNil = types.Universe.Lookup("nil") + builtinString = types.Universe.Lookup("string") + builtinTrue = types.Universe.Lookup("true") + byteSliceType = types.NewSlice(types.Typ[types.Byte]) + omitemptyRegex = regexp.MustCompile(`(?:^json| json):"[^"]*(,omitempty)(?:"|,[^"]*")\s?`) +) + +// lookup returns the symbol denoted by name at the position of the cursor. +func lookup(info *types.Info, cur inspector.Cursor, name string) types.Object { + scope := typesinternal.EnclosingScope(info, cur) + _, obj := scope.LookupParent(name, cur.Node().Pos()) + return obj +} diff --git a/vendor/golang.org/x/tools/go/analysis/passes/modernize/newexpr.go b/vendor/golang.org/x/tools/go/analysis/passes/modernize/newexpr.go new file mode 100644 index 00000000..6cb75f24 --- /dev/null +++ b/vendor/golang.org/x/tools/go/analysis/passes/modernize/newexpr.go @@ -0,0 +1,202 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modernize + +import ( + _ "embed" + "go/ast" + "go/token" + "go/types" + "strings" + + "fmt" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/analysis/analyzerutil" + "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/versions" +) + +var NewExprAnalyzer = &analysis.Analyzer{ + Name: "newexpr", + Doc: analyzerutil.MustExtractDoc(doc, "newexpr"), + URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize#newexpr", + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: run, + FactTypes: []analysis.Fact{&newLike{}}, +} + +func run(pass *analysis.Pass) (any, error) { + var ( + inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + info = pass.TypesInfo + ) + + // Detect functions that are new-like, i.e. have the form: + // + // func f(x T) *T { return &x } + // + // meaning that it is equivalent to new(x), if x has type T. + for curFuncDecl := range inspect.Root().Preorder((*ast.FuncDecl)(nil)) { + decl := curFuncDecl.Node().(*ast.FuncDecl) + fn := info.Defs[decl.Name].(*types.Func) + if decl.Body != nil && len(decl.Body.List) == 1 { + if ret, ok := decl.Body.List[0].(*ast.ReturnStmt); ok && len(ret.Results) == 1 { + if unary, ok := ret.Results[0].(*ast.UnaryExpr); ok && unary.Op == token.AND { + if id, ok := unary.X.(*ast.Ident); ok { + if v, ok := info.Uses[id].(*types.Var); ok { + sig := fn.Signature() + if sig.Results().Len() == 1 && + is[*types.Pointer](sig.Results().At(0).Type()) && // => no iface conversion + sig.Params().Len() == 1 && + sig.Params().At(0) == v { + + // Export a fact for each one. + pass.ExportObjectFact(fn, &newLike{}) + + // Check file version. + file := astutil.EnclosingFile(curFuncDecl) + if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_26) { + continue // new(expr) not available in this file + } + + var edits []analysis.TextEdit + + // If 'new' is not shadowed, replace func body: &x -> new(x). + // This makes it safely and cleanly inlinable. + curRet, _ := curFuncDecl.FindNode(ret) + if lookup(info, curRet, "new") == builtinNew { + edits = []analysis.TextEdit{ + // return &x + // ---- - + // return new(x) + { + Pos: unary.OpPos, + End: unary.OpPos + token.Pos(len("&")), + NewText: []byte("new("), + }, + { + Pos: unary.X.End(), + End: unary.X.End(), + NewText: []byte(")"), + }, + } + } + + // Add a //go:fix inline annotation, if not already present. + // + // The inliner will not inline a newer callee body into an + // older Go file; see https://go.dev/issue/75726. + // + // TODO(adonovan): use ast.ParseDirective when go1.26 is assured. + if !strings.Contains(decl.Doc.Text(), "go:fix inline") { + edits = append(edits, analysis.TextEdit{ + Pos: decl.Pos(), + End: decl.Pos(), + NewText: []byte("//go:fix inline\n"), + }) + } + + if len(edits) > 0 { + pass.Report(analysis.Diagnostic{ + Pos: decl.Name.Pos(), + End: decl.Name.End(), + Message: fmt.Sprintf("%s can be an inlinable wrapper around new(expr)", decl.Name), + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: "Make %s an inlinable wrapper around new(expr)", + TextEdits: edits, + }, + }, + }) + } + } + } + } + } + } + } + } + + // Report and transform calls, when safe. + // In effect, this is inlining the new-like function + // even before we have marked the callee with //go:fix inline. + for curCall := range inspect.Root().Preorder((*ast.CallExpr)(nil)) { + call := curCall.Node().(*ast.CallExpr) + var fact newLike + if fn, ok := typeutil.Callee(info, call).(*types.Func); ok && + pass.ImportObjectFact(fn, &fact) { + + // Check file version. + file := astutil.EnclosingFile(curCall) + if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_26) { + continue // new(expr) not available in this file + } + + // Check new is not shadowed. + if lookup(info, curCall, "new") != builtinNew { + continue + } + + // The return type *T must exactly match the argument type T. + // (We formulate it this way--not in terms of the parameter + // type--to support generics.) + var targ types.Type + { + arg := call.Args[0] + tvarg := info.Types[arg] + + // Constants: we must work around the type checker + // bug that causes info.Types to wrongly report the + // "typed" type for an untyped constant. + // (See "historical reasons" in issue go.dev/issue/70638.) + // + // We don't have a reliable way to do this but we can attempt + // to re-typecheck the constant expression on its own, in + // the original lexical environment but not as a part of some + // larger expression that implies a conversion to some "typed" type. + // (For the genesis of this idea see (*state).arguments + // in ../../../../internal/refactor/inline/inline.go.) + if tvarg.Value != nil { + info2 := &types.Info{Types: make(map[ast.Expr]types.TypeAndValue)} + if err := types.CheckExpr(token.NewFileSet(), pass.Pkg, token.NoPos, arg, info2); err != nil { + continue // unexpected error + } + tvarg = info2.Types[arg] + } + + targ = types.Default(tvarg.Type) + } + if !types.Identical(types.NewPointer(targ), info.TypeOf(call)) { + continue + } + + pass.Report(analysis.Diagnostic{ + Pos: call.Pos(), + End: call.End(), + Message: fmt.Sprintf("call of %s(x) can be simplified to new(x)", fn.Name()), + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("Simplify %s(x) to new(x)", fn.Name()), + TextEdits: []analysis.TextEdit{{ + Pos: call.Fun.Pos(), + End: call.Fun.End(), + NewText: []byte("new"), + }}, + }}, + }) + } + } + + return nil, nil +} + +// A newLike fact records that its associated function is "new-like". +type newLike struct{} + +func (*newLike) AFact() {} +func (*newLike) String() string { return "newlike" } diff --git a/vendor/golang.org/x/tools/go/analysis/passes/modernize/omitzero.go b/vendor/golang.org/x/tools/go/analysis/passes/modernize/omitzero.go new file mode 100644 index 00000000..4a05d64f --- /dev/null +++ b/vendor/golang.org/x/tools/go/analysis/passes/modernize/omitzero.go @@ -0,0 +1,106 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modernize + +import ( + "go/ast" + "go/types" + "reflect" + "strconv" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/internal/analysis/analyzerutil" + "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/versions" +) + +var OmitZeroAnalyzer = &analysis.Analyzer{ + Name: "omitzero", + Doc: analyzerutil.MustExtractDoc(doc, "omitzero"), + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: omitzero, + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#omitzero", +} + +func checkOmitEmptyField(pass *analysis.Pass, info *types.Info, curField *ast.Field) { + typ := info.TypeOf(curField.Type) + _, ok := typ.Underlying().(*types.Struct) + if !ok { + // Not a struct + return + } + tag := curField.Tag + if tag == nil { + // No tag to check + return + } + // The omitempty tag may be used by other packages besides json, but we should only modify its use with json + tagconv, _ := strconv.Unquote(tag.Value) + match := omitemptyRegex.FindStringSubmatchIndex(tagconv) + if match == nil { + // No omitempty in json tag + return + } + omitEmpty, err := astutil.RangeInStringLiteral(curField.Tag, match[2], match[3]) + if err != nil { + return + } + var remove analysis.Range = omitEmpty + + jsonTag := reflect.StructTag(tagconv).Get("json") + if jsonTag == ",omitempty" { + // Remove the entire struct tag if json is the only package used + if match[1]-match[0] == len(tagconv) { + remove = curField.Tag + } else { + // Remove the json tag if omitempty is the only field + remove, err = astutil.RangeInStringLiteral(curField.Tag, match[0], match[1]) + if err != nil { + return + } + } + } + pass.Report(analysis.Diagnostic{ + Pos: curField.Tag.Pos(), + End: curField.Tag.End(), + Message: "Omitempty has no effect on nested struct fields", + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: "Remove redundant omitempty tag", + TextEdits: []analysis.TextEdit{ + { + Pos: remove.Pos(), + End: remove.End(), + }, + }, + }, + { + Message: "Replace omitempty with omitzero (behavior change)", + TextEdits: []analysis.TextEdit{ + { + Pos: omitEmpty.Pos(), + End: omitEmpty.End(), + NewText: []byte(",omitzero"), + }, + }, + }, + }}) +} + +// The omitzero pass searches for instances of "omitempty" in a json field tag on a +// struct. Since "omitfilesUsingGoVersions not have any effect when applied to a struct field, +// it suggests either deleting "omitempty" or replacing it with "omitzero", which +// correctly excludes structs from a json encoding. +func omitzero(pass *analysis.Pass) (any, error) { + for curFile := range filesUsingGoVersion(pass, versions.Go1_24) { + for curStruct := range curFile.Preorder((*ast.StructType)(nil)) { + for _, curField := range curStruct.Node().(*ast.StructType).Fields.List { + checkOmitEmptyField(pass, pass.TypesInfo, curField) + } + } + } + return nil, nil +} diff --git a/vendor/golang.org/x/tools/go/analysis/passes/modernize/plusbuild.go b/vendor/golang.org/x/tools/go/analysis/passes/modernize/plusbuild.go new file mode 100644 index 00000000..57b502ab --- /dev/null +++ b/vendor/golang.org/x/tools/go/analysis/passes/modernize/plusbuild.go @@ -0,0 +1,84 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modernize + +import ( + "go/ast" + "go/parser" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/internal/analysis/analyzerutil" + "golang.org/x/tools/internal/goplsexport" + "golang.org/x/tools/internal/versions" +) + +var plusBuildAnalyzer = &analysis.Analyzer{ + Name: "plusbuild", + Doc: analyzerutil.MustExtractDoc(doc, "plusbuild"), + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#plusbuild", + Run: plusbuild, +} + +func init() { + // Export to gopls until this is a published modernizer. + goplsexport.PlusBuildModernizer = plusBuildAnalyzer +} + +func plusbuild(pass *analysis.Pass) (any, error) { + check := func(f *ast.File) { + if !analyzerutil.FileUsesGoVersion(pass, f, versions.Go1_18) { + return + } + + // When gofmt sees a +build comment, it adds a + // preceding equivalent //go:build directive, so in + // formatted files we can assume that a +build line is + // part of a comment group that starts with a + // //go:build line and is followed by a blank line. + // + // While we cannot delete comments from an AST and + // expect consistent output in general, this specific + // case--deleting only some lines from a comment + // block--does format correctly. + for _, g := range f.Comments { + sawGoBuild := false + for _, c := range g.List { + if sawGoBuild && strings.HasPrefix(c.Text, "// +build ") { + pass.Report(analysis.Diagnostic{ + Pos: c.Pos(), + End: c.End(), + Message: "+build line is no longer needed", + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Remove obsolete +build line", + TextEdits: []analysis.TextEdit{{ + Pos: c.Pos(), + End: c.End(), + }}, + }}, + }) + break + } + if strings.HasPrefix(c.Text, "//go:build ") { + sawGoBuild = true + } + } + } + } + + for _, f := range pass.Files { + check(f) + } + for _, name := range pass.IgnoredFiles { + if strings.HasSuffix(name, ".go") { + f, err := parser.ParseFile(pass.Fset, name, nil, parser.ParseComments|parser.SkipObjectResolution) + if err != nil { + continue // parse error: ignore + } + check(f) + } + } + return nil, nil +} diff --git a/vendor/golang.org/x/tools/go/analysis/passes/modernize/rangeint.go b/vendor/golang.org/x/tools/go/analysis/passes/modernize/rangeint.go new file mode 100644 index 00000000..6b1edf38 --- /dev/null +++ b/vendor/golang.org/x/tools/go/analysis/passes/modernize/rangeint.go @@ -0,0 +1,307 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modernize + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/edge" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/analysis/analyzerutil" + typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex" + "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/typesinternal" + "golang.org/x/tools/internal/typesinternal/typeindex" + "golang.org/x/tools/internal/versions" +) + +var RangeIntAnalyzer = &analysis.Analyzer{ + Name: "rangeint", + Doc: analyzerutil.MustExtractDoc(doc, "rangeint"), + Requires: []*analysis.Analyzer{ + inspect.Analyzer, + typeindexanalyzer.Analyzer, + }, + Run: rangeint, + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#rangeint", +} + +// rangeint offers a fix to replace a 3-clause 'for' loop: +// +// for i := 0; i < limit; i++ {} +// +// by a range loop with an integer operand: +// +// for i := range limit {} +// +// Variants: +// - The ':=' may be replaced by '='. +// - The fix may remove "i :=" if it would become unused. +// +// Restrictions: +// - The variable i must not be assigned or address-taken within the +// loop, because a "for range int" loop does not respect assignments +// to the loop index. +// - The limit must not be b.N, to avoid redundancy with bloop's fixes. +// +// Caveats: +// +// The fix causes the limit expression to be evaluated exactly once, +// instead of once per iteration. So, to avoid changing the +// cardinality of side effects, the limit expression must not involve +// function calls (e.g. seq.Len()) or channel receives. Moreover, the +// value of the limit expression must be loop invariant, which in +// practice means it must take one of the following forms: +// +// - a local variable that is assigned only once and not address-taken; +// - a constant; or +// - len(s), where s has the above properties. +func rangeint(pass *analysis.Pass) (any, error) { + var ( + info = pass.TypesInfo + typeindex = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) + ) + + for curFile := range filesUsingGoVersion(pass, versions.Go1_22) { + nextLoop: + for curLoop := range curFile.Preorder((*ast.ForStmt)(nil)) { + loop := curLoop.Node().(*ast.ForStmt) + if init, ok := loop.Init.(*ast.AssignStmt); ok && + isSimpleAssign(init) && + is[*ast.Ident](init.Lhs[0]) && + isZeroIntConst(info, init.Rhs[0]) { + // Have: for i = 0; ... (or i := 0) + index := init.Lhs[0].(*ast.Ident) + + if compare, ok := loop.Cond.(*ast.BinaryExpr); ok && + compare.Op == token.LSS && + astutil.EqualSyntax(compare.X, init.Lhs[0]) { + // Have: for i = 0; i < limit; ... {} + + limit := compare.Y + + // If limit is "len(slice)", simplify it to "slice". + // + // (Don't replace "for i := 0; i < len(map); i++" + // with "for range m" because it's too hard to prove + // that len(m) is loop-invariant). + if call, ok := limit.(*ast.CallExpr); ok && + typeutil.Callee(info, call) == builtinLen && + is[*types.Slice](info.TypeOf(call.Args[0]).Underlying()) { + limit = call.Args[0] + } + + // Check the form of limit: must be a constant, + // or a local var that is not assigned or address-taken. + limitOK := false + if info.Types[limit].Value != nil { + limitOK = true // constant + } else if id, ok := limit.(*ast.Ident); ok { + if v, ok := info.Uses[id].(*types.Var); ok && + !(v.Exported() && typesinternal.IsPackageLevel(v)) { + // limit is a local or unexported global var. + // (An exported global may have uses we can't see.) + for cur := range typeindex.Uses(v) { + if isScalarLvalue(info, cur) { + // Limit var is assigned or address-taken. + continue nextLoop + } + } + limitOK = true + } + } + if !limitOK { + continue nextLoop + } + + if inc, ok := loop.Post.(*ast.IncDecStmt); ok && + inc.Tok == token.INC && + astutil.EqualSyntax(compare.X, inc.X) { + // Have: for i = 0; i < limit; i++ {} + + // Find references to i within the loop body. + v := info.ObjectOf(index).(*types.Var) + // TODO(adonovan): use go1.25 v.Kind() == types.PackageVar + if typesinternal.IsPackageLevel(v) { + continue nextLoop + } + used := false + for curId := range curLoop.Child(loop.Body).Preorder((*ast.Ident)(nil)) { + id := curId.Node().(*ast.Ident) + if info.Uses[id] == v { + used = true + + // Reject if any is an l-value (assigned or address-taken): + // a "for range int" loop does not respect assignments to + // the loop variable. + if isScalarLvalue(info, curId) { + continue nextLoop + } + } + } + + // If i is no longer used, delete "i := ". + var edits []analysis.TextEdit + if !used && init.Tok == token.DEFINE { + edits = append(edits, analysis.TextEdit{ + Pos: index.Pos(), + End: init.Rhs[0].Pos(), + }) + } + + // If i is used after the loop, + // don't offer a fix, as a range loop + // leaves i with a different final value (limit-1). + if init.Tok == token.ASSIGN { + for curId := range curLoop.Parent().Preorder((*ast.Ident)(nil)) { + id := curId.Node().(*ast.Ident) + if info.Uses[id] == v { + // Is i used after loop? + if id.Pos() > loop.End() { + continue nextLoop + } + // Is i used within a defer statement + // that is within the scope of i? + // var i int + // defer func() { print(i)} + // for i = ... { ... } + for curDefer := range curId.Enclosing((*ast.DeferStmt)(nil)) { + if curDefer.Node().Pos() > v.Pos() { + continue nextLoop + } + } + } + } + } + + // If limit is len(slice), + // simplify "range len(slice)" to "range slice". + if call, ok := limit.(*ast.CallExpr); ok && + typeutil.Callee(info, call) == builtinLen && + is[*types.Slice](info.TypeOf(call.Args[0]).Underlying()) { + limit = call.Args[0] + } + + // If the limit is a untyped constant of non-integer type, + // such as "const limit = 1e3", its effective type may + // differ between the two forms. + // In a for loop, it must be comparable with int i, + // for i := 0; i < limit; i++ + // but in a range loop it would become a float, + // for i := range limit {} + // which is a type error. We need to convert it to int + // in this case. + // + // Unfortunately go/types discards the untyped type + // (but see Untyped in golang/go#70638) so we must + // re-type check the expression to detect this case. + var beforeLimit, afterLimit string + if v := info.Types[limit].Value; v != nil { + tVar := info.TypeOf(init.Rhs[0]) + file := curFile.Node().(*ast.File) + // TODO(mkalil): use a types.Qualifier that respects the existing + // imports of this file that are visible (not shadowed) at the current position. + qual := typesinternal.FileQualifier(file, pass.Pkg) + beforeLimit, afterLimit = fmt.Sprintf("%s(", types.TypeString(tVar, qual)), ")" + info2 := &types.Info{Types: make(map[ast.Expr]types.TypeAndValue)} + if types.CheckExpr(pass.Fset, pass.Pkg, limit.Pos(), limit, info2) == nil { + tLimit := types.Default(info2.TypeOf(limit)) + if types.AssignableTo(tLimit, tVar) { + beforeLimit, afterLimit = "", "" + } + } + } + + pass.Report(analysis.Diagnostic{ + Pos: init.Pos(), + End: inc.End(), + Message: "for loop can be modernized using range over int", + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("Replace for loop with range %s", + astutil.Format(pass.Fset, limit)), + TextEdits: append(edits, []analysis.TextEdit{ + // for i := 0; i < limit; i++ {} + // ----- --- + // ------- + // for i := range limit {} + + // Delete init. + { + Pos: init.Rhs[0].Pos(), + End: limit.Pos(), + NewText: []byte("range "), + }, + // Add "int(" before limit, if needed. + { + Pos: limit.Pos(), + End: limit.Pos(), + NewText: []byte(beforeLimit), + }, + // Delete inc. + { + Pos: limit.End(), + End: inc.End(), + }, + // Add ")" after limit, if needed. + { + Pos: limit.End(), + End: limit.End(), + NewText: []byte(afterLimit), + }, + }...), + }}, + }) + } + } + } + } + } + return nil, nil +} + +// isScalarLvalue reports whether the specified identifier is +// address-taken or appears on the left side of an assignment. +// +// This function is valid only for scalars (x = ...), +// not for aggregates (x.a[i] = ...) +func isScalarLvalue(info *types.Info, curId inspector.Cursor) bool { + // Unfortunately we can't simply use info.Types[e].Assignable() + // as it is always true for a variable even when that variable is + // used only as an r-value. So we must inspect enclosing syntax. + + cur := curId + + // Strip enclosing parens. + ek, _ := cur.ParentEdge() + for ek == edge.ParenExpr_X { + cur = cur.Parent() + ek, _ = cur.ParentEdge() + } + + switch ek { + case edge.AssignStmt_Lhs: + assign := cur.Parent().Node().(*ast.AssignStmt) + if assign.Tok != token.DEFINE { + return true // i = j or i += j + } + id := curId.Node().(*ast.Ident) + if v, ok := info.Defs[id]; ok && v.Pos() != id.Pos() { + return true // reassignment of i (i, j := 1, 2) + } + case edge.IncDecStmt_X: + return true // i++, i-- + case edge.UnaryExpr_X: + if cur.Parent().Node().(*ast.UnaryExpr).Op == token.AND { + return true // &i + } + } + return false +} diff --git a/vendor/golang.org/x/tools/go/analysis/passes/modernize/reflect.go b/vendor/golang.org/x/tools/go/analysis/passes/modernize/reflect.go new file mode 100644 index 00000000..0fc78181 --- /dev/null +++ b/vendor/golang.org/x/tools/go/analysis/passes/modernize/reflect.go @@ -0,0 +1,139 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modernize + +// This file defines modernizers that use the "reflect" package. + +import ( + "go/ast" + "go/types" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/edge" + "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/analysis/analyzerutil" + typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex" + "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/refactor" + "golang.org/x/tools/internal/typesinternal" + "golang.org/x/tools/internal/typesinternal/typeindex" + "golang.org/x/tools/internal/versions" +) + +var ReflectTypeForAnalyzer = &analysis.Analyzer{ + Name: "reflecttypefor", + Doc: analyzerutil.MustExtractDoc(doc, "reflecttypefor"), + Requires: []*analysis.Analyzer{ + inspect.Analyzer, + typeindexanalyzer.Analyzer, + }, + Run: reflecttypefor, + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#reflecttypefor", +} + +func reflecttypefor(pass *analysis.Pass) (any, error) { + var ( + index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) + info = pass.TypesInfo + + reflectTypeOf = index.Object("reflect", "TypeOf") + ) + + for curCall := range index.Calls(reflectTypeOf) { + call := curCall.Node().(*ast.CallExpr) + // Have: reflect.TypeOf(expr) + + expr := call.Args[0] + if !typesinternal.NoEffects(info, expr) { + continue // don't eliminate operand: may have effects + } + + t := info.TypeOf(expr) + var edits []analysis.TextEdit + + // Special case for TypeOf((*T)(nil)).Elem(), + // needed when T is an interface type. + if astutil.IsChildOf(curCall, edge.SelectorExpr_X) { + curSel := unparenEnclosing(curCall).Parent() + if astutil.IsChildOf(curSel, edge.CallExpr_Fun) { + call2 := unparenEnclosing(curSel).Parent().Node().(*ast.CallExpr) + obj := typeutil.Callee(info, call2) + if typesinternal.IsMethodNamed(obj, "reflect", "Type", "Elem") { + if ptr, ok := t.(*types.Pointer); ok { + // Have: TypeOf(expr).Elem() where expr : *T + t = ptr.Elem() + // reflect.TypeOf(expr).Elem() + // ------- + // reflect.TypeOf(expr) + edits = []analysis.TextEdit{{ + Pos: call.End(), + End: call2.End(), + }} + } + } + } + } + + // TypeOf(x) where x has an interface type is a + // dynamic operation; don't transform it to TypeFor. + // (edits == nil means "not the Elem() special case".) + if types.IsInterface(t) && edits == nil { + continue + } + + file := astutil.EnclosingFile(curCall) + if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_22) { + continue // TypeFor requires go1.22 + } + tokFile := pass.Fset.File(file.Pos()) + + // Format the type as valid Go syntax. + // TODO(adonovan): FileQualifier needs to respect + // visibility at the current point, and either fail + // or edit the imports as needed. + qual := typesinternal.FileQualifier(file, pass.Pkg) + tstr := types.TypeString(t, qual) + + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok { + continue // e.g. reflect was dot-imported + } + + // If the call argument contains the last use + // of a variable, as in: + // var zero T + // reflect.TypeOf(zero) + // remove the declaration of that variable. + curArg0 := curCall.ChildAt(edge.CallExpr_Args, 0) + edits = append(edits, refactor.DeleteUnusedVars(index, info, tokFile, curArg0)...) + + pass.Report(analysis.Diagnostic{ + Pos: call.Fun.Pos(), + End: call.Fun.End(), + Message: "reflect.TypeOf call can be simplified using TypeFor", + SuggestedFixes: []analysis.SuggestedFix{{ + // reflect.TypeOf (...T value...) + // ------ ------------- + // reflect.TypeFor[T]( ) + Message: "Replace TypeOf by TypeFor", + TextEdits: append([]analysis.TextEdit{ + { + Pos: sel.Sel.Pos(), + End: sel.Sel.End(), + NewText: []byte("TypeFor[" + tstr + "]"), + }, + // delete (pure) argument + { + Pos: call.Lparen + 1, + End: call.Rparen, + }, + }, edits...), + }}, + }) + } + + return nil, nil +} diff --git a/vendor/golang.org/x/tools/go/analysis/passes/modernize/slices.go b/vendor/golang.org/x/tools/go/analysis/passes/modernize/slices.go new file mode 100644 index 00000000..960a4664 --- /dev/null +++ b/vendor/golang.org/x/tools/go/analysis/passes/modernize/slices.go @@ -0,0 +1,293 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modernize + +import ( + "fmt" + "go/ast" + "go/types" + "slices" + "strconv" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/analysis/analyzerutil" + "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/refactor" + "golang.org/x/tools/internal/typesinternal" + "golang.org/x/tools/internal/versions" +) + +// Warning: this analyzer is not safe to enable by default. +var AppendClippedAnalyzer = &analysis.Analyzer{ + Name: "appendclipped", + Doc: analyzerutil.MustExtractDoc(doc, "appendclipped"), + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: appendclipped, + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#appendclipped", +} + +// The appendclipped pass offers to simplify a tower of append calls: +// +// append(append(append(base, a...), b..., c...) +// +// with a call to go1.21's slices.Concat(base, a, b, c), or simpler +// replacements such as slices.Clone(a) in degenerate cases. +// +// We offer bytes.Clone in preference to slices.Clone where +// appropriate, if the package already imports "bytes"; +// their behaviors are identical. +// +// The base expression must denote a clipped slice (see [isClipped] +// for definition), otherwise the replacement might eliminate intended +// side effects to the base slice's array. +// +// Examples: +// +// append(append(append(x[:0:0], a...), b...), c...) -> slices.Concat(a, b, c) +// append(append(slices.Clip(a), b...) -> slices.Concat(a, b) +// append([]T{}, a...) -> slices.Clone(a) +// append([]string(nil), os.Environ()...) -> os.Environ() +// +// The fix does not always preserve nilness the of base slice when the +// addends (a, b, c) are all empty (see #73557). +func appendclipped(pass *analysis.Pass) (any, error) { + // Skip the analyzer in packages where its + // fixes would create an import cycle. + if within(pass, "slices", "bytes", "runtime") { + return nil, nil + } + + info := pass.TypesInfo + + // sliceArgs is a non-empty (reversed) list of slices to be concatenated. + simplifyAppendEllipsis := func(file *ast.File, call *ast.CallExpr, base ast.Expr, sliceArgs []ast.Expr) { + // Only appends whose base is a clipped slice can be simplified: + // We must conservatively assume an append to an unclipped slice + // such as append(y[:0], x...) is intended to have effects on y. + clipped, empty := clippedSlice(info, base) + if clipped == nil { + return + } + + // If any slice arg has a different type from the base + // (and thus the result) don't offer a fix, to avoid + // changing the return type, e.g: + // + // type S []int + // - x := append([]int(nil), S{}...) // x : []int + // + x := slices.Clone(S{}) // x : S + // + // We could do better by inserting an explicit generic + // instantiation: + // + // x := slices.Clone[[]int](S{}) + // + // but this is often unnecessary and unwanted, such as + // when the value is used an in assignment context that + // provides an explicit type: + // + // var x []int = slices.Clone(S{}) + baseType := info.TypeOf(base) + for _, arg := range sliceArgs { + if !types.Identical(info.TypeOf(arg), baseType) { + return + } + } + + // If the (clipped) base is empty, it may be safely ignored. + // Otherwise treat it (or its unclipped subexpression, if possible) + // as just another arg (the first) to Concat. + // + // TODO(adonovan): not so fast! If all the operands + // are empty, then the nilness of base matters, because + // append preserves nilness whereas Concat does not (#73557). + if !empty { + sliceArgs = append(sliceArgs, clipped) + } + slices.Reverse(sliceArgs) + + // TODO(adonovan): simplify sliceArgs[0] further: slices.Clone(s) -> s + + // Concat of a single (non-trivial) slice degenerates to Clone. + if len(sliceArgs) == 1 { + s := sliceArgs[0] + + // Special case for common but redundant clone of os.Environ(). + // append(zerocap, os.Environ()...) -> os.Environ() + if scall, ok := s.(*ast.CallExpr); ok { + obj := typeutil.Callee(info, scall) + if typesinternal.IsFunctionNamed(obj, "os", "Environ") { + pass.Report(analysis.Diagnostic{ + Pos: call.Pos(), + End: call.End(), + Message: "Redundant clone of os.Environ()", + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Eliminate redundant clone", + TextEdits: []analysis.TextEdit{{ + Pos: call.Pos(), + End: call.End(), + NewText: []byte(astutil.Format(pass.Fset, s)), + }}, + }}, + }) + return + } + } + + // If the slice type is []byte, and the file imports + // "bytes" but not "slices", prefer the (behaviorally + // identical) bytes.Clone for local consistency. + // https://go.dev/issue/70815#issuecomment-2671572984 + fileImports := func(path string) bool { + return slices.ContainsFunc(file.Imports, func(spec *ast.ImportSpec) bool { + value, _ := strconv.Unquote(spec.Path.Value) + return value == path + }) + } + clonepkg := cond( + types.Identical(info.TypeOf(call), byteSliceType) && + !fileImports("slices") && fileImports("bytes"), + "bytes", + "slices") + + // append(zerocap, s...) -> slices.Clone(s) or bytes.Clone(s) + // + // This is unsound if s is empty and its nilness + // differs from zerocap (#73557). + prefix, importEdits := refactor.AddImport(info, file, clonepkg, clonepkg, "Clone", call.Pos()) + message := fmt.Sprintf("Replace append with %s.Clone", clonepkg) + pass.Report(analysis.Diagnostic{ + Pos: call.Pos(), + End: call.End(), + Message: message, + SuggestedFixes: []analysis.SuggestedFix{{ + Message: message, + TextEdits: append(importEdits, []analysis.TextEdit{{ + Pos: call.Pos(), + End: call.End(), + NewText: fmt.Appendf(nil, "%sClone(%s)", prefix, astutil.Format(pass.Fset, s)), + }}...), + }}, + }) + return + } + + // append(append(append(base, a...), b..., c...) -> slices.Concat(base, a, b, c) + // + // This is unsound if all slices are empty and base is non-nil (#73557). + prefix, importEdits := refactor.AddImport(info, file, "slices", "slices", "Concat", call.Pos()) + pass.Report(analysis.Diagnostic{ + Pos: call.Pos(), + End: call.End(), + Message: "Replace append with slices.Concat", + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Replace append with slices.Concat", + TextEdits: append(importEdits, []analysis.TextEdit{{ + Pos: call.Pos(), + End: call.End(), + NewText: fmt.Appendf(nil, "%sConcat(%s)", prefix, formatExprs(pass.Fset, sliceArgs)), + }}...), + }}, + }) + } + + // Mark nested calls to append so that we don't emit diagnostics for them. + skip := make(map[*ast.CallExpr]bool) + + // Visit calls of form append(x, y...). + for curFile := range filesUsingGoVersion(pass, versions.Go1_21) { + file := curFile.Node().(*ast.File) + + for curCall := range curFile.Preorder((*ast.CallExpr)(nil)) { + call := curCall.Node().(*ast.CallExpr) + if skip[call] { + continue + } + + // Recursively unwrap ellipsis calls to append, so + // append(append(append(base, a...), b..., c...) + // yields (base, [c b a]). + base, slices := ast.Expr(call), []ast.Expr(nil) // base case: (call, nil) + again: + if call, ok := base.(*ast.CallExpr); ok { + if id, ok := call.Fun.(*ast.Ident); ok && + call.Ellipsis.IsValid() && + len(call.Args) == 2 && + info.Uses[id] == builtinAppend { + + // Have: append(base, s...) + base, slices = call.Args[0], append(slices, call.Args[1]) + skip[call] = true + goto again + } + } + + if len(slices) > 0 { + simplifyAppendEllipsis(file, call, base, slices) + } + } + } + return nil, nil +} + +// clippedSlice returns res != nil if e denotes a slice that is +// definitely clipped, that is, its len(s)==cap(s). +// +// The value of res is either the same as e or is a subexpression of e +// that denotes the same slice but without the clipping operation. +// +// In addition, it reports whether the slice is definitely empty. +// +// Examples of clipped slices: +// +// x[:0:0] (empty) +// []T(nil) (empty) +// Slice{} (empty) +// x[:len(x):len(x)] (nonempty) res=x +// x[:k:k] (nonempty) +// slices.Clip(x) (nonempty) res=x +// +// TODO(adonovan): Add a check that the expression x has no side effects in +// case x[:len(x):len(x)] -> x. Now the program behavior may change. +func clippedSlice(info *types.Info, e ast.Expr) (res ast.Expr, empty bool) { + switch e := e.(type) { + case *ast.SliceExpr: + // x[:0:0], x[:len(x):len(x)], x[:k:k] + if e.Slice3 && e.High != nil && e.Max != nil && astutil.EqualSyntax(e.High, e.Max) { // x[:k:k] + res = e + empty = isZeroIntConst(info, e.High) // x[:0:0] + if call, ok := e.High.(*ast.CallExpr); ok && + typeutil.Callee(info, call) == builtinLen && + astutil.EqualSyntax(call.Args[0], e.X) { + res = e.X // x[:len(x):len(x)] -> x + } + return + } + return + + case *ast.CallExpr: + // []T(nil)? + if info.Types[e.Fun].IsType() && + is[*ast.Ident](e.Args[0]) && + info.Uses[e.Args[0].(*ast.Ident)] == builtinNil { + return e, true + } + + // slices.Clip(x)? + obj := typeutil.Callee(info, e) + if typesinternal.IsFunctionNamed(obj, "slices", "Clip") { + return e.Args[0], false // slices.Clip(x) -> x + } + + case *ast.CompositeLit: + // Slice{}? + if len(e.Elts) == 0 { + return e, true + } + } + return nil, false +} diff --git a/vendor/golang.org/x/tools/go/analysis/passes/modernize/slicescontains.go b/vendor/golang.org/x/tools/go/analysis/passes/modernize/slicescontains.go new file mode 100644 index 00000000..3b326852 --- /dev/null +++ b/vendor/golang.org/x/tools/go/analysis/passes/modernize/slicescontains.go @@ -0,0 +1,433 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modernize + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/analysis/analyzerutil" + typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex" + "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/refactor" + "golang.org/x/tools/internal/typeparams" + "golang.org/x/tools/internal/typesinternal/typeindex" + "golang.org/x/tools/internal/versions" +) + +var SlicesContainsAnalyzer = &analysis.Analyzer{ + Name: "slicescontains", + Doc: analyzerutil.MustExtractDoc(doc, "slicescontains"), + Requires: []*analysis.Analyzer{ + inspect.Analyzer, + typeindexanalyzer.Analyzer, + }, + Run: slicescontains, + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicescontains", +} + +// The slicescontains pass identifies loops that can be replaced by a +// call to slices.Contains{,Func}. For example: +// +// for i, elem := range s { +// if elem == needle { +// ... +// break +// } +// } +// +// => +// +// if slices.Contains(s, needle) { ... } +// +// Variants: +// - if the if-condition is f(elem), the replacement +// uses slices.ContainsFunc(s, f). +// - if the if-body is "return true" and the fallthrough +// statement is "return false" (or vice versa), the +// loop becomes "return [!]slices.Contains(...)". +// - if the if-body is "found = true" and the previous +// statement is "found = false" (or vice versa), the +// loop becomes "found = [!]slices.Contains(...)". +// +// It may change cardinality of effects of the "needle" expression. +// (Mostly this appears to be a desirable optimization, avoiding +// redundantly repeated evaluation.) +// +// TODO(adonovan): Add a check that needle/predicate expression from +// if-statement has no effects. Now the program behavior may change. +func slicescontains(pass *analysis.Pass) (any, error) { + // Skip the analyzer in packages where its + // fixes would create an import cycle. + if within(pass, "slices", "runtime") { + return nil, nil + } + + var ( + index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) + info = pass.TypesInfo + ) + + // check is called for each RangeStmt of this form: + // for i, elem := range s { if cond { ... } } + check := func(file *ast.File, curRange inspector.Cursor) { + rng := curRange.Node().(*ast.RangeStmt) + ifStmt := rng.Body.List[0].(*ast.IfStmt) + + // isSliceElem reports whether e denotes the + // current slice element (elem or s[i]). + isSliceElem := func(e ast.Expr) bool { + if rng.Value != nil && astutil.EqualSyntax(e, rng.Value) { + return true // "elem" + } + if x, ok := e.(*ast.IndexExpr); ok && + astutil.EqualSyntax(x.X, rng.X) && + astutil.EqualSyntax(x.Index, rng.Key) { + return true // "s[i]" + } + return false + } + + // Examine the condition for one of these forms: + // + // - if elem or s[i] == needle { ... } => Contains + // - if predicate(s[i] or elem) { ... } => ContainsFunc + var ( + funcName string // "Contains" or "ContainsFunc" + arg2 ast.Expr // second argument to func (needle or predicate) + ) + switch cond := ifStmt.Cond.(type) { + case *ast.BinaryExpr: + if cond.Op == token.EQL { + var elem ast.Expr + if isSliceElem(cond.X) { + funcName = "Contains" + elem = cond.X + arg2 = cond.Y // "if elem == needle" + } else if isSliceElem(cond.Y) { + funcName = "Contains" + elem = cond.Y + arg2 = cond.X // "if needle == elem" + } + + // Reject if elem and needle have different types. + if elem != nil { + tElem := info.TypeOf(elem) + tNeedle := info.TypeOf(arg2) + if !types.Identical(tElem, tNeedle) { + // Avoid ill-typed slices.Contains([]error, any). + if !types.AssignableTo(tNeedle, tElem) { + return + } + // TODO(adonovan): relax this check to allow + // slices.Contains([]error, error(any)), + // inserting an explicit widening conversion + // around the needle. + return + } + } + } + + case *ast.CallExpr: + if len(cond.Args) == 1 && + isSliceElem(cond.Args[0]) && + typeutil.Callee(info, cond) != nil { // not a conversion + + // Attempt to get signature + sig, isSignature := info.TypeOf(cond.Fun).(*types.Signature) + if isSignature { + // skip variadic functions + if sig.Variadic() { + return + } + + // Slice element type must match function parameter type. + var ( + tElem = typeparams.CoreType(info.TypeOf(rng.X)).(*types.Slice).Elem() + tParam = sig.Params().At(0).Type() + ) + if !types.Identical(tElem, tParam) { + return + } + } + + funcName = "ContainsFunc" + arg2 = cond.Fun // "if predicate(elem)" + } + } + if funcName == "" { + return // not a candidate for Contains{,Func} + } + + // body is the "true" body. + body := ifStmt.Body + if len(body.List) == 0 { + // (We could perhaps delete the loop entirely.) + return + } + + // Reject if the body, needle or predicate references either range variable. + usesRangeVar := func(n ast.Node) bool { + cur, ok := curRange.FindNode(n) + if !ok { + panic(fmt.Sprintf("FindNode(%T) failed", n)) + } + return uses(index, cur, info.Defs[rng.Key.(*ast.Ident)]) || + rng.Value != nil && uses(index, cur, info.Defs[rng.Value.(*ast.Ident)]) + } + if usesRangeVar(body) { + // Body uses range var "i" or "elem". + // + // (The check for "i" could be relaxed when we + // generalize this to support slices.Index; + // and the check for "elem" could be relaxed + // if "elem" can safely be replaced in the + // body by "needle".) + return + } + if usesRangeVar(arg2) { + return + } + + // Prepare slices.Contains{,Func} call. + prefix, importEdits := refactor.AddImport(info, file, "slices", "slices", funcName, rng.Pos()) + contains := fmt.Sprintf("%s%s(%s, %s)", + prefix, + funcName, + astutil.Format(pass.Fset, rng.X), + astutil.Format(pass.Fset, arg2)) + + report := func(edits []analysis.TextEdit) { + pass.Report(analysis.Diagnostic{ + Pos: rng.Pos(), + End: rng.End(), + Message: fmt.Sprintf("Loop can be simplified using slices.%s", funcName), + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Replace loop by call to slices." + funcName, + TextEdits: append(edits, importEdits...), + }}, + }) + } + + // Last statement of body must return/break out of the loop. + // + // TODO(adonovan): opt:consider avoiding FindNode with new API of form: + // curRange.Get(edge.RangeStmt_Body, -1). + // Get(edge.BodyStmt_List, 0). + // Get(edge.IfStmt_Body) + curBody, _ := curRange.FindNode(body) + curLastStmt, _ := curBody.LastChild() + + // Reject if any statement in the body except the + // last has a free continuation (continue or break) + // that might affected by melting down the loop. + // + // TODO(adonovan): relax check by analyzing branch target. + for curBodyStmt := range curBody.Children() { + if curBodyStmt != curLastStmt { + for range curBodyStmt.Preorder((*ast.BranchStmt)(nil), (*ast.ReturnStmt)(nil)) { + return + } + } + } + + switch lastStmt := curLastStmt.Node().(type) { + case *ast.ReturnStmt: + // Have: for ... range seq { if ... { stmts; return x } } + + // Special case: + // body={ return true } next="return false" (or negation) + // => return [!]slices.Contains(...) + if curNext, ok := curRange.NextSibling(); ok { + nextStmt := curNext.Node().(ast.Stmt) + tval := isReturnTrueOrFalse(info, lastStmt) + fval := isReturnTrueOrFalse(info, nextStmt) + if len(body.List) == 1 && tval*fval < 0 { + // for ... { if ... { return true/false } } + // => return [!]slices.Contains(...) + report([]analysis.TextEdit{ + // Delete the range statement and following space. + { + Pos: rng.Pos(), + End: nextStmt.Pos(), + }, + // Change return to [!]slices.Contains(...). + { + Pos: nextStmt.Pos(), + End: nextStmt.End(), + NewText: fmt.Appendf(nil, "return %s%s", + cond(tval > 0, "", "!"), + contains), + }, + }) + return + } + } + + // General case: + // => if slices.Contains(...) { stmts; return x } + report([]analysis.TextEdit{ + // Replace "for ... { if ... " with "if slices.Contains(...)". + { + Pos: rng.Pos(), + End: ifStmt.Body.Pos(), + NewText: fmt.Appendf(nil, "if %s ", contains), + }, + // Delete '}' of range statement and preceding space. + { + Pos: ifStmt.Body.End(), + End: rng.End(), + }, + }) + return + + case *ast.BranchStmt: + if lastStmt.Tok == token.BREAK && lastStmt.Label == nil { // unlabeled break + // Have: for ... { if ... { stmts; break } } + + var prevStmt ast.Stmt // previous statement to range (if any) + if curPrev, ok := curRange.PrevSibling(); ok { + // If the RangeStmt's previous sibling is a Stmt, + // the RangeStmt must be among the Body list of + // a BlockStmt, CauseClause, or CommClause. + // In all cases, the prevStmt is the immediate + // predecessor of the RangeStmt during execution. + // + // (This is not true for Stmts in general; + // see [Cursor.Children] and #71074.) + prevStmt, _ = curPrev.Node().(ast.Stmt) + } + + // Special case: + // prev="lhs = false" body={ lhs = true; break } + // => lhs = slices.Contains(...) (or its negation) + if assign, ok := body.List[0].(*ast.AssignStmt); ok && + len(body.List) == 2 && + assign.Tok == token.ASSIGN && + len(assign.Lhs) == 1 && + len(assign.Rhs) == 1 { + + // Have: body={ lhs = rhs; break } + if prevAssign, ok := prevStmt.(*ast.AssignStmt); ok && + len(prevAssign.Lhs) == 1 && + len(prevAssign.Rhs) == 1 && + astutil.EqualSyntax(prevAssign.Lhs[0], assign.Lhs[0]) && + isTrueOrFalse(info, assign.Rhs[0]) == + -isTrueOrFalse(info, prevAssign.Rhs[0]) { + + // Have: + // lhs = false + // for ... { if ... { lhs = true; break } } + // => + // lhs = slices.Contains(...) + // + // TODO(adonovan): + // - support "var lhs bool = false" and variants. + // - allow the break to be omitted. + neg := cond(isTrueOrFalse(info, assign.Rhs[0]) < 0, "!", "") + report([]analysis.TextEdit{ + // Replace "rhs" of previous assignment by [!]slices.Contains(...) + { + Pos: prevAssign.Rhs[0].Pos(), + End: prevAssign.Rhs[0].End(), + NewText: []byte(neg + contains), + }, + // Delete the loop and preceding space. + { + Pos: prevAssign.Rhs[0].End(), + End: rng.End(), + }, + }) + return + } + } + + // General case: + // for ... { if ... { stmts; break } } + // => if slices.Contains(...) { stmts } + report([]analysis.TextEdit{ + // Replace "for ... { if ... " with "if slices.Contains(...)". + { + Pos: rng.Pos(), + End: ifStmt.Body.Pos(), + NewText: fmt.Appendf(nil, "if %s ", contains), + }, + // Delete break statement and preceding space. + { + Pos: func() token.Pos { + if len(body.List) > 1 { + beforeBreak, _ := curLastStmt.PrevSibling() + return beforeBreak.Node().End() + } + return lastStmt.Pos() + }(), + End: lastStmt.End(), + }, + // Delete '}' of range statement and preceding space. + { + Pos: ifStmt.Body.End(), + End: rng.End(), + }, + }) + return + } + } + } + + for curFile := range filesUsingGoVersion(pass, versions.Go1_21) { + file := curFile.Node().(*ast.File) + + for curRange := range curFile.Preorder((*ast.RangeStmt)(nil)) { + rng := curRange.Node().(*ast.RangeStmt) + + if is[*ast.Ident](rng.Key) && + rng.Tok == token.DEFINE && + len(rng.Body.List) == 1 && + is[*types.Slice](typeparams.CoreType(info.TypeOf(rng.X))) { + + // Have: + // - for _, elem := range s { S } + // - for i := range s { S } + + if ifStmt, ok := rng.Body.List[0].(*ast.IfStmt); ok && + ifStmt.Init == nil && ifStmt.Else == nil { + + // Have: for i, elem := range s { if cond { ... } } + check(file, curRange) + } + } + } + } + return nil, nil +} + +// -- helpers -- + +// isReturnTrueOrFalse returns nonzero if stmt returns true (+1) or false (-1). +func isReturnTrueOrFalse(info *types.Info, stmt ast.Stmt) int { + if ret, ok := stmt.(*ast.ReturnStmt); ok && len(ret.Results) == 1 { + return isTrueOrFalse(info, ret.Results[0]) + } + return 0 +} + +// isTrueOrFalse returns nonzero if expr is literally true (+1) or false (-1). +func isTrueOrFalse(info *types.Info, expr ast.Expr) int { + if id, ok := expr.(*ast.Ident); ok { + switch info.Uses[id] { + case builtinTrue: + return +1 + case builtinFalse: + return -1 + } + } + return 0 +} diff --git a/vendor/golang.org/x/tools/go/analysis/passes/modernize/slicesdelete.go b/vendor/golang.org/x/tools/go/analysis/passes/modernize/slicesdelete.go new file mode 100644 index 00000000..7b3aa875 --- /dev/null +++ b/vendor/golang.org/x/tools/go/analysis/passes/modernize/slicesdelete.go @@ -0,0 +1,177 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modernize + +import ( + "go/ast" + "go/constant" + "go/token" + "go/types" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/internal/analysis/analyzerutil" + "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/refactor" + "golang.org/x/tools/internal/typesinternal" + "golang.org/x/tools/internal/versions" +) + +// Warning: this analyzer is not safe to enable by default (not nil-preserving). +var SlicesDeleteAnalyzer = &analysis.Analyzer{ + Name: "slicesdelete", + Doc: analyzerutil.MustExtractDoc(doc, "slicesdelete"), + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: slicesdelete, + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicesdelete", +} + +// The slicesdelete pass attempts to replace instances of append(s[:i], s[i+k:]...) +// with slices.Delete(s, i, i+k) where k is some positive constant. +// Other variations that will also have suggested replacements include: +// append(s[:i-1], s[i:]...) and append(s[:i+k1], s[i+k2:]) where k2 > k1. +func slicesdelete(pass *analysis.Pass) (any, error) { + // Skip the analyzer in packages where its + // fixes would create an import cycle. + if within(pass, "slices", "runtime") { + return nil, nil + } + + info := pass.TypesInfo + report := func(file *ast.File, call *ast.CallExpr, slice1, slice2 *ast.SliceExpr) { + insert := func(pos token.Pos, text string) analysis.TextEdit { + return analysis.TextEdit{Pos: pos, End: pos, NewText: []byte(text)} + } + isIntExpr := func(e ast.Expr) bool { + return types.Identical(types.Default(info.TypeOf(e)), builtinInt.Type()) + } + isIntShadowed := func() bool { + scope := info.Scopes[file].Innermost(call.Lparen) + if _, obj := scope.LookupParent("int", call.Lparen); obj != builtinInt { + return true // int type is shadowed + } + return false + } + + prefix, edits := refactor.AddImport(info, file, "slices", "slices", "Delete", call.Pos()) + // append's indices may be any integer type; slices.Delete requires int. + // Insert int conversions as needed (and if possible). + if isIntShadowed() && (!isIntExpr(slice1.High) || !isIntExpr(slice2.Low)) { + return + } + if !isIntExpr(slice1.High) { + edits = append(edits, + insert(slice1.High.Pos(), "int("), + insert(slice1.High.End(), ")"), + ) + } + if !isIntExpr(slice2.Low) { + edits = append(edits, + insert(slice2.Low.Pos(), "int("), + insert(slice2.Low.End(), ")"), + ) + } + + pass.Report(analysis.Diagnostic{ + Pos: call.Pos(), + End: call.End(), + Message: "Replace append with slices.Delete", + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Replace append with slices.Delete", + TextEdits: append(edits, []analysis.TextEdit{ + // Change name of called function. + { + Pos: call.Fun.Pos(), + End: call.Fun.End(), + NewText: []byte(prefix + "Delete"), + }, + // Delete ellipsis. + { + Pos: call.Ellipsis, + End: call.Ellipsis + token.Pos(len("...")), // delete ellipsis + }, + // Remove second slice variable name. + { + Pos: slice2.X.Pos(), + End: slice2.X.End(), + }, + // Insert after first slice variable name. + { + Pos: slice1.X.End(), + NewText: []byte(", "), + }, + // Remove brackets and colons. + { + Pos: slice1.Lbrack, + End: slice1.High.Pos(), + }, + { + Pos: slice1.Rbrack, + End: slice1.Rbrack + 1, + }, + { + Pos: slice2.Lbrack, + End: slice2.Lbrack + 1, + }, + { + Pos: slice2.Low.End(), + End: slice2.Rbrack + 1, + }, + }...), + }}, + }) + } + for curFile := range filesUsingGoVersion(pass, versions.Go1_21) { + file := curFile.Node().(*ast.File) + for curCall := range curFile.Preorder((*ast.CallExpr)(nil)) { + call := curCall.Node().(*ast.CallExpr) + if id, ok := call.Fun.(*ast.Ident); ok && len(call.Args) == 2 { + // Verify we have append with two slices and ... operator, + // the first slice has no low index and second slice has no + // high index, and not a three-index slice. + if call.Ellipsis.IsValid() && info.Uses[id] == builtinAppend { + slice1, ok1 := call.Args[0].(*ast.SliceExpr) + slice2, ok2 := call.Args[1].(*ast.SliceExpr) + if ok1 && slice1.Low == nil && !slice1.Slice3 && + ok2 && slice2.High == nil && !slice2.Slice3 && + astutil.EqualSyntax(slice1.X, slice2.X) && + typesinternal.NoEffects(info, slice1.X) && + increasingSliceIndices(info, slice1.High, slice2.Low) { + // Have append(s[:a], s[b:]...) where we can verify a < b. + report(file, call, slice1, slice2) + } + } + } + } + } + return nil, nil +} + +// Given two slice indices a and b, returns true if we can verify that a < b. +// It recognizes certain forms such as i+k1 < i+k2 where k1 < k2. +func increasingSliceIndices(info *types.Info, a, b ast.Expr) bool { + // Given an expression of the form i±k, returns (i, k) + // where k is a signed constant. Otherwise it returns (e, 0). + split := func(e ast.Expr) (ast.Expr, constant.Value) { + if binary, ok := e.(*ast.BinaryExpr); ok && (binary.Op == token.SUB || binary.Op == token.ADD) { + // Negate constants if operation is subtract instead of add + if k := info.Types[binary.Y].Value; k != nil { + return binary.X, constant.UnaryOp(binary.Op, k, 0) // i ± k + } + } + return e, constant.MakeInt64(0) + } + + // Handle case where either a or b is a constant + ak := info.Types[a].Value + bk := info.Types[b].Value + if ak != nil || bk != nil { + return ak != nil && bk != nil && constant.Compare(ak, token.LSS, bk) + } + + ai, ak := split(a) + bi, bk := split(b) + return astutil.EqualSyntax(ai, bi) && constant.Compare(ak, token.LSS, bk) +} diff --git a/vendor/golang.org/x/tools/go/analysis/passes/modernize/sortslice.go b/vendor/golang.org/x/tools/go/analysis/passes/modernize/sortslice.go new file mode 100644 index 00000000..e22b8c55 --- /dev/null +++ b/vendor/golang.org/x/tools/go/analysis/passes/modernize/sortslice.go @@ -0,0 +1,121 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modernize + +import ( + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/internal/analysis/analyzerutil" + typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex" + "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/refactor" + "golang.org/x/tools/internal/typesinternal/typeindex" + "golang.org/x/tools/internal/versions" +) + +// (Not to be confused with go/analysis/passes/sortslice.) +var SlicesSortAnalyzer = &analysis.Analyzer{ + Name: "slicessort", + Doc: analyzerutil.MustExtractDoc(doc, "slicessort"), + Requires: []*analysis.Analyzer{ + inspect.Analyzer, + typeindexanalyzer.Analyzer, + }, + Run: slicessort, + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicessort", +} + +// The slicessort pass replaces sort.Slice(slice, less) with +// slices.Sort(slice) when slice is a []T and less is a FuncLit +// equivalent to cmp.Ordered[T]. +// +// sort.Slice(s, func(i, j int) bool { return s[i] < s[j] }) +// => slices.Sort(s) +// +// There is no slices.SortStable. +// +// TODO(adonovan): support +// +// - sort.Slice(s, func(i, j int) bool { return s[i] ... s[j] }) +// -> slices.SortFunc(s, func(x, y T) int { return x ... y }) +// iff all uses of i, j can be replaced by s[i], s[j] and "<" can be replaced with cmp.Compare. +// +// - As above for sort.SliceStable -> slices.SortStableFunc. +// +// - sort.Sort(x) where x has a named slice type whose Less method is the natural order. +// -> sort.Slice(x) +func slicessort(pass *analysis.Pass) (any, error) { + // Skip the analyzer in packages where its + // fixes would create an import cycle. + if within(pass, "slices", "sort", "runtime") { + return nil, nil + } + + var ( + info = pass.TypesInfo + index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) + sortSlice = index.Object("sort", "Slice") + ) + for curCall := range index.Calls(sortSlice) { + call := curCall.Node().(*ast.CallExpr) + if lit, ok := call.Args[1].(*ast.FuncLit); ok && len(lit.Body.List) == 1 { + sig := info.Types[lit.Type].Type.(*types.Signature) + + // Have: sort.Slice(s, func(i, j int) bool { return ... }) + s := call.Args[0] + i := sig.Params().At(0) + j := sig.Params().At(1) + + if ret, ok := lit.Body.List[0].(*ast.ReturnStmt); ok { + if compare, ok := ret.Results[0].(*ast.BinaryExpr); ok && compare.Op == token.LSS { + // isIndex reports whether e is s[v]. + isIndex := func(e ast.Expr, v *types.Var) bool { + index, ok := e.(*ast.IndexExpr) + return ok && + astutil.EqualSyntax(index.X, s) && + is[*ast.Ident](index.Index) && + info.Uses[index.Index.(*ast.Ident)] == v + } + file := astutil.EnclosingFile(curCall) + if isIndex(compare.X, i) && isIndex(compare.Y, j) && + analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_21) { + // Have: sort.Slice(s, func(i, j int) bool { return s[i] < s[j] }) + + prefix, importEdits := refactor.AddImport( + info, file, "slices", "slices", "Sort", call.Pos()) + + pass.Report(analysis.Diagnostic{ + // Highlight "sort.Slice". + Pos: call.Fun.Pos(), + End: call.Fun.End(), + Message: "sort.Slice can be modernized using slices.Sort", + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Replace sort.Slice call by slices.Sort", + TextEdits: append(importEdits, []analysis.TextEdit{ + { + // Replace sort.Slice with slices.Sort. + Pos: call.Fun.Pos(), + End: call.Fun.End(), + NewText: []byte(prefix + "Sort"), + }, + { + // Eliminate FuncLit. + Pos: call.Args[0].End(), + End: call.Rparen, + }, + }...), + }}, + }) + } + } + } + } + } + return nil, nil +} diff --git a/vendor/golang.org/x/tools/go/analysis/passes/modernize/stditerators.go b/vendor/golang.org/x/tools/go/analysis/passes/modernize/stditerators.go new file mode 100644 index 00000000..cc595806 --- /dev/null +++ b/vendor/golang.org/x/tools/go/analysis/passes/modernize/stditerators.go @@ -0,0 +1,387 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modernize + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/ast/edge" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/analysis/analyzerutil" + typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex" + "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/goplsexport" + "golang.org/x/tools/internal/refactor" + "golang.org/x/tools/internal/stdlib" + "golang.org/x/tools/internal/typesinternal/typeindex" +) + +var stditeratorsAnalyzer = &analysis.Analyzer{ + Name: "stditerators", + Doc: analyzerutil.MustExtractDoc(doc, "stditerators"), + Requires: []*analysis.Analyzer{ + typeindexanalyzer.Analyzer, + }, + Run: stditerators, + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stditerators", +} + +func init() { + // Export to gopls until this is a published modernizer. + goplsexport.StdIteratorsModernizer = stditeratorsAnalyzer +} + +// stditeratorsTable records std types that have legacy T.{Len,At} +// iteration methods as well as a newer T.All method that returns an +// iter.Seq. +var stditeratorsTable = [...]struct { + pkgpath, typename, lenmethod, atmethod, itermethod, elemname string +}{ + // Example: in go/types, (*Tuple).Variables returns an + // iterator that replaces a loop over (*Tuple).{Len,At}. + // The loop variable is named "v". + {"go/types", "Interface", "NumEmbeddeds", "EmbeddedType", "EmbeddedTypes", "etyp"}, + {"go/types", "Interface", "NumExplicitMethods", "ExplicitMethod", "ExplicitMethods", "method"}, + {"go/types", "Interface", "NumMethods", "Method", "Methods", "method"}, + {"go/types", "MethodSet", "Len", "At", "Methods", "method"}, + {"go/types", "Named", "NumMethods", "Method", "Methods", "method"}, + {"go/types", "Scope", "NumChildren", "Child", "Children", "child"}, + {"go/types", "Struct", "NumFields", "Field", "Fields", "field"}, + {"go/types", "Tuple", "Len", "At", "Variables", "v"}, + {"go/types", "TypeList", "Len", "At", "Types", "t"}, + {"go/types", "TypeParamList", "Len", "At", "TypeParams", "tparam"}, + {"go/types", "Union", "Len", "Term", "Terms", "term"}, + // TODO(adonovan): support Seq2. Bonus: transform uses of both key and value. + // {"reflect", "Value", "NumFields", "Field", "Fields", "field"}, +} + +// stditerators suggests fixes to replace loops using Len/At-style +// iterator APIs by a range loop over an iterator. The set of +// participating types and methods is defined by [iteratorsTable]. +// +// Pattern: +// +// for i := 0; i < x.Len(); i++ { +// use(x.At(i)) +// } +// +// => +// +// for elem := range x.All() { +// use(elem) +// } +// +// Variant: +// +// for i := range x.Len() { ... } +// +// Note: Iterators have a dynamic cost. How do we know that +// the user hasn't intentionally chosen not to use an +// iterator for that reason? We don't want to go fix to +// undo optimizations. Do we need a suppression mechanism? +func stditerators(pass *analysis.Pass) (any, error) { + var ( + index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) + info = pass.TypesInfo + ) + + for _, row := range stditeratorsTable { + // Don't offer fixes within the package + // that defines the iterator in question. + if within(pass, row.pkgpath) { + continue + } + + var ( + lenMethod = index.Selection(row.pkgpath, row.typename, row.lenmethod) + atMethod = index.Selection(row.pkgpath, row.typename, row.atmethod) + ) + + // chooseName returns an appropriate fresh name + // for the index variable of the iterator loop + // whose body is specified. + // + // If the loop body starts with + // + // for ... { e := x.At(i); use(e) } + // + // or + // + // for ... { if e := x.At(i); cond { use(e) } } + // + // then chooseName prefers the name e and additionally + // returns the var's symbol. We'll transform this to: + // + // for e := range x.Len() { e := e; use(e) } + // + // which leaves a redundant assignment that a + // subsequent 'forvar' pass will eliminate. + chooseName := func(curBody inspector.Cursor, x ast.Expr, i *types.Var) (string, *types.Var) { + + // isVarAssign reports whether stmt has the form v := x.At(i) + // and returns the variable if so. + isVarAssign := func(stmt ast.Stmt) *types.Var { + if assign, ok := stmt.(*ast.AssignStmt); ok && + assign.Tok == token.DEFINE && + len(assign.Lhs) == 1 && + len(assign.Rhs) == 1 && + is[*ast.Ident](assign.Lhs[0]) { + // call to x.At(i)? + if call, ok := assign.Rhs[0].(*ast.CallExpr); ok && + typeutil.Callee(info, call) == atMethod && + astutil.EqualSyntax(ast.Unparen(call.Fun).(*ast.SelectorExpr).X, x) && + is[*ast.Ident](call.Args[0]) && + info.Uses[call.Args[0].(*ast.Ident)] == i { + // Have: elem := x.At(i) + id := assign.Lhs[0].(*ast.Ident) + return info.Defs[id].(*types.Var) + } + } + return nil + } + + body := curBody.Node().(*ast.BlockStmt) + if len(body.List) > 0 { + // Is body { elem := x.At(i); ... } ? + if v := isVarAssign(body.List[0]); v != nil { + return v.Name(), v + } + + // Or { if elem := x.At(i); cond { ... } } ? + if ifstmt, ok := body.List[0].(*ast.IfStmt); ok && ifstmt.Init != nil { + if v := isVarAssign(ifstmt.Init); v != nil { + return v.Name(), v + } + } + } + + loop := curBody.Parent().Node() + + // Choose a fresh name only if + // (a) the preferred name is already declared here, and + // (b) there are references to it from the loop body. + // TODO(adonovan): this pattern also appears in errorsastype, + // and is wanted elsewhere; factor. + name := row.elemname + if v := lookup(info, curBody, name); v != nil { + // is it free in body? + for curUse := range index.Uses(v) { + if curBody.Contains(curUse) { + name = refactor.FreshName(info.Scopes[loop], loop.Pos(), name) + break + } + } + } + return name, nil + } + + // Process each call of x.Len(). + nextCall: + for curLenCall := range index.Calls(lenMethod) { + lenSel, ok := ast.Unparen(curLenCall.Node().(*ast.CallExpr).Fun).(*ast.SelectorExpr) + if !ok { + continue + } + // lenSel is "x.Len" + + var ( + rng analysis.Range // where to report diagnostic + curBody inspector.Cursor // loop body + indexVar *types.Var // old loop index var + elemVar *types.Var // existing "elem := x.At(i)" var, if present + elem string // name for new loop var + edits []analysis.TextEdit + ) + + // Analyze enclosing loop. + switch ek, _ := curLenCall.ParentEdge(); ek { + case edge.BinaryExpr_Y: + // pattern 1: for i := 0; i < x.Len(); i++ { ... } + var ( + curCmp = curLenCall.Parent() + cmp = curCmp.Node().(*ast.BinaryExpr) + ) + if cmp.Op != token.LSS || + !astutil.IsChildOf(curCmp, edge.ForStmt_Cond) { + continue + } + if id, ok := cmp.X.(*ast.Ident); ok { + // Have: for _; i < x.Len(); _ { ... } + var ( + v = info.Uses[id].(*types.Var) + curFor = curCmp.Parent() + loop = curFor.Node().(*ast.ForStmt) + ) + if v != isIncrementLoop(info, loop) { + continue + } + // Have: for i := 0; i < x.Len(); i++ { ... }. + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + rng = astutil.RangeOf(loop.For, loop.Post.End()) + indexVar = v + curBody = curFor.ChildAt(edge.ForStmt_Body, -1) + elem, elemVar = chooseName(curBody, lenSel.X, indexVar) + + // for i := 0; i < x.Len(); i++ { + // ---- ------- --- ----- + // for elem := range x.All() { + edits = []analysis.TextEdit{ + { + Pos: v.Pos(), + End: v.Pos() + token.Pos(len(v.Name())), + NewText: []byte(elem), + }, + { + Pos: loop.Init.(*ast.AssignStmt).Rhs[0].Pos(), + End: cmp.Y.Pos(), + NewText: []byte("range "), + }, + { + Pos: lenSel.Sel.Pos(), + End: lenSel.Sel.End(), + NewText: []byte(row.itermethod), + }, + { + Pos: curLenCall.Node().End(), + End: loop.Post.End(), + }, + } + } + + case edge.RangeStmt_X: + // pattern 2: for i := range x.Len() { ... } + var ( + curRange = curLenCall.Parent() + loop = curRange.Node().(*ast.RangeStmt) + ) + if id, ok := loop.Key.(*ast.Ident); ok && + loop.Value == nil && + loop.Tok == token.DEFINE { + // Have: for i := range x.Len() { ... } + // ~~~~~~~~~~~~~ + + rng = astutil.RangeOf(loop.Range, loop.X.End()) + indexVar = info.Defs[id].(*types.Var) + curBody = curRange.ChildAt(edge.RangeStmt_Body, -1) + elem, elemVar = chooseName(curBody, lenSel.X, indexVar) + + // for i := range x.Len() { + // ---- --- + // for elem := range x.All() { + edits = []analysis.TextEdit{ + { + Pos: loop.Key.Pos(), + End: loop.Key.End(), + NewText: []byte(elem), + }, + { + Pos: lenSel.Sel.Pos(), + End: lenSel.Sel.End(), + NewText: []byte(row.itermethod), + }, + } + } + } + + if indexVar == nil { + continue // no loop of the required form + } + + // TODO(adonovan): what about possible + // modifications of x within the loop? + // Aliasing seems to make a conservative + // treatment impossible. + + // Check that all uses of var i within loop body are x.At(i). + for curUse := range index.Uses(indexVar) { + if !curBody.Contains(curUse) { + continue + } + if ek, argidx := curUse.ParentEdge(); ek != edge.CallExpr_Args || argidx != 0 { + continue nextCall // use is not arg of call + } + curAtCall := curUse.Parent() + atCall := curAtCall.Node().(*ast.CallExpr) + if typeutil.Callee(info, atCall) != atMethod { + continue nextCall // use is not arg of call to T.At + } + atSel := ast.Unparen(atCall.Fun).(*ast.SelectorExpr) + + // Check receivers of Len, At calls match (syntactically). + if !astutil.EqualSyntax(lenSel.X, atSel.X) { + continue nextCall + } + + // At each point of use, check that + // the fresh variable is not shadowed + // by an intervening local declaration + // (or by the idiomatic elemVar optionally + // found by chooseName). + if obj := lookup(info, curAtCall, elem); obj != nil && obj != elemVar && obj.Pos() > indexVar.Pos() { + // (Ideally, instead of giving up, we would + // embellish the name and try again.) + continue nextCall + } + + // use(x.At(i)) + // ------- + // use(elem ) + edits = append(edits, analysis.TextEdit{ + Pos: atCall.Pos(), + End: atCall.End(), + NewText: []byte(elem), + }) + } + + // Check file Go version is new enough for the iterator method. + // (In the long run, version filters are not highly selective, + // so there's no need to do them first, especially as this check + // may be somewhat expensive.) + if v, ok := methodGoVersion(row.pkgpath, row.typename, row.itermethod); !ok { + panic("no version found") + } else if !analyzerutil.FileUsesGoVersion(pass, astutil.EnclosingFile(curLenCall), v.String()) { + continue nextCall + } + + pass.Report(analysis.Diagnostic{ + Pos: rng.Pos(), + End: rng.End(), + Message: fmt.Sprintf("%s/%s loop can simplified using %s.%s iteration", + row.lenmethod, row.atmethod, row.typename, row.itermethod), + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf( + "Replace %s/%s loop with %s.%s iteration", + row.lenmethod, row.atmethod, row.typename, row.itermethod), + TextEdits: edits, + }}, + }) + } + } + return nil, nil +} + +// -- helpers -- + +// methodGoVersion reports the version at which the method +// (pkgpath.recvtype).method appeared in the standard library. +func methodGoVersion(pkgpath, recvtype, method string) (stdlib.Version, bool) { + // TODO(adonovan): opt: this might be inefficient for large packages + // like go/types. If so, memoize using a map (and kill two birds with + // one stone by also memoizing the 'within' check above). + for _, sym := range stdlib.PackageSymbols[pkgpath] { + if sym.Kind == stdlib.Method { + _, recv, name := sym.SplitMethod() + if recv == recvtype && name == method { + return sym.Version, true + } + } + } + return 0, false +} diff --git a/vendor/golang.org/x/tools/go/analysis/passes/modernize/stringsbuilder.go b/vendor/golang.org/x/tools/go/analysis/passes/modernize/stringsbuilder.go new file mode 100644 index 00000000..56c5d0e3 --- /dev/null +++ b/vendor/golang.org/x/tools/go/analysis/passes/modernize/stringsbuilder.go @@ -0,0 +1,324 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modernize + +import ( + "fmt" + "go/ast" + "go/constant" + "go/token" + "go/types" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/edge" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/analysis/analyzerutil" + typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex" + "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/refactor" + "golang.org/x/tools/internal/typesinternal" + "golang.org/x/tools/internal/typesinternal/typeindex" +) + +var StringsBuilderAnalyzer = &analysis.Analyzer{ + Name: "stringsbuilder", + Doc: analyzerutil.MustExtractDoc(doc, "stringsbuilder"), + Requires: []*analysis.Analyzer{ + inspect.Analyzer, + typeindexanalyzer.Analyzer, + }, + Run: stringsbuilder, + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringbuilder", +} + +// stringsbuilder replaces string += string in a loop by strings.Builder. +func stringsbuilder(pass *analysis.Pass) (any, error) { + // Skip the analyzer in packages where its + // fixes would create an import cycle. + if within(pass, "strings", "runtime") { + return nil, nil + } + + var ( + inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) + ) + + // Gather all local string variables that appear on the + // LHS of some string += string assignment. + candidates := make(map[*types.Var]bool) + for curAssign := range inspect.Root().Preorder((*ast.AssignStmt)(nil)) { + assign := curAssign.Node().(*ast.AssignStmt) + if assign.Tok == token.ADD_ASSIGN && is[*ast.Ident](assign.Lhs[0]) { + if v, ok := pass.TypesInfo.Uses[assign.Lhs[0].(*ast.Ident)].(*types.Var); ok && + !typesinternal.IsPackageLevel(v) && // TODO(adonovan): in go1.25, use v.Kind() == types.LocalVar && + types.Identical(v.Type(), builtinString.Type()) { + candidates[v] = true + } + } + } + + // Now check each candidate variable's decl and uses. +nextcand: + for v := range candidates { + var edits []analysis.TextEdit + + // Check declaration of s: + // + // s := expr + // var s [string] [= expr] + // + // and transform to: + // + // var s strings.Builder; s.WriteString(expr) + // + def, ok := index.Def(v) + if !ok { + continue + } + ek, _ := def.ParentEdge() + if ek == edge.AssignStmt_Lhs && + len(def.Parent().Node().(*ast.AssignStmt).Lhs) == 1 { + // Have: s := expr + // => var s strings.Builder; s.WriteString(expr) + + assign := def.Parent().Node().(*ast.AssignStmt) + + // Reject "if s := f(); ..." since in that context + // we can't replace the assign with two statements. + switch def.Parent().Parent().Node().(type) { + case *ast.BlockStmt, *ast.CaseClause, *ast.CommClause: + // OK: these are the parts of syntax that + // allow unrestricted statement lists. + default: + continue + } + + // Add strings import. + prefix, importEdits := refactor.AddImport( + pass.TypesInfo, astutil.EnclosingFile(def), "strings", "strings", "Builder", v.Pos()) + edits = append(edits, importEdits...) + + if isEmptyString(pass.TypesInfo, assign.Rhs[0]) { + // s := "" + // --------------------- + // var s strings.Builder + edits = append(edits, analysis.TextEdit{ + Pos: assign.Pos(), + End: assign.End(), + NewText: fmt.Appendf(nil, "var %[1]s %[2]sBuilder", v.Name(), prefix), + }) + + } else { + // s := expr + // ------------------------------------- - + // var s strings.Builder; s.WriteString(expr) + edits = append(edits, []analysis.TextEdit{ + { + Pos: assign.Pos(), + End: assign.Rhs[0].Pos(), + NewText: fmt.Appendf(nil, "var %[1]s %[2]sBuilder; %[1]s.WriteString(", v.Name(), prefix), + }, + { + Pos: assign.End(), + End: assign.End(), + NewText: []byte(")"), + }, + }...) + + } + + } else if ek == edge.ValueSpec_Names && + len(def.Parent().Node().(*ast.ValueSpec).Names) == 1 { + // Have: var s [string] [= expr] + // => var s strings.Builder; s.WriteString(expr) + + // Add strings import. + prefix, importEdits := refactor.AddImport( + pass.TypesInfo, astutil.EnclosingFile(def), "strings", "strings", "Builder", v.Pos()) + edits = append(edits, importEdits...) + + spec := def.Parent().Node().(*ast.ValueSpec) + decl := def.Parent().Parent().Node().(*ast.GenDecl) + + init := spec.Names[0].End() // start of " = expr" + if spec.Type != nil { + init = spec.Type.End() + } + + // var s [string] + // ---------------- + // var s strings.Builder + edits = append(edits, analysis.TextEdit{ + Pos: spec.Names[0].End(), + End: init, + NewText: fmt.Appendf(nil, " %sBuilder", prefix), + }) + + if len(spec.Values) > 0 && !isEmptyString(pass.TypesInfo, spec.Values[0]) { + // = expr + // ---------------- - + // ; s.WriteString(expr) + edits = append(edits, []analysis.TextEdit{ + { + Pos: init, + End: spec.Values[0].Pos(), + NewText: fmt.Appendf(nil, "; %s.WriteString(", v.Name()), + }, + { + Pos: decl.End(), + End: decl.End(), + NewText: []byte(")"), + }, + }...) + } else { + // delete "= expr" + edits = append(edits, analysis.TextEdit{ + Pos: init, + End: spec.End(), + }) + } + + } else { + continue + } + + // Check uses of s. + // + // - All uses of s except the final one must be of the form + // + // s += expr + // + // Each of these will become s.WriteString(expr). + // At least one of them must be in an intervening loop + // w.r.t. the declaration of s: + // + // var s string + // for ... { s += expr } + // + // - The final use of s must be as an rvalue (e.g. use(s), not &s). + // This will become s.String(). + // + // Perhaps surprisingly, it is fine for there to be an + // intervening loop or lambda w.r.t. the declaration of s: + // + // var s strings.Builder + // for range kSmall { s.WriteString(expr) } + // for range kLarge { use(s.String()) } // called repeatedly + // + // Even though that might cause the s.String() operation to be + // executed repeatedly, this is not a deoptimization because, + // by design, (*strings.Builder).String does not allocate. + var ( + numLoopAssigns int // number of += assignments within a loop + loopAssign *ast.AssignStmt // first += assignment within a loop + seenRvalueUse bool // => we've seen the sole final use of s as an rvalue + ) + for curUse := range index.Uses(v) { + // Strip enclosing parens around Ident. + ek, _ := curUse.ParentEdge() + for ek == edge.ParenExpr_X { + curUse = curUse.Parent() + ek, _ = curUse.ParentEdge() + } + + // The rvalueUse must be the lexically last use. + if seenRvalueUse { + continue nextcand + } + + // intervening reports whether cur has an ancestor of + // one of the given types that is within the scope of v. + intervening := func(types ...ast.Node) bool { + for cur := range curUse.Enclosing(types...) { + if v.Pos() <= cur.Node().Pos() { // in scope of v + return true + } + } + return false + } + + if ek == edge.AssignStmt_Lhs { + assign := curUse.Parent().Node().(*ast.AssignStmt) + if assign.Tok != token.ADD_ASSIGN { + continue nextcand + } + // Have: s += expr + + // At least one of the += operations + // must appear within a loop. + // relative to the declaration of s. + if intervening((*ast.ForStmt)(nil), (*ast.RangeStmt)(nil)) { + numLoopAssigns++ + if loopAssign == nil { + loopAssign = assign + } + } + + // s += expr + // ------------- - + // s.WriteString(expr) + edits = append(edits, []analysis.TextEdit{ + // replace += with .WriteString() + { + Pos: assign.TokPos, + End: assign.Rhs[0].Pos(), + NewText: []byte(".WriteString("), + }, + // insert ")" + { + Pos: assign.End(), + End: assign.End(), + NewText: []byte(")"), + }, + }...) + + } else if ek == edge.UnaryExpr_X && + curUse.Parent().Node().(*ast.UnaryExpr).Op == token.AND { + // Have: use(&s) + continue nextcand // s is used as an lvalue; reject + + } else { + // The only possible l-value uses of a string variable + // are assignments (s=expr, s+=expr, etc) and &s. + // (For strings, we can ignore method calls s.m().) + // All other uses are r-values. + seenRvalueUse = true + + edits = append(edits, analysis.TextEdit{ + // insert ".String()" + Pos: curUse.Node().End(), + End: curUse.Node().End(), + NewText: []byte(".String()"), + }) + } + } + if !seenRvalueUse { + continue nextcand // no rvalue use; reject + } + if numLoopAssigns == 0 { + continue nextcand // no += in a loop; reject + } + + pass.Report(analysis.Diagnostic{ + Pos: loopAssign.Pos(), + End: loopAssign.End(), + Message: "using string += string in a loop is inefficient", + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Replace string += string with strings.Builder", + TextEdits: edits, + }}, + }) + } + + return nil, nil +} + +// isEmptyString reports whether e (a string-typed expression) has constant value "". +func isEmptyString(info *types.Info, e ast.Expr) bool { + tv, ok := info.Types[e] + return ok && tv.Value != nil && constant.StringVal(tv.Value) == "" +} diff --git a/vendor/golang.org/x/tools/go/analysis/passes/modernize/stringscut.go b/vendor/golang.org/x/tools/go/analysis/passes/modernize/stringscut.go new file mode 100644 index 00000000..521c264c --- /dev/null +++ b/vendor/golang.org/x/tools/go/analysis/passes/modernize/stringscut.go @@ -0,0 +1,580 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modernize + +import ( + "fmt" + "go/ast" + "go/constant" + "go/token" + "go/types" + "iter" + "strconv" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/edge" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/analysis/analyzerutil" + typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex" + "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/goplsexport" + "golang.org/x/tools/internal/refactor" + "golang.org/x/tools/internal/typesinternal" + "golang.org/x/tools/internal/typesinternal/typeindex" + "golang.org/x/tools/internal/versions" +) + +var stringscutAnalyzer = &analysis.Analyzer{ + Name: "stringscut", + Doc: analyzerutil.MustExtractDoc(doc, "stringscut"), + Requires: []*analysis.Analyzer{ + inspect.Analyzer, + typeindexanalyzer.Analyzer, + }, + Run: stringscut, + URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize#stringscut", +} + +func init() { + // Export to gopls until this is a published modernizer. + goplsexport.StringsCutModernizer = stringscutAnalyzer +} + +// stringscut offers a fix to replace an occurrence of strings.Index{,Byte} with +// strings.{Cut,Contains}, and similar fixes for functions in the bytes package. +// Consider some candidate for replacement i := strings.Index(s, substr). +// The following must hold for a replacement to occur: +// +// 1. All instances of i and s must be in one of these forms. +// Binary expressions: +// (a): establishing that i < 0: e.g.: i < 0, 0 > i, i == -1, -1 == i +// (b): establishing that i > -1: e.g.: i >= 0, 0 <= i, i == 0, 0 == i +// +// Slice expressions: +// a: s[:i], s[0:i] +// b: s[i+len(substr):], s[len(substr) + i:], s[i + const], s[k + i] (where k = len(substr)) +// +// 2. There can be no uses of s, substr, or i where they are +// potentially modified (i.e. in assignments, or function calls with unknown side +// effects). +// +// Then, the replacement involves the following substitutions: +// +// 1. Replace "i := strings.Index(s, substr)" with "before, after, ok := strings.Cut(s, substr)" +// +// 2. Replace instances of binary expressions (a) with !ok and binary expressions (b) with ok. +// +// 3. Replace slice expressions (a) with "before" and slice expressions (b) with after. +// +// 4. The assignments to before, after, and ok may use the blank identifier "_" if they are unused. +// +// For example: +// +// i := strings.Index(s, substr) +// if i >= 0 { +// use(s[:i], s[i+len(substr):]) +// } +// +// Would become: +// +// before, after, ok := strings.Cut(s, substr) +// if ok { +// use(before, after) +// } +// +// If the condition involving `i` establishes that i > -1, then we replace it with +// `if ok“. Variants listed above include i >= 0, i > 0, and i == 0. +// If the condition is negated (e.g. establishes `i < 0`), we use `if !ok` instead. +// If the slices of `s` match `s[:i]` or `s[i+len(substr):]` or their variants listed above, +// then we replace them with before and after. +// +// When the index `i` is used only to check for the presence of the substring or byte slice, +// the suggested fix uses Contains() instead of Cut. +// +// For example: +// +// i := strings.Index(s, substr) +// if i >= 0 { +// return +// } +// +// Would become: +// +// found := strings.Contains(s, substr) +// if found { +// return +// } +func stringscut(pass *analysis.Pass) (any, error) { + var ( + index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) + info = pass.TypesInfo + + stringsIndex = index.Object("strings", "Index") + stringsIndexByte = index.Object("strings", "IndexByte") + bytesIndex = index.Object("bytes", "Index") + bytesIndexByte = index.Object("bytes", "IndexByte") + ) + + for _, obj := range []types.Object{ + stringsIndex, + stringsIndexByte, + bytesIndex, + bytesIndexByte, + } { + // (obj may be nil) + nextcall: + for curCall := range index.Calls(obj) { + // Check file version. + if !analyzerutil.FileUsesGoVersion(pass, astutil.EnclosingFile(curCall), versions.Go1_18) { + continue // strings.Index not available in this file + } + indexCall := curCall.Node().(*ast.CallExpr) // the call to strings.Index, etc. + obj := typeutil.Callee(info, indexCall) + if obj == nil { + continue + } + + var iIdent *ast.Ident // defining identifier of i var + switch ek, idx := curCall.ParentEdge(); ek { + case edge.ValueSpec_Values: + // Have: var i = strings.Index(...) + curName := curCall.Parent().ChildAt(edge.ValueSpec_Names, idx) + iIdent = curName.Node().(*ast.Ident) + case edge.AssignStmt_Rhs: + // Have: i := strings.Index(...) + // (Must be i's definition.) + curLhs := curCall.Parent().ChildAt(edge.AssignStmt_Lhs, idx) + iIdent, _ = curLhs.Node().(*ast.Ident) // may be nil + } + + if iIdent == nil { + continue + } + // Inv: iIdent is i's definition. The following would be skipped: 'var i int; i = strings.Index(...)' + // Get uses of i. + iObj := info.ObjectOf(iIdent) + if iObj == nil { + continue + } + + var ( + s = indexCall.Args[0] + substr = indexCall.Args[1] + ) + + // Check that there are no statements that alter the value of s + // or substr after the call to Index(). + if !indexArgValid(info, index, s, indexCall.Pos()) || + !indexArgValid(info, index, substr, indexCall.Pos()) { + continue nextcall + } + + // Next, examine all uses of i. If the only uses are of the + // forms mentioned above (e.g. i < 0, i >= 0, s[:i] and s[i + + // len(substr)]), then we can replace the call to Index() + // with a call to Cut() and use the returned ok, before, + // and after variables accordingly. + lessZero, greaterNegOne, beforeSlice, afterSlice := checkIdxUses(pass.TypesInfo, index.Uses(iObj), s, substr) + + // Either there are no uses of before, after, or ok, or some use + // of i does not match our criteria - don't suggest a fix. + if lessZero == nil && greaterNegOne == nil && beforeSlice == nil && afterSlice == nil { + continue + } + + // If the only uses are ok and !ok, don't suggest a Cut() fix - these should be using Contains() + isContains := (len(lessZero) > 0 || len(greaterNegOne) > 0) && len(beforeSlice) == 0 && len(afterSlice) == 0 + + scope := iObj.Parent() + var ( + // TODO(adonovan): avoid FreshName when not needed; see errorsastype. + okVarName = refactor.FreshName(scope, iIdent.Pos(), "ok") + beforeVarName = refactor.FreshName(scope, iIdent.Pos(), "before") + afterVarName = refactor.FreshName(scope, iIdent.Pos(), "after") + foundVarName = refactor.FreshName(scope, iIdent.Pos(), "found") // for Contains() + ) + + // If there will be no uses of ok, before, or after, use the + // blank identifier instead. + if len(lessZero) == 0 && len(greaterNegOne) == 0 { + okVarName = "_" + } + if len(beforeSlice) == 0 { + beforeVarName = "_" + } + if len(afterSlice) == 0 { + afterVarName = "_" + } + + var edits []analysis.TextEdit + replace := func(exprs []ast.Expr, new string) { + for _, expr := range exprs { + edits = append(edits, analysis.TextEdit{ + Pos: expr.Pos(), + End: expr.End(), + NewText: []byte(new), + }) + } + } + // Get the ident for the call to strings.Index, which could just be + // "Index" if the strings package is dot imported. + indexCallId := typesinternal.UsedIdent(info, indexCall.Fun) + replacedFunc := "Cut" + if isContains { + replacedFunc = "Contains" + replace(lessZero, "!"+foundVarName) // idx < 0 -> !found + replace(greaterNegOne, foundVarName) // idx > -1 -> found + + // Replace the assignment with found, and replace the call to + // Index or IndexByte with a call to Contains. + // i := strings.Index (...) + // ----- -------- + // found := strings.Contains(...) + edits = append(edits, analysis.TextEdit{ + Pos: iIdent.Pos(), + End: iIdent.End(), + NewText: []byte(foundVarName), + }, analysis.TextEdit{ + Pos: indexCallId.Pos(), + End: indexCallId.End(), + NewText: []byte("Contains"), + }) + } else { + replace(lessZero, "!"+okVarName) // idx < 0 -> !ok + replace(greaterNegOne, okVarName) // idx > -1 -> ok + replace(beforeSlice, beforeVarName) // s[:idx] -> before + replace(afterSlice, afterVarName) // s[idx+k:] -> after + + // Replace the assignment with before, after, ok, and replace + // the call to Index or IndexByte with a call to Cut. + // i := strings.Index(...) + // ----------------- ----- + // before, after, ok := strings.Cut (...) + edits = append(edits, analysis.TextEdit{ + Pos: iIdent.Pos(), + End: iIdent.End(), + NewText: fmt.Appendf(nil, "%s, %s, %s", beforeVarName, afterVarName, okVarName), + }, analysis.TextEdit{ + Pos: indexCallId.Pos(), + End: indexCallId.End(), + NewText: []byte("Cut"), + }) + } + + // Calls to IndexByte have a byte as their second arg, which + // must be converted to a string or []byte to be a valid arg for Cut/Contains. + if obj.Name() == "IndexByte" { + switch obj.Pkg().Name() { + case "strings": + searchByteVal := info.Types[substr].Value + if searchByteVal == nil { + // substr is a variable, e.g. substr := byte('b') + // use string(substr) + edits = append(edits, []analysis.TextEdit{ + { + Pos: substr.Pos(), + NewText: []byte("string("), + }, + { + Pos: substr.End(), + NewText: []byte(")"), + }, + }...) + } else { + // substr is a byte constant + val, _ := constant.Int64Val(searchByteVal) // inv: must be a valid byte + // strings.Cut/Contains requires a string, so convert byte literal to string literal; e.g. 'a' -> "a", 55 -> "7" + edits = append(edits, analysis.TextEdit{ + Pos: substr.Pos(), + End: substr.End(), + NewText: strconv.AppendQuote(nil, string(byte(val))), + }) + } + case "bytes": + // bytes.Cut/Contains requires a []byte, so wrap substr in a []byte{} + edits = append(edits, []analysis.TextEdit{ + { + Pos: substr.Pos(), + NewText: []byte("[]byte{"), + }, + { + Pos: substr.End(), + NewText: []byte("}"), + }, + }...) + } + } + pass.Report(analysis.Diagnostic{ + Pos: indexCall.Fun.Pos(), + End: indexCall.Fun.End(), + Message: fmt.Sprintf("%s.%s can be simplified using %s.%s", + obj.Pkg().Name(), obj.Name(), obj.Pkg().Name(), replacedFunc), + Category: "stringscut", + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("Simplify %s.%s call using %s.%s", obj.Pkg().Name(), obj.Name(), obj.Pkg().Name(), replacedFunc), + TextEdits: edits, + }}, + }) + } + } + + return nil, nil +} + +// indexArgValid reports whether expr is a valid strings.Index(_, _) arg +// for the transformation. An arg is valid iff it is: +// - constant; +// - a local variable with no modifying uses after the Index() call; or +// - []byte(x) where x is also valid by this definition. +// All other expressions are assumed not referentially transparent, +// so we cannot be sure that all uses are safe to replace. +func indexArgValid(info *types.Info, index *typeindex.Index, expr ast.Expr, afterPos token.Pos) bool { + tv := info.Types[expr] + if tv.Value != nil { + return true // constant + } + switch expr := expr.(type) { + case *ast.CallExpr: + return types.Identical(tv.Type, byteSliceType) && + indexArgValid(info, index, expr.Args[0], afterPos) // check s in []byte(s) + case *ast.Ident: + sObj := info.Uses[expr] + sUses := index.Uses(sObj) + return !hasModifyingUses(info, sUses, afterPos) + default: + // For now, skip instances where s or substr are not + // identifers, basic lits, or call expressions of the form + // []byte(s). + // TODO(mkalil): Handle s and substr being expressions like ptr.field[i]. + // From adonovan: We'd need to analyze s and substr to see + // whether they are referentially transparent, and if not, + // analyze all code between declaration and use and see if + // there are statements or expressions with potential side + // effects. + return false + } +} + +// checkIdxUses inspects the uses of i to make sure they match certain criteria that +// allows us to suggest a modernization. If all uses of i, s and substr match +// one of the following four valid formats, it returns a list of occurrences for +// each format. If any of the uses do not match one of the formats, return nil +// for all values, since we should not offer a replacement. +// 1. lessZero - a condition involving i establishing that i is negative (e.g. i < 0, 0 > i, i == -1, -1 == i) +// 2. greaterNegOne - a condition involving i establishing that i is non-negative (e.g. i >= 0, 0 <= i, i == 0, 0 == i) +// 3. beforeSlice - a slice of `s` that matches either s[:i], s[0:i] +// 4. afterSlice - a slice of `s` that matches one of: s[i+len(substr):], s[len(substr) + i:], s[i + const], s[k + i] (where k = len(substr)) +func checkIdxUses(info *types.Info, uses iter.Seq[inspector.Cursor], s, substr ast.Expr) (lessZero, greaterNegOne, beforeSlice, afterSlice []ast.Expr) { + use := func(cur inspector.Cursor) bool { + ek, _ := cur.ParentEdge() + n := cur.Parent().Node() + switch ek { + case edge.BinaryExpr_X, edge.BinaryExpr_Y: + check := n.(*ast.BinaryExpr) + switch checkIdxComparison(info, check) { + case -1: + lessZero = append(lessZero, check) + return true + case 1: + greaterNegOne = append(greaterNegOne, check) + return true + } + // Check does not establish that i < 0 or i > -1. + // Might be part of an outer slice expression like s[i + k] + // which requires a different check. + // Check that the thing being sliced is s and that the slice + // doesn't have a max index. + if slice, ok := cur.Parent().Parent().Node().(*ast.SliceExpr); ok && + sameObject(info, s, slice.X) && + slice.Max == nil { + if isBeforeSlice(info, ek, slice) { + beforeSlice = append(beforeSlice, slice) + return true + } else if isAfterSlice(info, ek, slice, substr) { + afterSlice = append(afterSlice, slice) + return true + } + } + case edge.SliceExpr_Low, edge.SliceExpr_High: + slice := n.(*ast.SliceExpr) + // Check that the thing being sliced is s and that the slice doesn't + // have a max index. + if sameObject(info, s, slice.X) && slice.Max == nil { + if isBeforeSlice(info, ek, slice) { + beforeSlice = append(beforeSlice, slice) + return true + } else if isAfterSlice(info, ek, slice, substr) { + afterSlice = append(afterSlice, slice) + return true + } + } + } + return false + } + + for curIdent := range uses { + if !use(curIdent) { + return nil, nil, nil, nil + } + } + return lessZero, greaterNegOne, beforeSlice, afterSlice +} + +// hasModifyingUses reports whether any of the uses involve potential +// modifications. Uses involving assignments before the "afterPos" won't be +// considered. +func hasModifyingUses(info *types.Info, uses iter.Seq[inspector.Cursor], afterPos token.Pos) bool { + for curUse := range uses { + ek, _ := curUse.ParentEdge() + if ek == edge.AssignStmt_Lhs { + if curUse.Node().Pos() <= afterPos { + continue + } + assign := curUse.Parent().Node().(*ast.AssignStmt) + if sameObject(info, assign.Lhs[0], curUse.Node().(*ast.Ident)) { + // Modifying use because we are reassigning the value of the object. + return true + } + } else if ek == edge.UnaryExpr_X && + curUse.Parent().Node().(*ast.UnaryExpr).Op == token.AND { + // Modifying use because we might be passing the object by reference (an explicit &). + // We can ignore the case where we have a method call on the expression (which + // has an implicit &) because we know the type of s and substr are strings + // which cannot have methods on them. + return true + } + } + return false +} + +// checkIdxComparison reports whether the check establishes that i is negative +// or non-negative. It returns -1 in the first case, 1 in the second, and 0 if +// we can confirm neither condition. We assume that a check passed to +// checkIdxComparison has i as one of its operands. +func checkIdxComparison(info *types.Info, check *ast.BinaryExpr) int { + // Check establishes that i is negative. + // e.g.: i < 0, 0 > i, i == -1, -1 == i + if check.Op == token.LSS && (isNegativeConst(info, check.Y) || isZeroIntConst(info, check.Y)) || //i < (0 or neg) + check.Op == token.GTR && (isNegativeConst(info, check.X) || isZeroIntConst(info, check.X)) || // (0 or neg) > i + check.Op == token.LEQ && (isNegativeConst(info, check.Y)) || //i <= (neg) + check.Op == token.GEQ && (isNegativeConst(info, check.X)) || // (neg) >= i + check.Op == token.EQL && + (isNegativeConst(info, check.X) || isNegativeConst(info, check.Y)) { // i == neg; neg == i + return -1 + } + // Check establishes that i is non-negative. + // e.g.: i >= 0, 0 <= i, i == 0, 0 == i + if check.Op == token.GTR && (isNonNegativeConst(info, check.Y) || isIntLiteral(info, check.Y, -1)) || // i > (non-neg or -1) + check.Op == token.LSS && (isNonNegativeConst(info, check.X) || isIntLiteral(info, check.X, -1)) || // (non-neg or -1) < i + check.Op == token.GEQ && isNonNegativeConst(info, check.Y) || // i >= (non-neg) + check.Op == token.LEQ && isNonNegativeConst(info, check.X) || // (non-neg) <= i + check.Op == token.EQL && + (isNonNegativeConst(info, check.X) || isNonNegativeConst(info, check.Y)) { // i == non-neg; non-neg == i + return 1 + } + return 0 +} + +// isNegativeConst returns true if the expr is a const int with value < zero. +func isNegativeConst(info *types.Info, expr ast.Expr) bool { + if tv, ok := info.Types[expr]; ok && tv.Value != nil && tv.Value.Kind() == constant.Int { + if v, ok := constant.Int64Val(tv.Value); ok { + return v < 0 + } + } + return false +} + +// isNoneNegativeConst returns true if the expr is a const int with value >= zero. +func isNonNegativeConst(info *types.Info, expr ast.Expr) bool { + if tv, ok := info.Types[expr]; ok && tv.Value != nil && tv.Value.Kind() == constant.Int { + if v, ok := constant.Int64Val(tv.Value); ok { + return v >= 0 + } + } + return false +} + +// isBeforeSlice reports whether the SliceExpr is of the form s[:i] or s[0:i]. +func isBeforeSlice(info *types.Info, ek edge.Kind, slice *ast.SliceExpr) bool { + return ek == edge.SliceExpr_High && (slice.Low == nil || isZeroIntConst(info, slice.Low)) +} + +// isAfterSlice reports whether the SliceExpr is of the form s[i+len(substr):], +// or s[i + k:] where k is a const is equal to len(substr). +func isAfterSlice(info *types.Info, ek edge.Kind, slice *ast.SliceExpr, substr ast.Expr) bool { + lowExpr, ok := slice.Low.(*ast.BinaryExpr) + if !ok || slice.High != nil { + return false + } + // Returns true if the expression is a call to len(substr). + isLenCall := func(expr ast.Expr) bool { + call, ok := expr.(*ast.CallExpr) + if !ok || len(call.Args) != 1 { + return false + } + return sameObject(info, substr, call.Args[0]) && typeutil.Callee(info, call) == builtinLen + } + + // Handle len([]byte(substr)) + if is[*ast.CallExpr](substr) { + call := substr.(*ast.CallExpr) + tv := info.Types[call.Fun] + if tv.IsType() && types.Identical(tv.Type, byteSliceType) { + // Only one arg in []byte conversion. + substr = call.Args[0] + } + } + substrLen := -1 + substrVal := info.Types[substr].Value + if substrVal != nil { + switch substrVal.Kind() { + case constant.String: + substrLen = len(constant.StringVal(substrVal)) + case constant.Int: + // constant.Value is a byte literal, e.g. bytes.IndexByte(_, 'a') + // or a numeric byte literal, e.g. bytes.IndexByte(_, 65) + substrLen = 1 + } + } + + switch ek { + case edge.BinaryExpr_X: + kVal := info.Types[lowExpr.Y].Value + if kVal == nil { + // i + len(substr) + return lowExpr.Op == token.ADD && isLenCall(lowExpr.Y) + } else { + // i + k + kInt, ok := constant.Int64Val(kVal) + return ok && substrLen == int(kInt) + } + case edge.BinaryExpr_Y: + kVal := info.Types[lowExpr.X].Value + if kVal == nil { + // len(substr) + i + return lowExpr.Op == token.ADD && isLenCall(lowExpr.X) + } else { + // k + i + kInt, ok := constant.Int64Val(kVal) + return ok && substrLen == int(kInt) + } + } + return false +} + +// sameObject reports whether we know that the expressions resolve to the same object. +func sameObject(info *types.Info, expr1, expr2 ast.Expr) bool { + if ident1, ok := expr1.(*ast.Ident); ok { + if ident2, ok := expr2.(*ast.Ident); ok { + uses1, ok1 := info.Uses[ident1] + uses2, ok2 := info.Uses[ident2] + return ok1 && ok2 && uses1 == uses2 + } + } + return false +} diff --git a/vendor/golang.org/x/tools/go/analysis/passes/modernize/stringscutprefix.go b/vendor/golang.org/x/tools/go/analysis/passes/modernize/stringscutprefix.go new file mode 100644 index 00000000..7dc11308 --- /dev/null +++ b/vendor/golang.org/x/tools/go/analysis/passes/modernize/stringscutprefix.go @@ -0,0 +1,257 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modernize + +import ( + "fmt" + "go/ast" + "go/token" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/analysis/analyzerutil" + typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex" + "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/refactor" + "golang.org/x/tools/internal/typesinternal" + "golang.org/x/tools/internal/typesinternal/typeindex" + "golang.org/x/tools/internal/versions" +) + +var StringsCutPrefixAnalyzer = &analysis.Analyzer{ + Name: "stringscutprefix", + Doc: analyzerutil.MustExtractDoc(doc, "stringscutprefix"), + Requires: []*analysis.Analyzer{ + inspect.Analyzer, + typeindexanalyzer.Analyzer, + }, + Run: stringscutprefix, + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringscutprefix", +} + +// stringscutprefix offers a fix to replace an if statement which +// calls to the 2 patterns below with strings.CutPrefix or strings.CutSuffix. +// +// Patterns: +// +// 1. if strings.HasPrefix(s, pre) { use(strings.TrimPrefix(s, pre) } +// => +// if after, ok := strings.CutPrefix(s, pre); ok { use(after) } +// +// 2. if after := strings.TrimPrefix(s, pre); after != s { use(after) } +// => +// if after, ok := strings.CutPrefix(s, pre); ok { use(after) } +// +// Similar patterns apply for CutSuffix. +// +// The use must occur within the first statement of the block, and the offered fix +// only replaces the first occurrence of strings.TrimPrefix/TrimSuffix. +// +// Variants: +// - bytes.HasPrefix/HasSuffix usage as pattern 1. +func stringscutprefix(pass *analysis.Pass) (any, error) { + var ( + index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) + info = pass.TypesInfo + + stringsTrimPrefix = index.Object("strings", "TrimPrefix") + bytesTrimPrefix = index.Object("bytes", "TrimPrefix") + stringsTrimSuffix = index.Object("strings", "TrimSuffix") + bytesTrimSuffix = index.Object("bytes", "TrimSuffix") + ) + if !index.Used(stringsTrimPrefix, bytesTrimPrefix, stringsTrimSuffix, bytesTrimSuffix) { + return nil, nil + } + + for curFile := range filesUsingGoVersion(pass, versions.Go1_20) { + for curIfStmt := range curFile.Preorder((*ast.IfStmt)(nil)) { + ifStmt := curIfStmt.Node().(*ast.IfStmt) + + // pattern1 + if call, ok := ifStmt.Cond.(*ast.CallExpr); ok && ifStmt.Init == nil && len(ifStmt.Body.List) > 0 { + + obj := typeutil.Callee(info, call) + if !typesinternal.IsFunctionNamed(obj, "strings", "HasPrefix", "HasSuffix") && + !typesinternal.IsFunctionNamed(obj, "bytes", "HasPrefix", "HasSuffix") { + continue + } + isPrefix := strings.HasSuffix(obj.Name(), "Prefix") + + // Replace the first occurrence of strings.TrimPrefix(s, pre) in the first statement only, + // but not later statements in case s or pre are modified by intervening logic (ditto Suffix). + firstStmt := curIfStmt.Child(ifStmt.Body).Child(ifStmt.Body.List[0]) + for curCall := range firstStmt.Preorder((*ast.CallExpr)(nil)) { + call1 := curCall.Node().(*ast.CallExpr) + obj1 := typeutil.Callee(info, call1) + // bytesTrimPrefix or stringsTrimPrefix might be nil if the file doesn't import it, + // so we need to ensure the obj1 is not nil otherwise the call1 is not TrimPrefix and cause a panic (ditto Suffix). + if obj1 == nil || + obj1 != stringsTrimPrefix && obj1 != bytesTrimPrefix && + obj1 != stringsTrimSuffix && obj1 != bytesTrimSuffix { + continue + } + + isPrefix1 := strings.HasSuffix(obj1.Name(), "Prefix") + var cutFuncName, varName, message, fixMessage string + if isPrefix && isPrefix1 { + cutFuncName = "CutPrefix" + varName = "after" + message = "HasPrefix + TrimPrefix can be simplified to CutPrefix" + fixMessage = "Replace HasPrefix/TrimPrefix with CutPrefix" + } else if !isPrefix && !isPrefix1 { + cutFuncName = "CutSuffix" + varName = "before" + message = "HasSuffix + TrimSuffix can be simplified to CutSuffix" + fixMessage = "Replace HasSuffix/TrimSuffix with CutSuffix" + } else { + continue + } + + // Have: if strings.HasPrefix(s0, pre0) { ...strings.TrimPrefix(s, pre)... } (ditto Suffix) + var ( + s0 = call.Args[0] + pre0 = call.Args[1] + s = call1.Args[0] + pre = call1.Args[1] + ) + + // check whether the obj1 uses the exact the same argument with strings.HasPrefix + // shadow variables won't be valid because we only access the first statement (ditto Suffix). + if astutil.EqualSyntax(s0, s) && astutil.EqualSyntax(pre0, pre) { + after := refactor.FreshName(info.Scopes[ifStmt], ifStmt.Pos(), varName) + prefix, importEdits := refactor.AddImport( + info, + curFile.Node().(*ast.File), + obj1.Pkg().Name(), + obj1.Pkg().Path(), + cutFuncName, + call.Pos(), + ) + okVarName := refactor.FreshName(info.Scopes[ifStmt], ifStmt.Pos(), "ok") + pass.Report(analysis.Diagnostic{ + // highlight at HasPrefix call (ditto Suffix). + Pos: call.Pos(), + End: call.End(), + Message: message, + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fixMessage, + // if strings.HasPrefix(s, pre) { use(strings.TrimPrefix(s, pre)) } + // ------------ ----------------- ----- -------------------------- + // if after, ok := strings.CutPrefix(s, pre); ok { use(after) } + // (ditto Suffix) + TextEdits: append(importEdits, []analysis.TextEdit{ + { + Pos: call.Fun.Pos(), + End: call.Fun.Pos(), + NewText: fmt.Appendf(nil, "%s, %s :=", after, okVarName), + }, + { + Pos: call.Fun.Pos(), + End: call.Fun.End(), + NewText: fmt.Appendf(nil, "%s%s", prefix, cutFuncName), + }, + { + Pos: call.End(), + End: call.End(), + NewText: fmt.Appendf(nil, "; %s ", okVarName), + }, + { + Pos: call1.Pos(), + End: call1.End(), + NewText: []byte(after), + }, + }...), + }}}, + ) + break + } + } + } + + // pattern2 + if bin, ok := ifStmt.Cond.(*ast.BinaryExpr); ok && + bin.Op == token.NEQ && + ifStmt.Init != nil && + isSimpleAssign(ifStmt.Init) { + assign := ifStmt.Init.(*ast.AssignStmt) + if call, ok := assign.Rhs[0].(*ast.CallExpr); ok && assign.Tok == token.DEFINE { + lhs := assign.Lhs[0] + obj := typeutil.Callee(info, call) + + if obj == nil || + obj != stringsTrimPrefix && obj != bytesTrimPrefix && obj != stringsTrimSuffix && obj != bytesTrimSuffix { + continue + } + + isPrefix1 := strings.HasSuffix(obj.Name(), "Prefix") + var cutFuncName, message, fixMessage string + if isPrefix1 { + cutFuncName = "CutPrefix" + message = "TrimPrefix can be simplified to CutPrefix" + fixMessage = "Replace TrimPrefix with CutPrefix" + } else { + cutFuncName = "CutSuffix" + message = "TrimSuffix can be simplified to CutSuffix" + fixMessage = "Replace TrimSuffix with CutSuffix" + } + + if astutil.EqualSyntax(lhs, bin.X) && astutil.EqualSyntax(call.Args[0], bin.Y) || + (astutil.EqualSyntax(lhs, bin.Y) && astutil.EqualSyntax(call.Args[0], bin.X)) { + // TODO(adonovan): avoid FreshName when not needed; see errorsastype. + okVarName := refactor.FreshName(info.Scopes[ifStmt], ifStmt.Pos(), "ok") + // Have one of: + // if rest := TrimPrefix(s, prefix); rest != s { (ditto Suffix) + // if rest := TrimPrefix(s, prefix); s != rest { (ditto Suffix) + + // We use AddImport not to add an import (since it exists already) + // but to compute the correct prefix in the dot-import case. + prefix, importEdits := refactor.AddImport( + info, + curFile.Node().(*ast.File), + obj.Pkg().Name(), + obj.Pkg().Path(), + cutFuncName, + call.Pos(), + ) + + pass.Report(analysis.Diagnostic{ + // highlight from the init and the condition end. + Pos: ifStmt.Init.Pos(), + End: ifStmt.Cond.End(), + Message: message, + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fixMessage, + // if x := strings.TrimPrefix(s, pre); x != s ... + // ---- ---------- ------ + // if x, ok := strings.CutPrefix (s, pre); ok ... + // (ditto Suffix) + TextEdits: append(importEdits, []analysis.TextEdit{ + { + Pos: assign.Lhs[0].End(), + End: assign.Lhs[0].End(), + NewText: fmt.Appendf(nil, ", %s", okVarName), + }, + { + Pos: call.Fun.Pos(), + End: call.Fun.End(), + NewText: fmt.Appendf(nil, "%s%s", prefix, cutFuncName), + }, + { + Pos: ifStmt.Cond.Pos(), + End: ifStmt.Cond.End(), + NewText: []byte(okVarName), + }, + }...), + }}, + }) + } + } + } + } + } + return nil, nil +} diff --git a/vendor/golang.org/x/tools/go/analysis/passes/modernize/stringsseq.go b/vendor/golang.org/x/tools/go/analysis/passes/modernize/stringsseq.go new file mode 100644 index 00000000..d02a5323 --- /dev/null +++ b/vendor/golang.org/x/tools/go/analysis/passes/modernize/stringsseq.go @@ -0,0 +1,140 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modernize + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/edge" + "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/analysis/analyzerutil" + typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex" + "golang.org/x/tools/internal/typesinternal/typeindex" + "golang.org/x/tools/internal/versions" +) + +var StringsSeqAnalyzer = &analysis.Analyzer{ + Name: "stringsseq", + Doc: analyzerutil.MustExtractDoc(doc, "stringsseq"), + Requires: []*analysis.Analyzer{ + inspect.Analyzer, + typeindexanalyzer.Analyzer, + }, + Run: stringsseq, + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringsseq", +} + +// stringsseq offers a fix to replace a call to strings.Split with +// SplitSeq or strings.Fields with FieldsSeq +// when it is the operand of a range loop, either directly: +// +// for _, line := range strings.Split() {...} +// +// or indirectly, if the variable's sole use is the range statement: +// +// lines := strings.Split() +// for _, line := range lines {...} +// +// Variants: +// - bytes.SplitSeq +// - bytes.FieldsSeq +func stringsseq(pass *analysis.Pass) (any, error) { + var ( + index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) + info = pass.TypesInfo + + stringsSplit = index.Object("strings", "Split") + stringsFields = index.Object("strings", "Fields") + bytesSplit = index.Object("bytes", "Split") + bytesFields = index.Object("bytes", "Fields") + ) + if !index.Used(stringsSplit, stringsFields, bytesSplit, bytesFields) { + return nil, nil + } + + for curFile := range filesUsingGoVersion(pass, versions.Go1_24) { + for curRange := range curFile.Preorder((*ast.RangeStmt)(nil)) { + rng := curRange.Node().(*ast.RangeStmt) + + // Reject "for i, line := ..." since SplitSeq is not an iter.Seq2. + // (We require that i is blank.) + if id, ok := rng.Key.(*ast.Ident); ok && id.Name != "_" { + continue + } + + // Find the call operand of the range statement, + // whether direct or indirect. + call, ok := rng.X.(*ast.CallExpr) + if !ok { + if id, ok := rng.X.(*ast.Ident); ok { + if v, ok := info.Uses[id].(*types.Var); ok { + if ek, idx := curRange.ParentEdge(); ek == edge.BlockStmt_List && idx > 0 { + curPrev, _ := curRange.PrevSibling() + if assign, ok := curPrev.Node().(*ast.AssignStmt); ok && + assign.Tok == token.DEFINE && + len(assign.Lhs) == 1 && + len(assign.Rhs) == 1 && + info.Defs[assign.Lhs[0].(*ast.Ident)] == v && + soleUseIs(index, v, id) { + // Have: + // lines := ... + // for _, line := range lines {...} + // and no other uses of lines. + call, _ = assign.Rhs[0].(*ast.CallExpr) + } + } + } + } + } + + if call != nil { + var edits []analysis.TextEdit + if rng.Key != nil { + // Delete (blank) RangeStmt.Key: + // for _, line := -> for line := + // for _, _ := -> for + // for _ := -> for + end := rng.Range + if rng.Value != nil { + end = rng.Value.Pos() + } + edits = append(edits, analysis.TextEdit{ + Pos: rng.Key.Pos(), + End: end, + }) + } + + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok { + continue + } + + switch obj := typeutil.Callee(info, call); obj { + case stringsSplit, stringsFields, bytesSplit, bytesFields: + oldFnName := obj.Name() + seqFnName := fmt.Sprintf("%sSeq", oldFnName) + pass.Report(analysis.Diagnostic{ + Pos: sel.Pos(), + End: sel.End(), + Message: fmt.Sprintf("Ranging over %s is more efficient", seqFnName), + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("Replace %s with %s", oldFnName, seqFnName), + TextEdits: append(edits, analysis.TextEdit{ + Pos: sel.Sel.Pos(), + End: sel.Sel.End(), + NewText: []byte(seqFnName)}), + }}, + }) + } + } + } + } + return nil, nil +} diff --git a/vendor/golang.org/x/tools/go/analysis/passes/modernize/testingcontext.go b/vendor/golang.org/x/tools/go/analysis/passes/modernize/testingcontext.go new file mode 100644 index 00000000..93933052 --- /dev/null +++ b/vendor/golang.org/x/tools/go/analysis/passes/modernize/testingcontext.go @@ -0,0 +1,250 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modernize + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + "strings" + "unicode" + "unicode/utf8" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/edge" + "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/analysis/analyzerutil" + typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex" + "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/typesinternal" + "golang.org/x/tools/internal/typesinternal/typeindex" + "golang.org/x/tools/internal/versions" +) + +var TestingContextAnalyzer = &analysis.Analyzer{ + Name: "testingcontext", + Doc: analyzerutil.MustExtractDoc(doc, "testingcontext"), + Requires: []*analysis.Analyzer{ + inspect.Analyzer, + typeindexanalyzer.Analyzer, + }, + Run: testingContext, + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#testingcontext", +} + +// The testingContext pass replaces calls to context.WithCancel from within +// tests to a use of testing.{T,B,F}.Context(), added in Go 1.24. +// +// Specifically, the testingContext pass suggests to replace: +// +// ctx, cancel := context.WithCancel(context.Background()) // or context.TODO +// defer cancel() +// +// with: +// +// ctx := t.Context() +// +// provided: +// +// - ctx and cancel are declared by the assignment +// - the deferred call is the only use of cancel +// - the call is within a test or subtest function +// - the relevant testing.{T,B,F} is named and not shadowed at the call +func testingContext(pass *analysis.Pass) (any, error) { + var ( + index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) + info = pass.TypesInfo + + contextWithCancel = index.Object("context", "WithCancel") + ) + +calls: + for cur := range index.Calls(contextWithCancel) { + call := cur.Node().(*ast.CallExpr) + // Have: context.WithCancel(...) + + arg, ok := call.Args[0].(*ast.CallExpr) + if !ok { + continue + } + if !typesinternal.IsFunctionNamed(typeutil.Callee(info, arg), "context", "Background", "TODO") { + continue + } + // Have: context.WithCancel(context.{Background,TODO}()) + + parent := cur.Parent() + assign, ok := parent.Node().(*ast.AssignStmt) + if !ok || assign.Tok != token.DEFINE { + continue + } + // Have: a, b := context.WithCancel(context.{Background,TODO}()) + + // Check that both a and b are declared, not redeclarations. + var lhs []types.Object + for _, expr := range assign.Lhs { + id, ok := expr.(*ast.Ident) + if !ok { + continue calls + } + obj, ok := info.Defs[id] + if !ok { + continue calls + } + lhs = append(lhs, obj) + } + + next, ok := parent.NextSibling() + if !ok { + continue + } + defr, ok := next.Node().(*ast.DeferStmt) + if !ok { + continue + } + deferId, ok := defr.Call.Fun.(*ast.Ident) + if !ok || !soleUseIs(index, lhs[1], deferId) { + continue // b is used elsewhere + } + // Have: + // a, b := context.WithCancel(context.{Background,TODO}()) + // defer b() + + // Check that we are in a test func. + var testObj types.Object // relevant testing.{T,B,F}, or nil + if curFunc, ok := enclosingFunc(cur); ok { + switch n := curFunc.Node().(type) { + case *ast.FuncLit: + if ek, idx := curFunc.ParentEdge(); ek == edge.CallExpr_Args && idx == 1 { + // Have: call(..., func(...) { ...context.WithCancel(...)... }) + obj := typeutil.Callee(info, curFunc.Parent().Node().(*ast.CallExpr)) + if (typesinternal.IsMethodNamed(obj, "testing", "T", "Run") || + typesinternal.IsMethodNamed(obj, "testing", "B", "Run")) && + len(n.Type.Params.List[0].Names) == 1 { + + // Have tb.Run(..., func(..., tb *testing.[TB]) { ...context.WithCancel(...)... } + testObj = info.Defs[n.Type.Params.List[0].Names[0]] + } + } + + case *ast.FuncDecl: + testObj = isTestFn(info, n) + } + } + if testObj != nil && analyzerutil.FileUsesGoVersion(pass, astutil.EnclosingFile(cur), versions.Go1_24) { + // Have a test function. Check that we can resolve the relevant + // testing.{T,B,F} at the current position. + if _, obj := lhs[0].Parent().LookupParent(testObj.Name(), lhs[0].Pos()); obj == testObj { + pass.Report(analysis.Diagnostic{ + Pos: call.Fun.Pos(), + End: call.Fun.End(), + Message: fmt.Sprintf("context.WithCancel can be modernized using %s.Context", testObj.Name()), + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("Replace context.WithCancel with %s.Context", testObj.Name()), + TextEdits: []analysis.TextEdit{{ + Pos: assign.Pos(), + End: defr.End(), + NewText: fmt.Appendf(nil, "%s := %s.Context()", lhs[0].Name(), testObj.Name()), + }}, + }}, + }) + } + } + } + return nil, nil +} + +// soleUseIs reports whether id is the sole Ident that uses obj. +// (It returns false if there were no uses of obj.) +func soleUseIs(index *typeindex.Index, obj types.Object, id *ast.Ident) bool { + empty := true + for use := range index.Uses(obj) { + empty = false + if use.Node() != id { + return false + } + } + return !empty +} + +// isTestFn checks whether fn is a test function (TestX, BenchmarkX, FuzzX), +// returning the corresponding types.Object of the *testing.{T,B,F} argument. +// Returns nil if fn is a test function, but the testing.{T,B,F} argument is +// unnamed (or _). +// +// TODO(rfindley): consider handling the case of an unnamed argument, by adding +// an edit to give the argument a name. +// +// Adapted from go/analysis/passes/tests. +// TODO(rfindley): consider refactoring to share logic. +func isTestFn(info *types.Info, fn *ast.FuncDecl) types.Object { + // Want functions with 0 results and 1 parameter. + if fn.Type.Results != nil && len(fn.Type.Results.List) > 0 || + fn.Type.Params == nil || + len(fn.Type.Params.List) != 1 || + len(fn.Type.Params.List[0].Names) != 1 { + + return nil + } + + prefix := testKind(fn.Name.Name) + if prefix == "" { + return nil + } + + if tparams := fn.Type.TypeParams; tparams != nil && len(tparams.List) > 0 { + return nil // test functions must not be generic + } + + obj := info.Defs[fn.Type.Params.List[0].Names[0]] + if obj == nil { + return nil // e.g. _ *testing.T + } + + var name string + switch prefix { + case "Test": + name = "T" + case "Benchmark": + name = "B" + case "Fuzz": + name = "F" + } + + if !typesinternal.IsPointerToNamed(obj.Type(), "testing", name) { + return nil + } + return obj +} + +// testKind returns "Test", "Benchmark", or "Fuzz" if name is a valid resp. +// test, benchmark, or fuzz function name. Otherwise, isTestName returns "". +// +// Adapted from go/analysis/passes/tests.isTestName. +func testKind(name string) string { + var prefix string + switch { + case strings.HasPrefix(name, "Test"): + prefix = "Test" + case strings.HasPrefix(name, "Benchmark"): + prefix = "Benchmark" + case strings.HasPrefix(name, "Fuzz"): + prefix = "Fuzz" + } + if prefix == "" { + return "" + } + suffix := name[len(prefix):] + if len(suffix) == 0 { + // "Test" is ok. + return prefix + } + r, _ := utf8.DecodeRuneInString(suffix) + if unicode.IsLower(r) { + return "" + } + return prefix +} diff --git a/vendor/golang.org/x/tools/go/analysis/passes/modernize/waitgroup.go b/vendor/golang.org/x/tools/go/analysis/passes/modernize/waitgroup.go new file mode 100644 index 00000000..19564c69 --- /dev/null +++ b/vendor/golang.org/x/tools/go/analysis/passes/modernize/waitgroup.go @@ -0,0 +1,172 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modernize + +import ( + "bytes" + "fmt" + "go/ast" + "go/printer" + "slices" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/analysis/analyzerutil" + typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex" + "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/refactor" + "golang.org/x/tools/internal/typesinternal/typeindex" + "golang.org/x/tools/internal/versions" +) + +var WaitGroupAnalyzer = &analysis.Analyzer{ + Name: "waitgroup", + Doc: analyzerutil.MustExtractDoc(doc, "waitgroup"), + Requires: []*analysis.Analyzer{ + inspect.Analyzer, + typeindexanalyzer.Analyzer, + }, + Run: waitgroup, + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#waitgroup", +} + +// The waitgroup pass replaces old more complex code with +// go1.25 added API WaitGroup.Go. +// +// Patterns: +// +// 1. wg.Add(1); go func() { defer wg.Done(); ... }() +// => +// wg.Go(go func() { ... }) +// +// 2. wg.Add(1); go func() { ...; wg.Done() }() +// => +// wg.Go(go func() { ... }) +// +// The wg.Done must occur within the first statement of the block in a +// defer format or last statement of the block, and the offered fix +// only removes the first/last wg.Done call. It doesn't fix existing +// wrong usage of sync.WaitGroup. +// +// The use of WaitGroup.Go in pattern 1 implicitly introduces a +// 'defer', which may change the behavior in the case of panic from +// the "..." logic. In this instance, the change is safe: before and +// after the transformation, an unhandled panic inevitably results in +// a fatal crash. The fact that the transformed code calls wg.Done() +// before the crash doesn't materially change anything. (If Done had +// other effects, or blocked, or if WaitGroup.Go propagated panics +// from child to parent goroutine, the argument would be different.) +func waitgroup(pass *analysis.Pass) (any, error) { + var ( + index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) + info = pass.TypesInfo + syncWaitGroupAdd = index.Selection("sync", "WaitGroup", "Add") + syncWaitGroupDone = index.Selection("sync", "WaitGroup", "Done") + ) + if !index.Used(syncWaitGroupDone) { + return nil, nil + } + + for curAddCall := range index.Calls(syncWaitGroupAdd) { + // Extract receiver from wg.Add call. + addCall := curAddCall.Node().(*ast.CallExpr) + if !isIntLiteral(info, addCall.Args[0], 1) { + continue // not a call to wg.Add(1) + } + // Inv: the Args[0] check ensures addCall is not of + // the form sync.WaitGroup.Add(&wg, 1). + addCallRecv := ast.Unparen(addCall.Fun).(*ast.SelectorExpr).X + + // Following statement must be go func() { ... } (). + curAddStmt := curAddCall.Parent() + if !is[*ast.ExprStmt](curAddStmt.Node()) { + continue // unnecessary parens? + } + curNext, ok := curAddCall.Parent().NextSibling() + if !ok { + continue // no successor + } + goStmt, ok := curNext.Node().(*ast.GoStmt) + if !ok { + continue // not a go stmt + } + lit, ok := goStmt.Call.Fun.(*ast.FuncLit) + if !ok || len(goStmt.Call.Args) != 0 { + continue // go argument is not func(){...}() + } + list := lit.Body.List + if len(list) == 0 { + continue + } + + // Body must start with "defer wg.Done()" or end with "wg.Done()". + var doneStmt ast.Stmt + if deferStmt, ok := list[0].(*ast.DeferStmt); ok && + typeutil.Callee(info, deferStmt.Call) == syncWaitGroupDone && + astutil.EqualSyntax(ast.Unparen(deferStmt.Call.Fun).(*ast.SelectorExpr).X, addCallRecv) { + doneStmt = deferStmt // "defer wg.Done()" + + } else if lastStmt, ok := list[len(list)-1].(*ast.ExprStmt); ok { + if doneCall, ok := lastStmt.X.(*ast.CallExpr); ok && + typeutil.Callee(info, doneCall) == syncWaitGroupDone && + astutil.EqualSyntax(ast.Unparen(doneCall.Fun).(*ast.SelectorExpr).X, addCallRecv) { + doneStmt = lastStmt // "wg.Done()" + } + } + if doneStmt == nil { + continue + } + curDoneStmt, ok := curNext.FindNode(doneStmt) + if !ok { + panic("can't find Cursor for 'done' statement") + } + + file := astutil.EnclosingFile(curAddCall) + if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_25) { + continue + } + tokFile := pass.Fset.File(file.Pos()) + + var addCallRecvText bytes.Buffer + err := printer.Fprint(&addCallRecvText, pass.Fset, addCallRecv) + if err != nil { + continue // error getting text for the edit + } + + pass.Report(analysis.Diagnostic{ + Pos: addCall.Pos(), + End: goStmt.End(), + Message: "Goroutine creation can be simplified using WaitGroup.Go", + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Simplify by using WaitGroup.Go", + TextEdits: slices.Concat( + // delete "wg.Add(1)" + refactor.DeleteStmt(tokFile, curAddStmt), + // delete "wg.Done()" or "defer wg.Done()" + refactor.DeleteStmt(tokFile, curDoneStmt), + []analysis.TextEdit{ + // go func() + // ------ + // wg.Go(func() + { + Pos: goStmt.Pos(), + End: goStmt.Call.Pos(), + NewText: fmt.Appendf(nil, "%s.Go(", addCallRecvText.String()), + }, + // ... }() + // - + // ... } ) + { + Pos: goStmt.Call.Lparen, + End: goStmt.Call.Rparen, + }, + }, + ), + }}, + }) + } + return nil, nil +} diff --git a/vendor/golang.org/x/tools/internal/goplsexport/export.go b/vendor/golang.org/x/tools/internal/goplsexport/export.go new file mode 100644 index 00000000..bca4d8a0 --- /dev/null +++ b/vendor/golang.org/x/tools/internal/goplsexport/export.go @@ -0,0 +1,16 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package goplsexport provides various backdoors to not-yet-published +// parts of x/tools that are needed by gopls. +package goplsexport + +import "golang.org/x/tools/go/analysis" + +var ( + ErrorsAsTypeModernizer *analysis.Analyzer // = modernize.errorsastypeAnalyzer + StdIteratorsModernizer *analysis.Analyzer // = modernize.stditeratorsAnalyzer + PlusBuildModernizer *analysis.Analyzer // = modernize.plusbuildAnalyzer + StringsCutModernizer *analysis.Analyzer // = modernize.stringscutAnalyzer +) diff --git a/vendor/modules.txt b/vendor/modules.txt index 425f722f..d89e35c4 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -23,6 +23,10 @@ github.com/4meepo/tagalign # github.com/Abirdcfly/dupword v0.1.7 ## explicit; go 1.24.0 github.com/Abirdcfly/dupword +# github.com/AdminBenni/iota-mixing v1.0.0 +## explicit; go 1.23.3 +github.com/AdminBenni/iota-mixing/pkg/analyzer +github.com/AdminBenni/iota-mixing/pkg/analyzer/flags # github.com/AlwxSin/noinlineerr v1.0.5 ## explicit; go 1.23.0 github.com/AlwxSin/noinlineerr @@ -50,6 +54,11 @@ github.com/Djarvur/go-err113 # github.com/Masterminds/semver/v3 v3.4.0 ## explicit; go 1.21 github.com/Masterminds/semver/v3 +# github.com/MirrexOne/unqueryvet v1.3.0 +## explicit; go 1.24.0 +github.com/MirrexOne/unqueryvet +github.com/MirrexOne/unqueryvet/internal/analyzer +github.com/MirrexOne/unqueryvet/pkg/config # github.com/OpenPeeDeeP/depguard/v2 v2.2.1 ## explicit; go 1.23.0 github.com/OpenPeeDeeP/depguard/v2 @@ -293,12 +302,31 @@ github.com/gobwas/glob/syntax/ast github.com/gobwas/glob/syntax/lexer github.com/gobwas/glob/util/runes github.com/gobwas/glob/util/strings +# github.com/godoc-lint/godoc-lint v0.10.2 +## explicit; go 1.24 +github.com/godoc-lint/godoc-lint/pkg/analysis +github.com/godoc-lint/godoc-lint/pkg/check +github.com/godoc-lint/godoc-lint/pkg/check/deprecated +github.com/godoc-lint/godoc-lint/pkg/check/max_len +github.com/godoc-lint/godoc-lint/pkg/check/no_unused_link +github.com/godoc-lint/godoc-lint/pkg/check/pkg_doc +github.com/godoc-lint/godoc-lint/pkg/check/require_doc +github.com/godoc-lint/godoc-lint/pkg/check/shared +github.com/godoc-lint/godoc-lint/pkg/check/start_with_name +github.com/godoc-lint/godoc-lint/pkg/compose +github.com/godoc-lint/godoc-lint/pkg/config +github.com/godoc-lint/godoc-lint/pkg/inspect +github.com/godoc-lint/godoc-lint/pkg/model +github.com/godoc-lint/godoc-lint/pkg/util # github.com/gofrs/flock v0.13.0 ## explicit; go 1.24.0 github.com/gofrs/flock # github.com/golang/snappy v1.0.0 ## explicit github.com/golang/snappy +# github.com/golangci/asciicheck v0.5.0 +## explicit; go 1.23.0 +github.com/golangci/asciicheck # github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 ## explicit; go 1.22.0 github.com/golangci/dupl/job @@ -313,7 +341,7 @@ github.com/golangci/go-printf-func-name/pkg/analyzer # github.com/golangci/gofmt v0.0.0-20250704145412-3e58ba0443c6 ## explicit; go 1.23.0 github.com/golangci/gofmt/gofmt -# github.com/golangci/golangci-lint/v2 v2.4.0 +# github.com/golangci/golangci-lint/v2 v2.7.0 ## explicit; go 1.24.0 github.com/golangci/golangci-lint/v2/cmd/golangci-lint github.com/golangci/golangci-lint/v2/internal/cache @@ -393,6 +421,7 @@ github.com/golangci/golangci-lint/v2/pkg/golinters/gocognit github.com/golangci/golangci-lint/v2/pkg/golinters/goconst github.com/golangci/golangci-lint/v2/pkg/golinters/gocritic github.com/golangci/golangci-lint/v2/pkg/golinters/gocyclo +github.com/golangci/golangci-lint/v2/pkg/golinters/godoclint github.com/golangci/golangci-lint/v2/pkg/golinters/godot github.com/golangci/golangci-lint/v2/pkg/golinters/godox github.com/golangci/golangci-lint/v2/pkg/golinters/gofmt @@ -414,6 +443,7 @@ github.com/golangci/golangci-lint/v2/pkg/golinters/ineffassign github.com/golangci/golangci-lint/v2/pkg/golinters/interfacebloat github.com/golangci/golangci-lint/v2/pkg/golinters/internal github.com/golangci/golangci-lint/v2/pkg/golinters/intrange +github.com/golangci/golangci-lint/v2/pkg/golinters/iotamixing github.com/golangci/golangci-lint/v2/pkg/golinters/ireturn github.com/golangci/golangci-lint/v2/pkg/golinters/lll github.com/golangci/golangci-lint/v2/pkg/golinters/loggercheck @@ -422,6 +452,7 @@ github.com/golangci/golangci-lint/v2/pkg/golinters/makezero github.com/golangci/golangci-lint/v2/pkg/golinters/mirror github.com/golangci/golangci-lint/v2/pkg/golinters/misspell github.com/golangci/golangci-lint/v2/pkg/golinters/mnd +github.com/golangci/golangci-lint/v2/pkg/golinters/modernize github.com/golangci/golangci-lint/v2/pkg/golinters/musttag github.com/golangci/golangci-lint/v2/pkg/golinters/nakedret github.com/golangci/golangci-lint/v2/pkg/golinters/nestif @@ -459,6 +490,7 @@ github.com/golangci/golangci-lint/v2/pkg/golinters/thelper github.com/golangci/golangci-lint/v2/pkg/golinters/tparallel github.com/golangci/golangci-lint/v2/pkg/golinters/unconvert github.com/golangci/golangci-lint/v2/pkg/golinters/unparam +github.com/golangci/golangci-lint/v2/pkg/golinters/unqueryvet github.com/golangci/golangci-lint/v2/pkg/golinters/unused github.com/golangci/golangci-lint/v2/pkg/golinters/usestdlibvars github.com/golangci/golangci-lint/v2/pkg/golinters/usetesting @@ -633,7 +665,7 @@ github.com/lucasb-eyer/go-colorful # github.com/macabu/inamedparam v0.2.0 ## explicit; go 1.23.0 github.com/macabu/inamedparam -# github.com/manuelarte/embeddedstructfieldcheck v0.3.0 +# github.com/manuelarte/embeddedstructfieldcheck v0.4.0 ## explicit; go 1.23.0 github.com/manuelarte/embeddedstructfieldcheck/analyzer github.com/manuelarte/embeddedstructfieldcheck/internal @@ -974,9 +1006,6 @@ github.com/stretchr/testify/require # github.com/subosito/gotenv v1.6.0 ## explicit; go 1.18 github.com/subosito/gotenv -# github.com/tdakkota/asciicheck v0.4.1 -## explicit; go 1.22.0 -github.com/tdakkota/asciicheck # github.com/termie/go-shutil v0.0.0-20140729215957-bcacb06fecae ## explicit github.com/termie/go-shutil @@ -1158,6 +1187,7 @@ golang.org/x/tools/go/analysis/passes/inspect golang.org/x/tools/go/analysis/passes/internal/ctrlflowinternal golang.org/x/tools/go/analysis/passes/loopclosure golang.org/x/tools/go/analysis/passes/lostcancel +golang.org/x/tools/go/analysis/passes/modernize golang.org/x/tools/go/analysis/passes/nilfunc golang.org/x/tools/go/analysis/passes/nilness golang.org/x/tools/go/analysis/passes/pkgfact @@ -1208,6 +1238,7 @@ golang.org/x/tools/internal/fmtstr golang.org/x/tools/internal/gcimporter golang.org/x/tools/internal/gocommand golang.org/x/tools/internal/gopathwalk +golang.org/x/tools/internal/goplsexport golang.org/x/tools/internal/imports golang.org/x/tools/internal/modindex golang.org/x/tools/internal/moreiters