1
2
3
4
5
6
7
8
9
10
11
12 package analysisinternal
13
14 import (
15 "cmp"
16 "fmt"
17 "go/token"
18 "os"
19 "slices"
20
21 "golang.org/x/tools/go/analysis"
22 )
23
24
25
26 func ReadFile(pass *analysis.Pass, filename string) ([]byte, *token.File, error) {
27 readFile := pass.ReadFile
28 if readFile == nil {
29 readFile = os.ReadFile
30 }
31 content, err := readFile(filename)
32 if err != nil {
33 return nil, nil, err
34 }
35 tf := pass.Fset.AddFile(filename, -1, len(content))
36 tf.SetLinesForContent(content)
37 return content, tf, nil
38 }
39
40
41
42 type ReadFileFunc = func(filename string) ([]byte, error)
43
44
45
46 func CheckedReadFile(pass *analysis.Pass, readFile ReadFileFunc) ReadFileFunc {
47 return func(filename string) ([]byte, error) {
48 if err := CheckReadable(pass, filename); err != nil {
49 return nil, err
50 }
51 return readFile(filename)
52 }
53 }
54
55
56 func CheckReadable(pass *analysis.Pass, filename string) error {
57 if slices.Contains(pass.OtherFiles, filename) ||
58 slices.Contains(pass.IgnoredFiles, filename) {
59 return nil
60 }
61 for _, f := range pass.Files {
62 if pass.Fset.File(f.FileStart).Name() == filename {
63 return nil
64 }
65 }
66 return fmt.Errorf("Pass.ReadFile: %s is not among OtherFiles, IgnoredFiles, or names of Files", filename)
67 }
68
69
70
71
72
73
74
75 func ValidateFixes(fset *token.FileSet, a *analysis.Analyzer, fixes []analysis.SuggestedFix) error {
76 fixMessages := make(map[string]bool)
77 for i := range fixes {
78 fix := &fixes[i]
79 if fixMessages[fix.Message] {
80 return fmt.Errorf("analyzer %q suggests two fixes with same Message (%s)", a.Name, fix.Message)
81 }
82 fixMessages[fix.Message] = true
83 if err := validateFix(fset, fix); err != nil {
84 return fmt.Errorf("analyzer %q suggests invalid fix (%s): %v", a.Name, fix.Message, err)
85 }
86 }
87 return nil
88 }
89
90
91
92
93
94 func validateFix(fset *token.FileSet, fix *analysis.SuggestedFix) error {
95
96
97
98
99
100 slices.SortStableFunc(fix.TextEdits, func(x, y analysis.TextEdit) int {
101 if sign := cmp.Compare(x.Pos, y.Pos); sign != 0 {
102 return sign
103 }
104 return cmp.Compare(x.End, y.End)
105 })
106
107 var prev *analysis.TextEdit
108 for i := range fix.TextEdits {
109 edit := &fix.TextEdits[i]
110
111
112 start := edit.Pos
113 file := fset.File(start)
114 if file == nil {
115 return fmt.Errorf("no token.File for TextEdit.Pos (%v)", edit.Pos)
116 }
117 fileEnd := token.Pos(file.Base() + file.Size())
118 if end := edit.End; end.IsValid() {
119 if end < start {
120 return fmt.Errorf("TextEdit.Pos (%v) > TextEdit.End (%v)", edit.Pos, edit.End)
121 }
122 endFile := fset.File(end)
123 if endFile != file && end < fileEnd+10 {
124
125
126
127
128
129
130
131
132
133 edit.End = fileEnd
134 continue
135 }
136 if endFile == nil {
137 return fmt.Errorf("no token.File for TextEdit.End (%v; File(start).FileEnd is %d)", end, file.Base()+file.Size())
138 }
139 if endFile != file {
140 return fmt.Errorf("edit #%d spans files (%v and %v)",
141 i, file.Position(edit.Pos), endFile.Position(edit.End))
142 }
143 } else {
144 edit.End = start
145 }
146 if eof := fileEnd; edit.End > eof {
147 return fmt.Errorf("end is (%v) beyond end of file (%v)", edit.End, eof)
148 }
149
150
151
152 if prev != nil && edit.Pos < prev.End {
153 xpos := fset.Position(prev.Pos)
154 xend := fset.Position(prev.End)
155 ypos := fset.Position(edit.Pos)
156 yend := fset.Position(edit.End)
157 return fmt.Errorf("overlapping edits to %s (%d:%d-%d:%d and %d:%d-%d:%d)",
158 xpos.Filename,
159 xpos.Line, xpos.Column,
160 xend.Line, xend.Column,
161 ypos.Line, ypos.Column,
162 yend.Line, yend.Column,
163 )
164 }
165 prev = edit
166 }
167
168 return nil
169 }
170
171
172 func Range(pos, end token.Pos) analysis.Range {
173 return tokenRange{pos, end}
174 }
175
176
177 type tokenRange struct{ StartPos, EndPos token.Pos }
178
179 func (r tokenRange) Pos() token.Pos { return r.StartPos }
180 func (r tokenRange) End() token.Pos { return r.EndPos }
181
View as plain text