Skip to content

Commit bce67c4

Browse files
adonovangopherbot
authored andcommitted
go/analysis/internal/checker: validate SuggestedFixes
This change causes the Pass.Report operation of all our drivers: - internal/checker, used by {single,multi}checker, analysistest, and the public checker API; - unitchecker, used by cmd/vet; and - gopls' analysis driver to assert that SuggestedFixes are valid, and to establish postconditions such as the fix.End is valid. Also, add a test that pass.Report panics informatively. Change-Id: I7ee4ac621852ab0a39d47edce1ab6e2304bfc53b Reviewed-on: https://go-review.googlesource.com/c/tools/+/643715 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Robert Findley <[email protected]> Auto-Submit: Alan Donovan <[email protected]>
1 parent bb0a9cd commit bce67c4

File tree

6 files changed

+283
-37
lines changed

6 files changed

+283
-37
lines changed

go/analysis/checker/checker.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,8 +337,14 @@ func (act *Action) execOnce() {
337337
TypeErrors: act.Package.TypeErrors,
338338
Module: module,
339339

340-
ResultOf: inputs,
341-
Report: func(d analysis.Diagnostic) { act.Diagnostics = append(act.Diagnostics, d) },
340+
ResultOf: inputs,
341+
Report: func(d analysis.Diagnostic) {
342+
// Assert that SuggestedFixes are well formed.
343+
if err := analysisinternal.ValidateFixes(act.Package.Fset, act.Analyzer, d.SuggestedFixes); err != nil {
344+
panic(err)
345+
}
346+
act.Diagnostics = append(act.Diagnostics, d)
347+
},
342348
ImportObjectFact: act.ObjectFact,
343349
ExportObjectFact: act.exportObjectFact,
344350
ImportPackageFact: act.PackageFact,

go/analysis/internal/checker/checker_test.go

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ var otherAnalyzer = &analysis.Analyzer{ // like analyzer but with a different Na
8484
}
8585

8686
func run(pass *analysis.Pass) (interface{}, error) {
87+
// TODO(adonovan): replace this entangled test with something completely data-driven.
8788
const (
8889
from = "bar"
8990
to = "baz"
@@ -109,11 +110,39 @@ func run(pass *analysis.Pass) (interface{}, error) {
109110
}
110111
switch pass.Pkg.Name() {
111112
case conflict:
112-
edits = append(edits, []analysis.TextEdit{
113-
{Pos: ident.Pos() - 1, End: ident.End(), NewText: []byte(to)},
114-
{Pos: ident.Pos(), End: ident.End() - 1, NewText: []byte(to)},
115-
{Pos: ident.Pos(), End: ident.End(), NewText: []byte("lorem ipsum")},
116-
}...)
113+
// Conflicting edits are legal, so long as they appear in different fixes.
114+
pass.Report(analysis.Diagnostic{
115+
Pos: ident.Pos(),
116+
End: ident.End(),
117+
Message: msg,
118+
SuggestedFixes: []analysis.SuggestedFix{{
119+
Message: msg, TextEdits: []analysis.TextEdit{
120+
{Pos: ident.Pos() - 1, End: ident.End(), NewText: []byte(to)},
121+
},
122+
}},
123+
})
124+
pass.Report(analysis.Diagnostic{
125+
Pos: ident.Pos(),
126+
End: ident.End(),
127+
Message: msg,
128+
SuggestedFixes: []analysis.SuggestedFix{{
129+
Message: msg, TextEdits: []analysis.TextEdit{
130+
{Pos: ident.Pos(), End: ident.End() - 1, NewText: []byte(to)},
131+
},
132+
}},
133+
})
134+
pass.Report(analysis.Diagnostic{
135+
Pos: ident.Pos(),
136+
End: ident.End(),
137+
Message: msg,
138+
SuggestedFixes: []analysis.SuggestedFix{{
139+
Message: msg, TextEdits: []analysis.TextEdit{
140+
{Pos: ident.Pos(), End: ident.End(), NewText: []byte("lorem ipsum")},
141+
},
142+
}},
143+
})
144+
return
145+
117146
case duplicate:
118147
// Duplicate (non-insertion) edits are disallowed,
119148
// so this is a buggy analyzer, and validatedFixes should reject it.

go/analysis/internal/checker/fix_test.go

Lines changed: 128 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import (
1919

2020
"golang.org/x/tools/go/analysis"
2121
"golang.org/x/tools/go/analysis/analysistest"
22+
"golang.org/x/tools/go/analysis/checker"
2223
"golang.org/x/tools/go/analysis/multichecker"
24+
"golang.org/x/tools/go/packages"
2325
"golang.org/x/tools/internal/testenv"
2426
)
2527

@@ -126,15 +128,6 @@ func Foo() {
126128
_ = bar
127129
}
128130
129-
// the end
130-
`,
131-
"duplicate/dup.go": `package duplicate
132-
133-
func Foo() {
134-
bar := 14
135-
_ = bar
136-
}
137-
138131
// the end
139132
`,
140133
}
@@ -164,15 +157,6 @@ func Foo() {
164157
_ = baz
165158
}
166159
167-
// the end
168-
`,
169-
"duplicate/dup.go": `package duplicate
170-
171-
func Foo() {
172-
baz := 14
173-
_ = baz
174-
}
175-
176160
// the end
177161
`,
178162
}
@@ -182,7 +166,7 @@ func Foo() {
182166
}
183167
defer cleanup()
184168

185-
fix(t, dir, "rename,other", exitCodeDiagnostics, "rename", "duplicate")
169+
fix(t, dir, "rename,other", exitCodeDiagnostics, "rename")
186170

187171
for name, want := range fixed {
188172
path := path.Join(dir, "src", name)
@@ -196,6 +180,117 @@ func Foo() {
196180
}
197181
}
198182

183+
// TestReportInvalidDiagnostic tests that a call to pass.Report with
184+
// certain kind of invalid diagnostic (e.g. conflicting fixes)
185+
// promptly results in a panic.
186+
func TestReportInvalidDiagnostic(t *testing.T) {
187+
testenv.NeedsGoPackages(t)
188+
189+
// Load the errors package.
190+
cfg := &packages.Config{Mode: packages.LoadAllSyntax}
191+
initial, err := packages.Load(cfg, "errors")
192+
if err != nil {
193+
t.Fatal(err)
194+
}
195+
196+
for _, test := range []struct {
197+
name string
198+
want string
199+
diag func(pos token.Pos) analysis.Diagnostic
200+
}{
201+
// Diagnostic has two alternative fixes with the same Message.
202+
{
203+
"duplicate message",
204+
`analyzer "a" suggests two fixes with same Message \(fix\)`,
205+
func(pos token.Pos) analysis.Diagnostic {
206+
return analysis.Diagnostic{
207+
Pos: pos,
208+
Message: "oops",
209+
SuggestedFixes: []analysis.SuggestedFix{
210+
{Message: "fix"},
211+
{Message: "fix"},
212+
},
213+
}
214+
},
215+
},
216+
// TextEdit has invalid Pos.
217+
{
218+
"bad Pos",
219+
`analyzer "a" suggests invalid fix .*: missing file info for pos`,
220+
func(pos token.Pos) analysis.Diagnostic {
221+
return analysis.Diagnostic{
222+
Pos: pos,
223+
Message: "oops",
224+
SuggestedFixes: []analysis.SuggestedFix{
225+
{
226+
Message: "fix",
227+
TextEdits: []analysis.TextEdit{{}},
228+
},
229+
},
230+
}
231+
},
232+
},
233+
// TextEdit has invalid End.
234+
{
235+
"End < Pos",
236+
`analyzer "a" suggests invalid fix .*: pos .* > end`,
237+
func(pos token.Pos) analysis.Diagnostic {
238+
return analysis.Diagnostic{
239+
Pos: pos,
240+
Message: "oops",
241+
SuggestedFixes: []analysis.SuggestedFix{
242+
{
243+
Message: "fix",
244+
TextEdits: []analysis.TextEdit{{
245+
Pos: pos + 2,
246+
End: pos,
247+
}},
248+
},
249+
},
250+
}
251+
},
252+
},
253+
// Two TextEdits overlap.
254+
{
255+
"overlapping edits",
256+
`analyzer "a" suggests invalid fix .*: overlapping edits to .*errors.go \(1:1-1:3 and 1:2-1:4\)`,
257+
func(pos token.Pos) analysis.Diagnostic {
258+
return analysis.Diagnostic{
259+
Pos: pos,
260+
Message: "oops",
261+
SuggestedFixes: []analysis.SuggestedFix{
262+
{
263+
Message: "fix",
264+
TextEdits: []analysis.TextEdit{
265+
{Pos: pos, End: pos + 2},
266+
{Pos: pos + 1, End: pos + 3},
267+
},
268+
},
269+
},
270+
}
271+
},
272+
},
273+
} {
274+
t.Run(test.name, func(t *testing.T) {
275+
reached := false
276+
a := &analysis.Analyzer{Name: "a", Doc: "doc", Run: func(pass *analysis.Pass) (any, error) {
277+
reached = true
278+
panics(t, test.want, func() {
279+
pos := pass.Files[0].FileStart
280+
pass.Report(test.diag(pos))
281+
})
282+
return nil, nil
283+
}}
284+
if _, err := checker.Analyze([]*analysis.Analyzer{a}, initial, &checker.Options{}); err != nil {
285+
t.Fatalf("Analyze failed: %v", err)
286+
}
287+
if !reached {
288+
t.Error("analyzer was never invoked")
289+
}
290+
})
291+
}
292+
}
293+
199294
// TestConflict ensures that checker.Run detects conflicts correctly.
200295
// This test fork/execs the main function above.
201296
func TestConflict(t *testing.T) {
@@ -333,3 +428,17 @@ func init() {
333428
},
334429
}
335430
}
431+
432+
// panics asserts that f() panics with with a value whose printed form matches the regexp want.
433+
func panics(t *testing.T, want string, f func()) {
434+
defer func() {
435+
if x := recover(); x == nil {
436+
t.Errorf("function returned normally, wanted panic")
437+
} else if m, err := regexp.MatchString(want, fmt.Sprint(x)); err != nil {
438+
t.Errorf("panics: invalid regexp %q", want)
439+
} else if !m {
440+
t.Errorf("function panicked with value %q, want match for %q", x, want)
441+
}
442+
}()
443+
f()
444+
}

go/analysis/unitchecker/unitchecker.go

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -367,17 +367,26 @@ func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]re
367367
}
368368

369369
pass := &analysis.Pass{
370-
Analyzer: a,
371-
Fset: fset,
372-
Files: files,
373-
OtherFiles: cfg.NonGoFiles,
374-
IgnoredFiles: cfg.IgnoredFiles,
375-
Pkg: pkg,
376-
TypesInfo: info,
377-
TypesSizes: tc.Sizes,
378-
TypeErrors: nil, // unitchecker doesn't RunDespiteErrors
379-
ResultOf: inputs,
380-
Report: func(d analysis.Diagnostic) { act.diagnostics = append(act.diagnostics, d) },
370+
Analyzer: a,
371+
Fset: fset,
372+
Files: files,
373+
OtherFiles: cfg.NonGoFiles,
374+
IgnoredFiles: cfg.IgnoredFiles,
375+
Pkg: pkg,
376+
TypesInfo: info,
377+
TypesSizes: tc.Sizes,
378+
TypeErrors: nil, // unitchecker doesn't RunDespiteErrors
379+
ResultOf: inputs,
380+
Report: func(d analysis.Diagnostic) {
381+
// Unitchecker doesn't apply fixes, but it does report them in the JSON output.
382+
if err := analysisinternal.ValidateFixes(fset, a, d.SuggestedFixes); err != nil {
383+
// Since we have diagnostics, the exit code will be nonzero,
384+
// so logging these errors is sufficient.
385+
log.Println(err)
386+
d.SuggestedFixes = nil
387+
}
388+
act.diagnostics = append(act.diagnostics, d)
389+
},
381390
ImportObjectFact: facts.ImportObjectFact,
382391
ExportObjectFact: facts.ExportObjectFact,
383392
AllObjectFacts: func() []analysis.ObjectFact { return facts.AllObjectFacts(factFilter) },

gopls/internal/cache/analysis.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,6 +1131,11 @@ func (act *action) exec(ctx context.Context) (any, *actionSummary, error) {
11311131
TypeErrors: apkg.typeErrors,
11321132
ResultOf: inputs,
11331133
Report: func(d analysis.Diagnostic) {
1134+
// Assert that SuggestedFixes are well formed.
1135+
if err := analysisinternal.ValidateFixes(apkg.pkg.FileSet(), analyzer, d.SuggestedFixes); err != nil {
1136+
bug.Reportf("invalid SuggestedFixes: %v", err)
1137+
d.SuggestedFixes = nil
1138+
}
11341139
diagnostic, err := toGobDiagnostic(posToLocation, analyzer, d)
11351140
if err != nil {
11361141
// Don't bug.Report here: these errors all originate in

0 commit comments

Comments
 (0)