1
2
3
4
5
6
7 package analysisinternal
8
9 import (
10 "bytes"
11 "cmp"
12 "fmt"
13 "go/ast"
14 "go/printer"
15 "go/scanner"
16 "go/token"
17 "go/types"
18 "iter"
19 pathpkg "path"
20 "slices"
21 "strings"
22
23 "golang.org/x/tools/go/analysis"
24 "golang.org/x/tools/go/ast/inspector"
25 "golang.org/x/tools/internal/astutil/cursor"
26 "golang.org/x/tools/internal/typesinternal"
27 )
28
29
30
31 func TypeErrorEndPos(fset *token.FileSet, src []byte, start token.Pos) token.Pos {
32
33 file := fset.File(start)
34 if file == nil {
35 return start
36 }
37 if offset := file.PositionFor(start, false).Offset; offset > len(src) {
38 return start
39 } else {
40 src = src[offset:]
41 }
42
43
44
45
46
47
48
49
50
51
52
53
54
55 end := start
56 {
57 var s scanner.Scanner
58 fset := token.NewFileSet()
59 f := fset.AddFile("", fset.Base(), len(src))
60 s.Init(f, src, nil , scanner.ScanComments)
61 pos, tok, lit := s.Scan()
62 if tok != token.SEMICOLON && token.Pos(f.Base()) <= pos && pos <= token.Pos(f.Base()+f.Size()) {
63 off := file.Offset(pos) + len(lit)
64 src = src[off:]
65 end += token.Pos(off)
66 }
67 }
68
69
70
71 if width := bytes.IndexAny(src, " \n,():;[]+-*/"); width > 0 {
72 end += token.Pos(width)
73 }
74 return end
75 }
76
77
78
79 func WalkASTWithParent(n ast.Node, f func(n ast.Node, parent ast.Node) bool) {
80 var ancestors []ast.Node
81 ast.Inspect(n, func(n ast.Node) (recurse bool) {
82 if n == nil {
83 ancestors = ancestors[:len(ancestors)-1]
84 return false
85 }
86
87 var parent ast.Node
88 if len(ancestors) > 0 {
89 parent = ancestors[len(ancestors)-1]
90 }
91 ancestors = append(ancestors, n)
92 return f(n, parent)
93 })
94 }
95
96
97
98
99
100 func MatchingIdents(typs []types.Type, node ast.Node, pos token.Pos, info *types.Info, pkg *types.Package) map[types.Type][]string {
101
102
103 matches := make(map[types.Type][]string)
104 for _, typ := range typs {
105 if typ == nil {
106 continue
107 }
108 matches[typ] = nil
109 }
110
111 seen := map[types.Object]struct{}{}
112 ast.Inspect(node, func(n ast.Node) bool {
113 if n == nil {
114 return false
115 }
116
117
118
119
120
121
122 if assign, ok := n.(*ast.AssignStmt); ok && pos > assign.Pos() && pos <= assign.End() {
123 return false
124 }
125 if n.End() > pos {
126 return n.Pos() <= pos
127 }
128 ident, ok := n.(*ast.Ident)
129 if !ok || ident.Name == "_" {
130 return true
131 }
132 obj := info.Defs[ident]
133 if obj == nil || obj.Type() == nil {
134 return true
135 }
136 if _, ok := obj.(*types.TypeName); ok {
137 return true
138 }
139
140 if _, ok = seen[obj]; ok {
141 return true
142 }
143 seen[obj] = struct{}{}
144
145
146 innerScope := pkg.Scope().Innermost(pos)
147 if innerScope == nil {
148 return true
149 }
150 _, foundObj := innerScope.LookupParent(ident.Name, pos)
151 if foundObj != obj {
152 return true
153 }
154
155
156 if names, ok := matches[obj.Type()]; ok {
157 matches[obj.Type()] = append(names, ident.Name)
158 } else {
159
160
161
162 for typ := range matches {
163 if equivalentTypes(obj.Type(), typ) {
164 matches[typ] = append(matches[typ], ident.Name)
165 }
166 }
167 }
168 return true
169 })
170 return matches
171 }
172
173 func equivalentTypes(want, got types.Type) bool {
174 if types.Identical(want, got) {
175 return true
176 }
177
178 if rhs, ok := want.(*types.Basic); ok && rhs.Info()&types.IsUntyped > 0 {
179 if lhs, ok := got.Underlying().(*types.Basic); ok {
180 return rhs.Info()&types.IsConstType == lhs.Info()&types.IsConstType
181 }
182 }
183 return types.AssignableTo(want, got)
184 }
185
186
187
188 type ReadFileFunc = func(filename string) ([]byte, error)
189
190
191
192 func CheckedReadFile(pass *analysis.Pass, readFile ReadFileFunc) ReadFileFunc {
193 return func(filename string) ([]byte, error) {
194 if err := CheckReadable(pass, filename); err != nil {
195 return nil, err
196 }
197 return readFile(filename)
198 }
199 }
200
201
202 func CheckReadable(pass *analysis.Pass, filename string) error {
203 if slices.Contains(pass.OtherFiles, filename) ||
204 slices.Contains(pass.IgnoredFiles, filename) {
205 return nil
206 }
207 for _, f := range pass.Files {
208 if pass.Fset.File(f.FileStart).Name() == filename {
209 return nil
210 }
211 }
212 return fmt.Errorf("Pass.ReadFile: %s is not among OtherFiles, IgnoredFiles, or names of Files", filename)
213 }
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229 func AddImport(info *types.Info, file *ast.File, preferredName, pkgpath, member string, pos token.Pos) (name, prefix string, newImport []analysis.TextEdit) {
230
231 scope := info.Scopes[file].Innermost(pos)
232 if scope == nil {
233 panic("no enclosing lexical block")
234 }
235
236
237
238 for _, spec := range file.Imports {
239 pkgname := info.PkgNameOf(spec)
240 if pkgname != nil && pkgname.Imported().Path() == pkgpath {
241 name = pkgname.Name()
242 if name == "." {
243
244 if s, _ := scope.LookupParent(member, pos); s == info.Scopes[file] {
245 return name, "", nil
246 }
247 } else if _, obj := scope.LookupParent(name, pos); obj == pkgname {
248 return name, name + ".", nil
249 }
250 }
251 }
252
253
254
255 newName := FreshName(scope, pos, preferredName)
256
257
258
259
260
261
262
263
264 newText := fmt.Sprintf("%q", pkgpath)
265 if newName != preferredName || newName != pathpkg.Base(pkgpath) {
266 newText = fmt.Sprintf("%s %q", newName, pkgpath)
267 }
268 decl0 := file.Decls[0]
269 var before ast.Node = decl0
270 switch decl0 := decl0.(type) {
271 case *ast.GenDecl:
272 if decl0.Doc != nil {
273 before = decl0.Doc
274 }
275 case *ast.FuncDecl:
276 if decl0.Doc != nil {
277 before = decl0.Doc
278 }
279 }
280
281 if gd, ok := before.(*ast.GenDecl); ok && gd.Tok == token.IMPORT && gd.Rparen.IsValid() {
282 pos = gd.Rparen
283 newText = "\t" + newText + "\n"
284 } else {
285 pos = before.Pos()
286 newText = "import " + newText + "\n\n"
287 }
288 return newName, newName + ".", []analysis.TextEdit{{
289 Pos: pos,
290 End: pos,
291 NewText: []byte(newText),
292 }}
293 }
294
295
296
297 func FreshName(scope *types.Scope, pos token.Pos, preferred string) string {
298 newName := preferred
299 for i := 0; ; i++ {
300 if _, obj := scope.LookupParent(newName, pos); obj == nil {
301 break
302 }
303 newName = fmt.Sprintf("%s%d", preferred, i)
304 }
305 return newName
306 }
307
308
309 func Format(fset *token.FileSet, e ast.Expr) string {
310 var buf strings.Builder
311 printer.Fprint(&buf, fset, e)
312 return buf.String()
313 }
314
315
316 func Imports(pkg *types.Package, path string) bool {
317 for _, imp := range pkg.Imports() {
318 if imp.Path() == path {
319 return true
320 }
321 }
322 return false
323 }
324
325
326
327
328
329
330
331 func IsTypeNamed(t types.Type, pkgPath string, names ...string) bool {
332 if named, ok := types.Unalias(t).(*types.Named); ok {
333 tname := named.Obj()
334 return tname != nil &&
335 typesinternal.IsPackageLevel(tname) &&
336 tname.Pkg().Path() == pkgPath &&
337 slices.Contains(names, tname.Name())
338 }
339 return false
340 }
341
342
343
344
345 func IsPointerToNamed(t types.Type, pkgPath string, names ...string) bool {
346 r := typesinternal.Unpointer(t)
347 if r == t {
348 return false
349 }
350 return IsTypeNamed(r, pkgPath, names...)
351 }
352
353
354
355
356
357
358
359 func IsFunctionNamed(obj types.Object, pkgPath string, names ...string) bool {
360 f, ok := obj.(*types.Func)
361 return ok &&
362 typesinternal.IsPackageLevel(obj) &&
363 f.Pkg().Path() == pkgPath &&
364 f.Type().(*types.Signature).Recv() == nil &&
365 slices.Contains(names, f.Name())
366 }
367
368
369
370
371
372
373
374 func IsMethodNamed(obj types.Object, pkgPath string, typeName string, names ...string) bool {
375 if fn, ok := obj.(*types.Func); ok {
376 if recv := fn.Type().(*types.Signature).Recv(); recv != nil {
377 _, T := typesinternal.ReceiverNamed(recv)
378 return T != nil &&
379 IsTypeNamed(T, pkgPath, typeName) &&
380 slices.Contains(names, fn.Name())
381 }
382 }
383 return false
384 }
385
386
387
388
389
390
391
392 func ValidateFixes(fset *token.FileSet, a *analysis.Analyzer, fixes []analysis.SuggestedFix) error {
393 fixMessages := make(map[string]bool)
394 for i := range fixes {
395 fix := &fixes[i]
396 if fixMessages[fix.Message] {
397 return fmt.Errorf("analyzer %q suggests two fixes with same Message (%s)", a.Name, fix.Message)
398 }
399 fixMessages[fix.Message] = true
400 if err := validateFix(fset, fix); err != nil {
401 return fmt.Errorf("analyzer %q suggests invalid fix (%s): %v", a.Name, fix.Message, err)
402 }
403 }
404 return nil
405 }
406
407
408
409
410
411 func validateFix(fset *token.FileSet, fix *analysis.SuggestedFix) error {
412
413
414
415
416
417 slices.SortStableFunc(fix.TextEdits, func(x, y analysis.TextEdit) int {
418 if sign := cmp.Compare(x.Pos, y.Pos); sign != 0 {
419 return sign
420 }
421 return cmp.Compare(x.End, y.End)
422 })
423
424 var prev *analysis.TextEdit
425 for i := range fix.TextEdits {
426 edit := &fix.TextEdits[i]
427
428
429 start := edit.Pos
430 file := fset.File(start)
431 if file == nil {
432 return fmt.Errorf("no token.File for TextEdit.Pos (%v)", edit.Pos)
433 }
434 if end := edit.End; end.IsValid() {
435 if end < start {
436 return fmt.Errorf("TextEdit.Pos (%v) > TextEdit.End (%v)", edit.Pos, edit.End)
437 }
438 endFile := fset.File(end)
439 if endFile == nil {
440 return fmt.Errorf("no token.File for TextEdit.End (%v; File(start).FileEnd is %d)", end, file.Base()+file.Size())
441 }
442 if endFile != file {
443 return fmt.Errorf("edit #%d spans files (%v and %v)",
444 i, file.Position(edit.Pos), endFile.Position(edit.End))
445 }
446 } else {
447 edit.End = start
448 }
449 if eof := token.Pos(file.Base() + file.Size()); edit.End > eof {
450 return fmt.Errorf("end is (%v) beyond end of file (%v)", edit.End, eof)
451 }
452
453
454
455 if prev != nil && edit.Pos < prev.End {
456 xpos := fset.Position(prev.Pos)
457 xend := fset.Position(prev.End)
458 ypos := fset.Position(edit.Pos)
459 yend := fset.Position(edit.End)
460 return fmt.Errorf("overlapping edits to %s (%d:%d-%d:%d and %d:%d-%d:%d)",
461 xpos.Filename,
462 xpos.Line, xpos.Column,
463 xend.Line, xend.Column,
464 ypos.Line, ypos.Column,
465 yend.Line, yend.Column,
466 )
467 }
468 prev = edit
469 }
470
471 return nil
472 }
473
474
475
476
477
478 func CanImport(from, to string) bool {
479
480 if to == "internal" || strings.HasPrefix(to, "internal/") {
481
482
483
484 first, _, _ := strings.Cut(from, "/")
485 if strings.Contains(first, ".") {
486 return false
487 }
488 if first == "testdata" {
489 return false
490 }
491 }
492 if strings.HasSuffix(to, "/internal") {
493 return strings.HasPrefix(from, to[:len(to)-len("/internal")])
494 }
495 if i := strings.LastIndex(to, "/internal/"); i >= 0 {
496 return strings.HasPrefix(from, to[:i])
497 }
498 return true
499 }
500
501
502
503
504 func DeleteStmt(fset *token.FileSet, astFile *ast.File, stmt ast.Stmt, report func(string, ...any)) []analysis.TextEdit {
505
506 insp := inspector.New([]*ast.File{astFile})
507 root := cursor.Root(insp)
508 cstmt, ok := root.FindNode(stmt)
509 if !ok {
510 report("%s not found in file", stmt.Pos())
511 return nil
512 }
513
514 if !stmt.Pos().IsValid() || !stmt.End().IsValid() {
515 report("%s: stmt has invalid position", stmt.Pos())
516 return nil
517 }
518
519
520
521
522
523
524
525 tokFile := fset.File(stmt.Pos())
526 lineOf := tokFile.Line
527 stmtStartLine, stmtEndLine := lineOf(stmt.Pos()), lineOf(stmt.End())
528
529 var from, to token.Pos
530
531 limits := func(left, right token.Pos) {
532 if lineOf(left) == stmtStartLine {
533 from = left
534 }
535 if lineOf(right) == stmtEndLine {
536 to = right
537 }
538 }
539
540
541
542
543
544 switch parent := cstmt.Parent().Node().(type) {
545 case *ast.SwitchStmt:
546 limits(parent.Switch, parent.Body.Lbrace)
547 case *ast.TypeSwitchStmt:
548 limits(parent.Switch, parent.Body.Lbrace)
549 if parent.Assign == stmt {
550 return nil
551 }
552 case *ast.BlockStmt:
553 limits(parent.Lbrace, parent.Rbrace)
554 case *ast.CommClause:
555 limits(parent.Colon, cstmt.Parent().Parent().Node().(*ast.BlockStmt).Rbrace)
556 if parent.Comm == stmt {
557 return nil
558 }
559 case *ast.CaseClause:
560 limits(parent.Colon, cstmt.Parent().Parent().Node().(*ast.BlockStmt).Rbrace)
561 case *ast.ForStmt:
562 limits(parent.For, parent.Body.Lbrace)
563
564 default:
565 return nil
566 }
567
568 if prev, found := cstmt.PrevSibling(); found && lineOf(prev.Node().End()) == stmtStartLine {
569 from = prev.Node().End()
570 }
571 if next, found := cstmt.NextSibling(); found && lineOf(next.Node().Pos()) == stmtEndLine {
572 to = next.Node().Pos()
573 }
574
575 Outer:
576 for _, cg := range astFile.Comments {
577 for _, co := range cg.List {
578 if lineOf(co.End()) < stmtStartLine {
579 continue
580 } else if lineOf(co.Pos()) > stmtEndLine {
581 break Outer
582 }
583 if lineOf(co.End()) == stmtStartLine && co.End() < stmt.Pos() {
584 if !from.IsValid() || co.End() > from {
585 from = co.End()
586 continue
587 }
588 }
589 if lineOf(co.Pos()) == stmtEndLine && co.Pos() > stmt.End() {
590 if !to.IsValid() || co.Pos() < to {
591 to = co.Pos()
592 continue
593 }
594 }
595 }
596 }
597
598
599 edit := analysis.TextEdit{Pos: stmt.Pos(), End: stmt.End()}
600 if from.IsValid() || to.IsValid() {
601
602
603
604
605
606
607 return []analysis.TextEdit{edit}
608 }
609
610 for lineOf(edit.Pos) == stmtStartLine {
611 edit.Pos--
612 }
613 edit.Pos++
614 for lineOf(edit.End) == stmtEndLine {
615 edit.End++
616 }
617 return []analysis.TextEdit{edit}
618 }
619
620
621 func Comments(file *ast.File, start, end token.Pos) iter.Seq[*ast.Comment] {
622
623 return func(yield func(*ast.Comment) bool) {
624 for _, cg := range file.Comments {
625 for _, co := range cg.List {
626 if co.Pos() > end {
627 return
628 }
629 if co.End() < start {
630 continue
631 }
632
633 if !yield(co) {
634 return
635 }
636 }
637 }
638 }
639 }
640
View as plain text