Skip to content

Commit 9c7c6b4

Browse files
committed
Merge remote-tracking branch 'origin/master' into gopls-release-branch.0.16
For golang/go#67936 Change-Id: Ic3981ea862fd2f8203214b29349543725d526315
2 parents 663dde6 + 1239d04 commit 9c7c6b4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1571
-799
lines changed

go/analysis/analysistest/analysistest.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,9 @@ type Testing interface {
114114
// into nonconflicting parts.
115115
//
116116
// Conflicts of the second kind can be avoided by giving the
117-
// alternative fixes different names (SuggestedFix.Message) and using
118-
// a multi-section .txtar file with a named section for each
119-
// alternative fix.
117+
// alternative fixes different names (SuggestedFix.Message) and
118+
// defining the .golden file as a multi-section txtar file with a
119+
// named section for each alternative fix, as shown above.
120120
//
121121
// Analyzers that compute fixes from a textual diff of the
122122
// before/after file contents (instead of directly from syntax tree
@@ -126,6 +126,16 @@ type Testing interface {
126126
// sufficient separation of the statements in the test input so that
127127
// the computed diffs do not overlap. If that fails, break the test
128128
// into smaller parts.
129+
//
130+
// TODO(adonovan): the behavior of RunWithSuggestedFixes as documented
131+
// above is impractical for tests that report multiple diagnostics and
132+
// offer multiple alternative fixes for the same diagnostic, and it is
133+
// inconsistent with the interpretation of multiple diagnostics
134+
// described at Diagnostic.SuggestedFixes.
135+
// We need to rethink the analyzer testing API to better support such
136+
// cases. In the meantime, users of RunWithSuggestedFixes testing
137+
// analyzers that offer alternative fixes are advised to put each fix
138+
// in a separate .go file in the testdata.
129139
func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns ...string) []*Result {
130140
r := Run(t, dir, a, patterns...)
131141

go/analysis/diagnostic.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,17 @@ type Diagnostic struct {
3333
URL string
3434

3535
// SuggestedFixes is an optional list of fixes to address the
36-
// problem described by the diagnostic, each one representing
36+
// problem described by the diagnostic. Each one represents
3737
// an alternative strategy; at most one may be applied.
38+
//
39+
// Fixes for different diagnostics should be treated as
40+
// independent changes to the same baseline file state,
41+
// analogous to a set of git commits all with the same parent.
42+
// Combining fixes requires resolving any conflicts that
43+
// arise, analogous to a git merge.
44+
// Any conflicts that remain may be dealt with, depending on
45+
// the tool, by discarding fixes, consulting the user, or
46+
// aborting the operation.
3847
SuggestedFixes []SuggestedFix
3948

4049
// Related contains optional secondary positions and messages
@@ -58,8 +67,10 @@ type RelatedInformation struct {
5867
//
5968
// The TextEdits must not overlap, nor contain edits for other packages.
6069
type SuggestedFix struct {
61-
// A description for this suggested fix to be shown to a user deciding
62-
// whether to accept it.
70+
// A verb phrase describing the fix, to be shown to
71+
// a user trying to decide whether to accept it.
72+
//
73+
// Example: "Remove the surplus argument"
6374
Message string
6475
TextEdits []TextEdit
6576
}

go/analysis/internal/checker/checker.go

Lines changed: 25 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ package checker
1111
import (
1212
"bytes"
1313
"encoding/gob"
14-
"errors"
1514
"flag"
1615
"fmt"
1716
"go/format"
@@ -134,16 +133,21 @@ func Run(args []string, analyzers []*analysis.Analyzer) (exitcode int) {
134133
allSyntax := needFacts(analyzers)
135134
initial, err := load(args, allSyntax)
136135
if err != nil {
137-
if _, ok := err.(typeParseError); !ok {
138-
// Fail when some of the errors are not
139-
// related to parsing nor typing.
140-
log.Print(err)
141-
return 1
142-
}
143-
// TODO: filter analyzers based on RunDespiteError?
136+
log.Print(err)
137+
return 1
144138
}
145139

146-
// Run the analysis.
140+
pkgsExitCode := 0
141+
// Print package errors regardless of RunDespiteErrors.
142+
// Do not exit if there are errors, yet.
143+
if n := packages.PrintErrors(initial); n > 0 {
144+
pkgsExitCode = 1
145+
}
146+
147+
// Run the analyzers. On each package with (transitive)
148+
// errors, we run only the subset of analyzers that are
149+
// marked (and whose transitive requirements are also
150+
// marked) with RunDespiteErrors.
147151
roots := analyze(initial, analyzers)
148152

149153
// Apply fixes.
@@ -155,18 +159,18 @@ func Run(args []string, analyzers []*analysis.Analyzer) (exitcode int) {
155159
}
156160
}
157161

158-
// Print the results.
159-
return printDiagnostics(roots)
160-
}
161-
162-
// typeParseError represents a package load error
163-
// that is related to typing and parsing.
164-
type typeParseError struct {
165-
error
162+
// Print the results. If !RunDespiteErrors and there
163+
// are errors in the packages, this will have 0 exit
164+
// code. Otherwise, we prefer to return exit code
165+
// indicating diagnostics.
166+
if diagExitCode := printDiagnostics(roots); diagExitCode != 0 {
167+
return diagExitCode // there were diagnostics
168+
}
169+
return pkgsExitCode // package errors but no diagnostics
166170
}
167171

168-
// load loads the initial packages. If all loading issues are related to
169-
// typing and parsing, the returned error is of type typeParseError.
172+
// load loads the initial packages. Returns only top-level loading
173+
// errors. Does not consider errors in packages.
170174
func load(patterns []string, allSyntax bool) ([]*packages.Package, error) {
171175
mode := packages.LoadSyntax
172176
if allSyntax {
@@ -178,44 +182,12 @@ func load(patterns []string, allSyntax bool) ([]*packages.Package, error) {
178182
Tests: IncludeTests,
179183
}
180184
initial, err := packages.Load(&conf, patterns...)
181-
if err == nil {
182-
if len(initial) == 0 {
183-
err = fmt.Errorf("%s matched no packages", strings.Join(patterns, " "))
184-
} else {
185-
err = loadingError(initial)
186-
}
185+
if err == nil && len(initial) == 0 {
186+
err = fmt.Errorf("%s matched no packages", strings.Join(patterns, " "))
187187
}
188188
return initial, err
189189
}
190190

191-
// loadingError checks for issues during the loading of initial
192-
// packages. Returns nil if there are no issues. Returns error
193-
// of type typeParseError if all errors, including those in
194-
// dependencies, are related to typing or parsing. Otherwise,
195-
// a plain error is returned with an appropriate message.
196-
func loadingError(initial []*packages.Package) error {
197-
var err error
198-
if n := packages.PrintErrors(initial); n > 1 {
199-
err = fmt.Errorf("%d errors during loading", n)
200-
} else if n == 1 {
201-
err = errors.New("error during loading")
202-
} else {
203-
// no errors
204-
return nil
205-
}
206-
all := true
207-
packages.Visit(initial, nil, func(pkg *packages.Package) {
208-
for _, err := range pkg.Errors {
209-
typeOrParse := err.Kind == packages.TypeError || err.Kind == packages.ParseError
210-
all = all && typeOrParse
211-
}
212-
})
213-
if all {
214-
return typeParseError{err}
215-
}
216-
return err
217-
}
218-
219191
// TestAnalyzer applies an analyzer to a set of packages (and their
220192
// dependencies if necessary) and returns the results.
221193
// The analyzer must be valid according to [analysis.Validate].

go/analysis/internal/checker/checker_test.go

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,11 @@ func Foo() {
6868
}
6969

7070
var renameAnalyzer = &analysis.Analyzer{
71-
Name: "rename",
72-
Requires: []*analysis.Analyzer{inspect.Analyzer},
73-
Run: run,
74-
Doc: "renames symbols named bar to baz",
71+
Name: "rename",
72+
Requires: []*analysis.Analyzer{inspect.Analyzer},
73+
Run: run,
74+
Doc: "renames symbols named bar to baz",
75+
RunDespiteErrors: true,
7576
}
7677

7778
var otherAnalyzer = &analysis.Analyzer{ // like analyzer but with a different Name.
@@ -140,13 +141,28 @@ func TestRunDespiteErrors(t *testing.T) {
140141
func Foo(s string) int {
141142
return s + 1
142143
}
143-
`}
144+
`,
145+
"cperr/test.go": `package copyerr
146+
147+
import "sync"
148+
149+
func bar() { } // for renameAnalyzer
150+
151+
type T struct{ mu sync.Mutex }
152+
type T1 struct{ t *T }
153+
154+
func NewT1() *T1 { return &T1{T} }
155+
`,
156+
}
144157

145158
testdata, cleanup, err := analysistest.WriteFiles(files)
146159
if err != nil {
147160
t.Fatal(err)
148161
}
149-
path := filepath.Join(testdata, "src/rderr/test.go")
162+
defer cleanup()
163+
164+
rderrFile := "file=" + filepath.Join(testdata, "src/rderr/test.go")
165+
cperrFile := "file=" + filepath.Join(testdata, "src/cperr/test.go")
150166

151167
// A no-op analyzer that should finish regardless of
152168
// parse or type errors in the code.
@@ -159,8 +175,8 @@ func Foo(s string) int {
159175
RunDespiteErrors: true,
160176
}
161177

162-
// A no-op analyzer that should finish regardless of
163-
// parse or type errors in the code.
178+
// A no-op analyzer, with facts, that should finish
179+
// regardless of parse or type errors in the code.
164180
noopWithFact := &analysis.Analyzer{
165181
Name: "noopfact",
166182
Requires: []*analysis.Analyzer{inspect.Analyzer},
@@ -178,30 +194,34 @@ func Foo(s string) int {
178194
code int
179195
}{
180196
// parse/type errors
181-
{name: "skip-error", pattern: []string{"file=" + path}, analyzers: []*analysis.Analyzer{renameAnalyzer}, code: 1},
197+
{name: "skip-error", pattern: []string{rderrFile}, analyzers: []*analysis.Analyzer{renameAnalyzer}, code: 1},
182198
// RunDespiteErrors allows a driver to run an Analyzer even after parse/type errors.
183199
//
184200
// The noop analyzer doesn't use facts, so the driver loads only the root
185201
// package from source. For the rest, it asks 'go list' for export data,
186202
// which fails because the compiler encounters the type error. Since the
187203
// errors come from 'go list', the driver doesn't run the analyzer.
188-
{name: "despite-error", pattern: []string{"file=" + path}, analyzers: []*analysis.Analyzer{noop}, code: 1},
204+
{name: "despite-error", pattern: []string{rderrFile}, analyzers: []*analysis.Analyzer{noop}, code: 1},
189205
// The noopfact analyzer does use facts, so the driver loads source for
190206
// all dependencies, does type checking itself, recognizes the error as a
191207
// type error, and runs the analyzer.
192-
{name: "despite-error-fact", pattern: []string{"file=" + path}, analyzers: []*analysis.Analyzer{noopWithFact}, code: 0},
208+
{name: "despite-error-fact", pattern: []string{rderrFile}, analyzers: []*analysis.Analyzer{noopWithFact}, code: 1},
193209
// combination of parse/type errors and no errors
194-
{name: "despite-error-and-no-error", pattern: []string{"file=" + path, "sort"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 1},
210+
{name: "despite-error-and-no-error", pattern: []string{rderrFile, "sort"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 1},
195211
// non-existing package error
196212
{name: "no-package", pattern: []string{"xyz"}, analyzers: []*analysis.Analyzer{renameAnalyzer}, code: 1},
197213
{name: "no-package-despite-error", pattern: []string{"abc"}, analyzers: []*analysis.Analyzer{noop}, code: 1},
198214
{name: "no-multi-package-despite-error", pattern: []string{"xyz", "abc"}, analyzers: []*analysis.Analyzer{noop}, code: 1},
199215
// combination of type/parsing and different errors
200-
{name: "different-errors", pattern: []string{"file=" + path, "xyz"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 1},
216+
{name: "different-errors", pattern: []string{rderrFile, "xyz"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 1},
201217
// non existing dir error
202218
{name: "no-match-dir", pattern: []string{"file=non/existing/dir"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 1},
203219
// no errors
204220
{name: "no-errors", pattern: []string{"sort"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 0},
221+
// duplicate list error with no findings
222+
{name: "list-error", pattern: []string{cperrFile}, analyzers: []*analysis.Analyzer{noop}, code: 1},
223+
// duplicate list errors with findings (issue #67790)
224+
{name: "list-error-findings", pattern: []string{cperrFile}, analyzers: []*analysis.Analyzer{renameAnalyzer}, code: 3},
205225
} {
206226
if test.name == "despite-error" && testenv.Go1Point() < 20 {
207227
// The behavior in the comment on the despite-error test only occurs for Go 1.20+.
@@ -211,8 +231,6 @@ func Foo(s string) int {
211231
t.Errorf("got incorrect exit code %d for test %s; want %d", got, test.name, test.code)
212232
}
213233
}
214-
215-
defer cleanup()
216234
}
217235

218236
type EmptyFact struct{}

go/analysis/passes/stringintconv/string.go

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
1717
"golang.org/x/tools/go/ast/inspector"
1818
"golang.org/x/tools/internal/aliases"
19+
"golang.org/x/tools/internal/analysisinternal"
1920
"golang.org/x/tools/internal/typeparams"
2021
)
2122

@@ -73,9 +74,15 @@ func typeName(t types.Type) string {
7374
func run(pass *analysis.Pass) (interface{}, error) {
7475
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
7576
nodeFilter := []ast.Node{
77+
(*ast.File)(nil),
7678
(*ast.CallExpr)(nil),
7779
}
80+
var file *ast.File
7881
inspect.Preorder(nodeFilter, func(n ast.Node) {
82+
if n, ok := n.(*ast.File); ok {
83+
file = n
84+
return
85+
}
7986
call := n.(*ast.CallExpr)
8087

8188
if len(call.Args) != 1 {
@@ -167,27 +174,74 @@ func run(pass *analysis.Pass) (interface{}, error) {
167174

168175
diag := analysis.Diagnostic{
169176
Pos: n.Pos(),
170-
Message: fmt.Sprintf("conversion from %s to %s yields a string of one rune, not a string of digits (did you mean fmt.Sprint(x)?)", source, target),
177+
Message: fmt.Sprintf("conversion from %s to %s yields a string of one rune, not a string of digits", source, target),
178+
}
179+
addFix := func(message string, edits []analysis.TextEdit) {
180+
diag.SuggestedFixes = append(diag.SuggestedFixes, analysis.SuggestedFix{
181+
Message: message,
182+
TextEdits: edits,
183+
})
171184
}
172185

186+
// Fix 1: use fmt.Sprint(x)
187+
//
188+
// Prefer fmt.Sprint over strconv.Itoa, FormatInt,
189+
// or FormatUint, as it works for any type.
190+
// Add an import of "fmt" as needed.
191+
//
192+
// Unless the type is exactly string, we must retain the conversion.
193+
//
194+
// Do not offer this fix if type parameters are involved,
195+
// as there are too many combinations and subtleties.
196+
// Consider x = rune | int16 | []byte: in all cases,
197+
// string(x) is legal, but the appropriate diagnostic
198+
// and fix differs. Similarly, don't offer the fix if
199+
// the type has methods, as some {String,GoString,Format}
200+
// may change the behavior of fmt.Sprint.
201+
if len(ttypes) == 1 && len(vtypes) == 1 && types.NewMethodSet(V0).Len() == 0 {
202+
fmtName, importEdit := analysisinternal.AddImport(pass.TypesInfo, file, arg.Pos(), "fmt", "fmt")
203+
if types.Identical(T0, types.Typ[types.String]) {
204+
// string(x) -> fmt.Sprint(x)
205+
addFix("Format the number as a decimal", []analysis.TextEdit{
206+
importEdit,
207+
{
208+
Pos: call.Fun.Pos(),
209+
End: call.Fun.End(),
210+
NewText: []byte(fmtName + ".Sprint"),
211+
},
212+
})
213+
} else {
214+
// mystring(x) -> mystring(fmt.Sprint(x))
215+
addFix("Format the number as a decimal", []analysis.TextEdit{
216+
importEdit,
217+
{
218+
Pos: call.Lparen + 1,
219+
End: call.Lparen + 1,
220+
NewText: []byte(fmtName + ".Sprint("),
221+
},
222+
{
223+
Pos: call.Rparen,
224+
End: call.Rparen,
225+
NewText: []byte(")"),
226+
},
227+
})
228+
}
229+
}
230+
231+
// Fix 2: use string(rune(x))
173232
if convertibleToRune {
174-
diag.SuggestedFixes = []analysis.SuggestedFix{
233+
addFix("Convert a single rune to a string", []analysis.TextEdit{
175234
{
176-
Message: "Did you mean to convert a rune to a string?",
177-
TextEdits: []analysis.TextEdit{
178-
{
179-
Pos: arg.Pos(),
180-
End: arg.Pos(),
181-
NewText: []byte("rune("),
182-
},
183-
{
184-
Pos: arg.End(),
185-
End: arg.End(),
186-
NewText: []byte(")"),
187-
},
188-
},
235+
Pos: arg.Pos(),
236+
End: arg.Pos(),
237+
NewText: []byte("rune("),
189238
},
190-
}
239+
{
240+
Pos: arg.End(),
241+
End: arg.End(),
242+
NewText: []byte(")"),
243+
},
244+
})
191245
}
192246
pass.Report(diag)
193247
})

0 commit comments

Comments
 (0)