Skip to content

Commit 74c9cfe

Browse files
adonovangopherbot
authored andcommitted
go/analysis: add Pass.ReadFile
The ReadFile function enables an analyzer to read the contents of a file through the driver. The allows the driver to provide the analyzer with a view of the file system that is consistent with that seen by the parser and type checker, and enables drivers to reject attempts to read arbitrary files, so that their dependency analysis (if any) can be sound and precise. + a test Fixes golang/go#62292 Change-Id: I9f301cbfd4a705a259f9e213fc2e63d74424294a Reviewed-on: https://go-review.googlesource.com/c/tools/+/581555 Reviewed-by: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Alan Donovan <[email protected]>
1 parent 5ef4fc9 commit 74c9cfe

File tree

13 files changed

+177
-24
lines changed

13 files changed

+177
-24
lines changed

go/analysis/analysis.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,19 @@ type Pass struct {
112112
// analysis's ResultType.
113113
ResultOf map[*Analyzer]interface{}
114114

115+
// ReadFile returns the contents of the named file.
116+
//
117+
// The only valid file names are the elements of OtherFiles
118+
// and IgnoredFiles, and names returned by
119+
// Fset.File(f.FileStart).Name() for each f in Files.
120+
//
121+
// Analyzers must use this function (if provided) instead of
122+
// accessing the file system directly. This allows a driver to
123+
// provide a virtualized file tree (including, for example,
124+
// unsaved editor buffers) and to track dependencies precisely
125+
// to avoid unnecessary recomputation.
126+
ReadFile func(filename string) ([]byte, error)
127+
115128
// -- facts --
116129

117130
// ImportObjectFact retrieves a fact associated with obj.

go/analysis/doc.go

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ bases, and so on.
3232
3333
# Analyzer
3434
35-
The primary type in the API is Analyzer. An Analyzer statically
35+
The primary type in the API is [Analyzer]. An Analyzer statically
3636
describes an analysis function: its name, documentation, flags,
3737
relationship to other analyzers, and of course, its logic.
3838
@@ -72,7 +72,7 @@ help that describes the analyses it performs.
7272
The doc comment contains a brief one-line summary,
7373
optionally followed by paragraphs of explanation.
7474
75-
The Analyzer type has more fields besides those shown above:
75+
The [Analyzer] type has more fields besides those shown above:
7676
7777
type Analyzer struct {
7878
Name string
@@ -114,7 +114,7 @@ instance of the Pass type.
114114
115115
# Pass
116116
117-
A Pass describes a single unit of work: the application of a particular
117+
A [Pass] describes a single unit of work: the application of a particular
118118
Analyzer to a particular package of Go code.
119119
The Pass provides information to the Analyzer's Run function about the
120120
package being analyzed, and provides operations to the Run function for
@@ -135,16 +135,14 @@ reporting diagnostics and other information back to the driver.
135135
The Fset, Files, Pkg, and TypesInfo fields provide the syntax trees,
136136
type information, and source positions for a single package of Go code.
137137
138-
The OtherFiles field provides the names, but not the contents, of non-Go
139-
files such as assembly that are part of this package. See the "asmdecl"
140-
or "buildtags" analyzers for examples of loading non-Go files and reporting
141-
diagnostics against them.
142-
143-
The IgnoredFiles field provides the names, but not the contents,
144-
of ignored Go and non-Go source files that are not part of this package
145-
with the current build configuration but may be part of other build
146-
configurations. See the "buildtags" analyzer for an example of loading
147-
and checking IgnoredFiles.
138+
The OtherFiles field provides the names of non-Go
139+
files such as assembly that are part of this package.
140+
Similarly, the IgnoredFiles field provides the names of Go and non-Go
141+
source files that are not part of this package with the current build
142+
configuration but may be part of other build configurations.
143+
The contents of these files may be read using Pass.ReadFile;
144+
see the "asmdecl" or "buildtags" analyzers for examples of loading
145+
non-Go files and reporting diagnostics against them.
148146
149147
The ResultOf field provides the results computed by the analyzers
150148
required by this one, as expressed in its Analyzer.Requires field. The
@@ -177,7 +175,7 @@ Diagnostic is defined as:
177175
The optional Category field is a short identifier that classifies the
178176
kind of message when an analysis produces several kinds of diagnostic.
179177
180-
The Diagnostic struct does not have a field to indicate its severity
178+
The [Diagnostic] struct does not have a field to indicate its severity
181179
because opinions about the relative importance of Analyzers and their
182180
diagnostics vary widely among users. The design of this framework does
183181
not hold each Analyzer responsible for identifying the severity of its
@@ -191,7 +189,7 @@ and buildtag, inspect the raw text of Go source files or even non-Go
191189
files such as assembly. To report a diagnostic against a line of a
192190
raw text file, use the following sequence:
193191
194-
content, err := os.ReadFile(filename)
192+
content, err := pass.ReadFile(filename)
195193
if err != nil { ... }
196194
tf := fset.AddFile(filename, -1, len(content))
197195
tf.SetLinesForContent(content)
@@ -216,7 +214,7 @@ addition, it records which functions are printf wrappers for use by
216214
later analysis passes to identify other printf wrappers by induction.
217215
A result such as “f is a printf wrapper” that is not interesting by
218216
itself but serves as a stepping stone to an interesting result (such as
219-
a diagnostic) is called a "fact".
217+
a diagnostic) is called a [Fact].
220218
221219
The analysis API allows an analysis to define new types of facts, to
222220
associate facts of these types with objects (named entities) declared

go/analysis/internal/checker/checker.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"golang.org/x/tools/go/analysis"
3232
"golang.org/x/tools/go/analysis/internal/analysisflags"
3333
"golang.org/x/tools/go/packages"
34+
"golang.org/x/tools/internal/analysisinternal"
3435
"golang.org/x/tools/internal/diff"
3536
"golang.org/x/tools/internal/robustio"
3637
)
@@ -432,6 +433,8 @@ func applyFixes(roots []*action) error {
432433

433434
// Now we've got a set of valid edits for each file. Apply them.
434435
for path, edits := range editsByPath {
436+
// TODO(adonovan): this should really work on the same
437+
// gulp from the file system that fed the analyzer (see #62292).
435438
contents, err := os.ReadFile(path)
436439
if err != nil {
437440
return err
@@ -766,6 +769,7 @@ func (act *action) execOnce() {
766769
AllObjectFacts: act.allObjectFacts,
767770
AllPackageFacts: act.allPackageFacts,
768771
}
772+
pass.ReadFile = analysisinternal.MakeReadFile(pass)
769773
act.pass = pass
770774

771775
var err error

go/analysis/internal/checker/checker_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"os"
1111
"path/filepath"
1212
"reflect"
13+
"strings"
1314
"testing"
1415

1516
"golang.org/x/tools/go/analysis"
@@ -18,6 +19,8 @@ import (
1819
"golang.org/x/tools/go/analysis/passes/inspect"
1920
"golang.org/x/tools/go/ast/inspector"
2021
"golang.org/x/tools/internal/testenv"
22+
"golang.org/x/tools/internal/testfiles"
23+
"golang.org/x/tools/txtar"
2124
)
2225

2326
func TestApplyFixes(t *testing.T) {
@@ -254,3 +257,90 @@ func TestURL(t *testing.T) {
254257
t.Errorf("Expected Diagnostics.URLs %v. got %v", want, urls)
255258
}
256259
}
260+
261+
// TestPassReadFile exercises the Pass.ReadFile function.
262+
func TestPassReadFile(t *testing.T) {
263+
cwd, _ := os.Getwd()
264+
265+
const src = `
266+
-- go.mod --
267+
module example.com
268+
269+
-- p/file.go --
270+
package p
271+
272+
-- p/ignored.go --
273+
//go:build darwin && mips64
274+
275+
package p
276+
277+
hello from ignored
278+
279+
-- p/other.s --
280+
hello from other
281+
`
282+
283+
// Expand archive into tmp tree.
284+
tmpdir := t.TempDir()
285+
if err := testfiles.ExtractTxtar(tmpdir, txtar.Parse([]byte(src))); err != nil {
286+
t.Fatal(err)
287+
}
288+
289+
ran := false
290+
a := &analysis.Analyzer{
291+
Name: "a",
292+
Requires: []*analysis.Analyzer{inspect.Analyzer},
293+
Doc: "doc",
294+
Run: func(pass *analysis.Pass) (any, error) {
295+
if len(pass.OtherFiles)+len(pass.IgnoredFiles) == 0 {
296+
t.Errorf("OtherFiles and IgnoredFiles are empty")
297+
return nil, nil
298+
}
299+
300+
for _, test := range []struct {
301+
filename string
302+
want string // substring of file content or error message
303+
}{
304+
{
305+
pass.OtherFiles[0], // [other.s]
306+
"hello from other",
307+
},
308+
{
309+
pass.IgnoredFiles[0], // [ignored.go]
310+
"hello from ignored",
311+
},
312+
{
313+
"nonesuch",
314+
"nonesuch is not among OtherFiles, ", // etc
315+
},
316+
{
317+
filepath.Join(cwd, "checker_test.go"),
318+
"checker_test.go is not among OtherFiles, ", // etc
319+
},
320+
} {
321+
content, err := pass.ReadFile(test.filename)
322+
var got string
323+
if err != nil {
324+
got = err.Error()
325+
} else {
326+
got = string(content)
327+
if len(got) > 100 {
328+
got = got[:100] + "..."
329+
}
330+
}
331+
if !strings.Contains(got, test.want) {
332+
t.Errorf("Pass.ReadFile(%q) did not contain %q; got:\n%s",
333+
test.filename, test.want, got)
334+
}
335+
}
336+
ran = true
337+
return nil, nil
338+
},
339+
}
340+
341+
analysistest.Run(t, tmpdir, a, "example.com/p")
342+
343+
if !ran {
344+
t.Error("analyzer did not run")
345+
}
346+
}

go/analysis/passes/asmdecl/asmdecl.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
173173

174174
Files:
175175
for _, fname := range sfiles {
176-
content, tf, err := analysisutil.ReadFile(pass.Fset, fname)
176+
content, tf, err := analysisutil.ReadFile(pass, fname)
177177
if err != nil {
178178
return nil, err
179179
}

go/analysis/passes/buildtag/buildtag.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func checkOtherFile(pass *analysis.Pass, filename string) error {
8989

9090
// We cannot use the Go parser, since this may not be a Go source file.
9191
// Read the raw bytes instead.
92-
content, tf, err := analysisutil.ReadFile(pass.Fset, filename)
92+
content, tf, err := analysisutil.ReadFile(pass, filename)
9393
if err != nil {
9494
return err
9595
}

go/analysis/passes/buildtag/buildtag_old.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func checkGoFile(pass *analysis.Pass, f *ast.File) {
8383
}
8484

8585
func checkOtherFile(pass *analysis.Pass, filename string) error {
86-
content, tf, err := analysisutil.ReadFile(pass.Fset, filename)
86+
content, tf, err := analysisutil.ReadFile(pass, filename)
8787
if err != nil {
8888
return err
8989
}

go/analysis/passes/directive/directive.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func checkGoFile(pass *analysis.Pass, f *ast.File) {
9090
func checkOtherFile(pass *analysis.Pass, filename string) error {
9191
// We cannot use the Go parser, since is not a Go source file.
9292
// Read the raw bytes instead.
93-
content, tf, err := analysisutil.ReadFile(pass.Fset, filename)
93+
content, tf, err := analysisutil.ReadFile(pass, filename)
9494
if err != nil {
9595
return err
9696
}

go/analysis/passes/framepointer/framepointer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
4848
}
4949

5050
for _, fname := range sfiles {
51-
content, tf, err := analysisutil.ReadFile(pass.Fset, fname)
51+
content, tf, err := analysisutil.ReadFile(pass, fname)
5252
if err != nil {
5353
return nil, err
5454
}

go/analysis/passes/internal/analysisutil/util.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"go/types"
1515
"os"
1616

17+
"golang.org/x/tools/go/analysis"
1718
"golang.org/x/tools/internal/aliases"
1819
"golang.org/x/tools/internal/analysisinternal"
1920
)
@@ -60,12 +61,16 @@ func HasSideEffects(info *types.Info, e ast.Expr) bool {
6061

6162
// ReadFile reads a file and adds it to the FileSet
6263
// so that we can report errors against it using lineStart.
63-
func ReadFile(fset *token.FileSet, filename string) ([]byte, *token.File, error) {
64-
content, err := os.ReadFile(filename)
64+
func ReadFile(pass *analysis.Pass, filename string) ([]byte, *token.File, error) {
65+
readFile := pass.ReadFile
66+
if readFile == nil {
67+
readFile = os.ReadFile
68+
}
69+
content, err := readFile(filename)
6570
if err != nil {
6671
return nil, nil, err
6772
}
68-
tf := fset.AddFile(filename, -1, len(content))
73+
tf := pass.Fset.AddFile(filename, -1, len(content))
6974
tf.SetLinesForContent(content)
7075
return content, tf, nil
7176
}

go/analysis/unitchecker/unitchecker.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import (
4949

5050
"golang.org/x/tools/go/analysis"
5151
"golang.org/x/tools/go/analysis/internal/analysisflags"
52+
"golang.org/x/tools/internal/analysisinternal"
5253
"golang.org/x/tools/internal/facts"
5354
"golang.org/x/tools/internal/versions"
5455
)
@@ -377,6 +378,7 @@ func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]re
377378
ExportPackageFact: facts.ExportPackageFact,
378379
AllPackageFacts: func() []analysis.PackageFact { return facts.AllPackageFacts(factFilter) },
379380
}
381+
pass.ReadFile = analysisinternal.MakeReadFile(pass)
380382

381383
t0 := time.Now()
382384
act.result, act.err = a.Run(pass)

gopls/internal/cache/analysis.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import (
4444
"golang.org/x/tools/gopls/internal/util/bug"
4545
"golang.org/x/tools/gopls/internal/util/frob"
4646
"golang.org/x/tools/gopls/internal/util/maps"
47+
"golang.org/x/tools/internal/analysisinternal"
4748
"golang.org/x/tools/internal/event"
4849
"golang.org/x/tools/internal/facts"
4950
"golang.org/x/tools/internal/gcimporter"
@@ -1363,6 +1364,9 @@ func (act *action) exec() (interface{}, *actionSummary, error) {
13631364
AllObjectFacts: func() []analysis.ObjectFact { return factset.AllObjectFacts(factFilter) },
13641365
AllPackageFacts: func() []analysis.PackageFact { return factset.AllPackageFacts(factFilter) },
13651366
}
1367+
// TODO(adonovan): integrate this into the snapshot's file
1368+
// cache and its dependency analysis.
1369+
pass.ReadFile = analysisinternal.MakeReadFile(pass)
13661370

13671371
// Recover from panics (only) within the analyzer logic.
13681372
// (Use an anonymous function to limit the recover scope.)

internal/analysisinternal/analysis.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ import (
1212
"go/ast"
1313
"go/token"
1414
"go/types"
15+
"os"
1516
"strconv"
1617

18+
"golang.org/x/tools/go/analysis"
1719
"golang.org/x/tools/internal/aliases"
1820
)
1921

@@ -393,3 +395,38 @@ func equivalentTypes(want, got types.Type) bool {
393395
}
394396
return types.AssignableTo(want, got)
395397
}
398+
399+
// MakeReadFile returns a simple implementation of the Pass.ReadFile function.
400+
func MakeReadFile(pass *analysis.Pass) func(filename string) ([]byte, error) {
401+
return func(filename string) ([]byte, error) {
402+
if err := checkReadable(pass, filename); err != nil {
403+
return nil, err
404+
}
405+
return os.ReadFile(filename)
406+
}
407+
}
408+
409+
// checkReadable enforces the access policy defined by the ReadFile field of [analysis.Pass].
410+
func checkReadable(pass *analysis.Pass, filename string) error {
411+
if slicesContains(pass.OtherFiles, filename) ||
412+
slicesContains(pass.IgnoredFiles, filename) {
413+
return nil
414+
}
415+
for _, f := range pass.Files {
416+
// TODO(adonovan): use go1.20 f.FileStart
417+
if pass.Fset.File(f.Pos()).Name() == filename {
418+
return nil
419+
}
420+
}
421+
return fmt.Errorf("Pass.ReadFile: %s is not among OtherFiles, IgnoredFiles, or names of Files", filename)
422+
}
423+
424+
// TODO(adonovan): use go1.21 slices.Contains.
425+
func slicesContains[S ~[]E, E comparable](slice S, x E) bool {
426+
for _, elem := range slice {
427+
if elem == x {
428+
return true
429+
}
430+
}
431+
return false
432+
}

0 commit comments

Comments
 (0)