Skip to content

Commit 114ac82

Browse files
adonovangopherbot
authored andcommitted
go/analysis: preparatory cleanups
This change contains some minor cleanups split out of the forthcoming three-way merge work. 1. Plumb pass.ReadFile down from a (hidden) checker option. Factor CheckedReadFile helper. 2. In "assign" checker, improve SuggestedFix title. 3. Fix bug in error handling in fix_test.go. 4. Define testenv.RedirectStderr helper to temporarily redirect stderr and log output to t.Log, to declutter the test output. Update golang/go#68765 Update golang/go#67049 Change-Id: Icac62afdeb160a2dfa3cc3637b79fe7d89e92272 Reviewed-on: https://go-review.googlesource.com/c/tools/+/643695 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Robert Findley <[email protected]> Commit-Queue: Alan Donovan <[email protected]> Auto-Submit: Alan Donovan <[email protected]>
1 parent 1c9f002 commit 114ac82

File tree

12 files changed

+101
-35
lines changed

12 files changed

+101
-35
lines changed

go/analysis/analysistest/analysistest.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns
167167
act := result.Action
168168

169169
// file -> message -> edits
170+
// TODO(adonovan): this mapping assumes fix.Messages are unique across analyzers.
170171
fileEdits := make(map[*token.File]map[string][]diff.Edit)
171172
fileContents := make(map[*token.File][]byte)
172173

@@ -179,6 +180,8 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns
179180
t.Errorf("missing Diagnostic.Category for SuggestedFix without TextEdits (gopls requires the category for the name of the fix command")
180181
}
181182

183+
// TODO(adonovan): factor in common with go/analysis/internal/checker.validateEdits.
184+
182185
for _, edit := range fix.TextEdits {
183186
start, end := edit.Pos, edit.End
184187
if !end.IsValid() {
@@ -275,7 +278,7 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns
275278
}
276279
} else {
277280
// all suggested fixes are represented by a single file
278-
281+
// TODO(adonovan): fix: this makes no sense if len(fixes) > 1.
279282
var catchallEdits []diff.Edit
280283
for _, edits := range fixes {
281284
catchallEdits = append(catchallEdits, edits...)

go/analysis/checker/checker.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"go/types"
3636
"io"
3737
"log"
38+
"os"
3839
"reflect"
3940
"sort"
4041
"strings"
@@ -55,9 +56,10 @@ type Options struct {
5556
SanityCheck bool // check fact encoding is ok and deterministic
5657
FactLog io.Writer // if non-nil, log each exported fact to it
5758

58-
// TODO(adonovan): add ReadFile so that an Overlay specified
59+
// TODO(adonovan): expose ReadFile so that an Overlay specified
5960
// in the [packages.Config] can be communicated via
6061
// Pass.ReadFile to each Analyzer.
62+
readFile analysisinternal.ReadFileFunc
6163
}
6264

6365
// Graph holds the results of a round of analysis, including the graph
@@ -344,7 +346,11 @@ func (act *Action) execOnce() {
344346
AllObjectFacts: act.AllObjectFacts,
345347
AllPackageFacts: act.AllPackageFacts,
346348
}
347-
pass.ReadFile = analysisinternal.MakeReadFile(pass)
349+
readFile := os.ReadFile
350+
if act.opts.readFile != nil {
351+
readFile = act.opts.readFile
352+
}
353+
pass.ReadFile = analysisinternal.CheckedReadFile(pass, readFile)
348354
act.pass = pass
349355

350356
act.Result, act.Err = func() (any, error) {

go/analysis/diagnostic.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ type RelatedInformation struct {
6565
// user can choose to apply to their code. Usually the SuggestedFix is
6666
// meant to fix the issue flagged by the diagnostic.
6767
//
68-
// The TextEdits must not overlap, nor contain edits for other packages.
68+
// The TextEdits must not overlap, nor contain edits for other
69+
// packages. Edits need not be totally ordered, but the order
70+
// determines how insertions at the same point will be applied.
6971
type SuggestedFix struct {
7072
// A verb phrase describing the fix, to be shown to
7173
// a user trying to decide whether to accept it.

go/analysis/internal/checker/checker.go

+4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"go/token"
2121
"io"
2222
"io/ioutil"
23+
2324
"log"
2425
"os"
2526
"runtime"
@@ -139,6 +140,9 @@ func Run(args []string, analyzers []*analysis.Analyzer) int {
139140
return 1
140141
}
141142

143+
// TODO(adonovan): simplify exit code logic by using a single
144+
// exit code variable and applying "code = max(code, X)" each
145+
// time an error of code X occurs.
142146
pkgsExitCode := 0
143147
// Print package and module errors regardless of RunDespiteErrors.
144148
// Do not exit if there are errors, yet.

go/analysis/internal/checker/checker_test.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525

2626
func TestApplyFixes(t *testing.T) {
2727
testenv.NeedsGoPackages(t)
28+
testenv.RedirectStderr(t) // associated checker.Run output with this test
2829

2930
files := map[string]string{
3031
"rename/test.go": `package rename
@@ -114,10 +115,12 @@ func run(pass *analysis.Pass) (interface{}, error) {
114115
{Pos: ident.Pos(), End: ident.End(), NewText: []byte("lorem ipsum")},
115116
}...)
116117
case duplicate:
118+
// Duplicate (non-insertion) edits are disallowed,
119+
// so this is a buggy analyzer, and validatedFixes should reject it.
117120
edits = append(edits, edits...)
118121
case other:
119122
if pass.Analyzer.Name == other {
120-
edits[0].Pos = edits[0].Pos + 1 // shift by one to mismatch analyzer and other
123+
edits[0].Pos++ // shift by one to mismatch analyzer and other
121124
}
122125
}
123126
pass.Report(analysis.Diagnostic{
@@ -133,6 +136,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
133136

134137
func TestRunDespiteErrors(t *testing.T) {
135138
testenv.NeedsGoPackages(t)
139+
testenv.RedirectStderr(t) // associate checker.Run output with this test
136140

137141
files := map[string]string{
138142
"rderr/test.go": `package rderr
@@ -360,4 +364,7 @@ hello from other
360364
if !ran {
361365
t.Error("analyzer did not run")
362366
}
367+
368+
// TODO(adonovan): test that fixes are applied to the
369+
// pass.ReadFile virtual file tree.
363370
}

go/analysis/internal/checker/fix_test.go

+16-8
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,23 @@ func fix(t *testing.T, dir, analyzers string, wantExit int, patterns ...string)
7777
return strings.ReplaceAll(s, os.TempDir(), "os.TempDir/")
7878
}
7979
outBytes, err := cmd.CombinedOutput()
80-
out := clean(string(outBytes))
81-
t.Logf("$ %s\n%s", clean(fmt.Sprint(cmd)), out)
82-
if err, ok := err.(*exec.ExitError); !ok {
83-
t.Fatalf("failed to execute multichecker: %v", err)
84-
} else if err.ExitCode() != wantExit {
85-
// plan9 ExitCode() currently only returns 0 for success or 1 for failure
86-
if !(runtime.GOOS == "plan9" && wantExit != exitCodeSuccess && err.ExitCode() != exitCodeSuccess) {
87-
t.Errorf("exit code was %d, want %d", err.ExitCode(), wantExit)
80+
switch err := err.(type) {
81+
case nil:
82+
// success
83+
case *exec.ExitError:
84+
if code := err.ExitCode(); code != wantExit {
85+
// plan9 ExitCode() currently only returns 0 for success or 1 for failure
86+
if !(runtime.GOOS == "plan9" && wantExit != exitCodeSuccess && code != exitCodeSuccess) {
87+
t.Errorf("exit code was %d, want %d", code, wantExit)
88+
}
8889
}
90+
default:
91+
t.Fatalf("failed to execute multichecker: %v", err)
8992
}
93+
94+
out := clean(string(outBytes))
95+
t.Logf("$ %s\n%s", clean(fmt.Sprint(cmd)), out)
96+
9097
return out
9198
}
9299

@@ -253,6 +260,7 @@ func Foo() {
253260
}
254261
defer cleanup()
255262

263+
// The 'rename' and 'other' analyzers suggest conflicting fixes.
256264
out := fix(t, dir, "rename,other", exitCodeFailed, "other")
257265

258266
pattern := `.*conflicting edits from other and rename on .*foo.go`

go/analysis/internal/checker/start_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
// of the file takes effect.
2323
func TestStartFixes(t *testing.T) {
2424
testenv.NeedsGoPackages(t)
25+
testenv.RedirectStderr(t) // associated checker.Run output with this test
2526

2627
files := map[string]string{
2728
"comment/doc.go": `/* Package comment */

go/analysis/passes/assign/assign.go

+6-4
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,12 @@ func run(pass *analysis.Pass) (any, error) {
6363
if le == re {
6464
pass.Report(analysis.Diagnostic{
6565
Pos: stmt.Pos(), Message: fmt.Sprintf("self-assignment of %s to %s", re, le),
66-
SuggestedFixes: []analysis.SuggestedFix{
67-
{Message: "Remove", TextEdits: []analysis.TextEdit{
68-
{Pos: stmt.Pos(), End: stmt.End(), NewText: []byte{}},
69-
}},
66+
SuggestedFixes: []analysis.SuggestedFix{{
67+
Message: "Remove self-assignment",
68+
TextEdits: []analysis.TextEdit{{
69+
Pos: stmt.Pos(),
70+
End: stmt.End(),
71+
}}},
7072
},
7173
})
7274
}

go/analysis/unitchecker/unitchecker.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]re
386386
AllPackageFacts: func() []analysis.PackageFact { return facts.AllPackageFacts(factFilter) },
387387
Module: module,
388388
}
389-
pass.ReadFile = analysisinternal.MakeReadFile(pass)
389+
pass.ReadFile = analysisinternal.CheckedReadFile(pass, os.ReadFile)
390390

391391
t0 := time.Now()
392392
act.result, act.err = a.Run(pass)

go/analysis/unitchecker/unitchecker_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ func _() {
133133
"message": "self-assignment of i to i",
134134
"suggested_fixes": \[
135135
\{
136-
"message": "Remove",
136+
"message": "Remove self-assignment",
137137
"edits": \[
138138
\{
139139
"filename": "([/._\-a-zA-Z0-9]+[\\/]fake[\\/])?c/c.go",

internal/analysisinternal/analysis.go

+10-16
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"go/scanner"
1515
"go/token"
1616
"go/types"
17-
"os"
1817
pathpkg "path"
1918
"slices"
2019
"strings"
@@ -178,20 +177,25 @@ func equivalentTypes(want, got types.Type) bool {
178177
return types.AssignableTo(want, got)
179178
}
180179

181-
// MakeReadFile returns a simple implementation of the Pass.ReadFile function.
182-
func MakeReadFile(pass *analysis.Pass) func(filename string) ([]byte, error) {
180+
// A ReadFileFunc is a function that returns the
181+
// contents of a file, such as [os.ReadFile].
182+
type ReadFileFunc = func(filename string) ([]byte, error)
183+
184+
// CheckedReadFile returns a wrapper around a Pass.ReadFile
185+
// function that performs the appropriate checks.
186+
func CheckedReadFile(pass *analysis.Pass, readFile ReadFileFunc) ReadFileFunc {
183187
return func(filename string) ([]byte, error) {
184188
if err := CheckReadable(pass, filename); err != nil {
185189
return nil, err
186190
}
187-
return os.ReadFile(filename)
191+
return readFile(filename)
188192
}
189193
}
190194

191195
// CheckReadable enforces the access policy defined by the ReadFile field of [analysis.Pass].
192196
func CheckReadable(pass *analysis.Pass, filename string) error {
193-
if slicesContains(pass.OtherFiles, filename) ||
194-
slicesContains(pass.IgnoredFiles, filename) {
197+
if slices.Contains(pass.OtherFiles, filename) ||
198+
slices.Contains(pass.IgnoredFiles, filename) {
195199
return nil
196200
}
197201
for _, f := range pass.Files {
@@ -202,16 +206,6 @@ func CheckReadable(pass *analysis.Pass, filename string) error {
202206
return fmt.Errorf("Pass.ReadFile: %s is not among OtherFiles, IgnoredFiles, or names of Files", filename)
203207
}
204208

205-
// TODO(adonovan): use go1.21 slices.Contains.
206-
func slicesContains[S ~[]E, E comparable](slice S, x E) bool {
207-
for _, elem := range slice {
208-
if elem == x {
209-
return true
210-
}
211-
}
212-
return false
213-
}
214-
215209
// AddImport checks whether this file already imports pkgpath and
216210
// that import is in scope at pos. If so, it returns the name under
217211
// which it was imported and a zero edit. Otherwise, it adds a new

internal/testenv/testenv.go

+39
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
package testenv
88

99
import (
10+
"bufio"
1011
"bytes"
1112
"context"
1213
"fmt"
1314
"go/build"
15+
"log"
1416
"os"
1517
"os/exec"
1618
"path/filepath"
@@ -553,3 +555,40 @@ func NeedsGOROOTDir(t *testing.T, dir string) {
553555
}
554556
}
555557
}
558+
559+
// RedirectStderr causes os.Stderr (and the global logger) to be
560+
// temporarily replaced so that writes to it are sent to t.Log.
561+
// It is restored at test cleanup.
562+
func RedirectStderr(t testing.TB) {
563+
t.Setenv("RedirectStderr", "") // side effect: assert t.Parallel wasn't called
564+
565+
// TODO(adonovan): if https://go.dev/issue/59928 is accepted,
566+
// simply set w = t.Output() and dispense with the pipe.
567+
r, w, err := os.Pipe()
568+
if err != nil {
569+
t.Fatalf("pipe: %v", err)
570+
}
571+
go func() {
572+
for sc := bufio.NewScanner(r); sc.Scan(); {
573+
t.Log(sc.Text())
574+
}
575+
r.Close()
576+
}()
577+
578+
// Also do the same for the global logger.
579+
savedWriter, savedPrefix, savedFlags := log.Writer(), log.Prefix(), log.Flags()
580+
log.SetPrefix("log: ")
581+
log.SetOutput(w)
582+
log.SetFlags(0)
583+
584+
oldStderr := os.Stderr
585+
os.Stderr = w
586+
t.Cleanup(func() {
587+
w.Close() // ignore error
588+
os.Stderr = oldStderr
589+
590+
log.SetOutput(savedWriter)
591+
log.SetPrefix(savedPrefix)
592+
log.SetFlags(savedFlags)
593+
})
594+
}

0 commit comments

Comments
 (0)