github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/unused/unused_test.go (about)

     1  package unused
     2  
     3  import (
     4  	"fmt"
     5  	"go/token"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  	"testing"
    10  
    11  	"golang.org/x/tools/go/analysis/analysistest"
    12  	"golang.org/x/tools/go/expect"
    13  )
    14  
    15  type expectation uint8
    16  
    17  const (
    18  	shouldBeUsed = iota
    19  	shouldBeUnused
    20  	shouldBeQuiet
    21  )
    22  
    23  func (exp expectation) String() string {
    24  	switch exp {
    25  	case shouldBeUsed:
    26  		return "used"
    27  	case shouldBeUnused:
    28  		return "unused"
    29  	case shouldBeQuiet:
    30  		return "quiet"
    31  	default:
    32  		panic("unreachable")
    33  	}
    34  }
    35  
    36  type key struct {
    37  	ident string
    38  	file  string
    39  	line  int
    40  }
    41  
    42  func (k key) String() string {
    43  	return fmt.Sprintf("%s:%d", k.file, k.line)
    44  }
    45  
    46  func relativePath(s string) string {
    47  	// This is only used in a test, so we don't care about failures, or the cost of repeatedly calling os.Getwd
    48  	cwd, err := os.Getwd()
    49  	if err != nil {
    50  		panic(err)
    51  	}
    52  	s, err = filepath.Rel(cwd, s)
    53  	if err != nil {
    54  		panic(err)
    55  	}
    56  	return s
    57  }
    58  
    59  func relativePosition(pos token.Position) string {
    60  	s := pos.Filename
    61  	if pos.IsValid() {
    62  		if s != "" {
    63  			// This is only used in a test, so we don't care about failures, or the cost of repeatedly calling os.Getwd
    64  			cwd, err := os.Getwd()
    65  			if err != nil {
    66  				panic(err)
    67  			}
    68  			s, err = filepath.Rel(cwd, s)
    69  			if err != nil {
    70  				panic(err)
    71  			}
    72  			s += ":"
    73  		}
    74  		s += fmt.Sprintf("%d", pos.Line)
    75  		if pos.Column != 0 {
    76  			s += fmt.Sprintf(":%d", pos.Column)
    77  		}
    78  	}
    79  	if s == "" {
    80  		s = "-"
    81  	}
    82  	return s
    83  }
    84  
    85  func check(t *testing.T, res *analysistest.Result) {
    86  	want := map[key]expectation{}
    87  	files := map[string]struct{}{}
    88  
    89  	isTest := false
    90  	for _, f := range res.Pass.Files {
    91  		filename := res.Pass.Fset.Position(f.Pos()).Filename
    92  		if strings.HasSuffix(filename, "_test.go") {
    93  			isTest = true
    94  			break
    95  		}
    96  	}
    97  	for _, f := range res.Pass.Files {
    98  		filename := res.Pass.Fset.Position(f.Pos()).Filename
    99  		if !strings.HasSuffix(filename, ".go") {
   100  			continue
   101  		}
   102  		files[filename] = struct{}{}
   103  		notes, err := expect.ExtractGo(res.Pass.Fset, f)
   104  		if err != nil {
   105  			t.Fatal(err)
   106  		}
   107  		for _, note := range notes {
   108  			posn := res.Pass.Fset.PositionFor(note.Pos, false)
   109  			switch note.Name {
   110  			case "quiet":
   111  				if len(note.Args) != 1 {
   112  					t.Fatalf("malformed directive at %s", posn)
   113  				}
   114  
   115  				if !isTest {
   116  					want[key{note.Args[0].(string), posn.Filename, posn.Line}] = expectation(shouldBeQuiet)
   117  				}
   118  			case "quiet_test":
   119  				if len(note.Args) != 1 {
   120  					t.Fatalf("malformed directive at %s", posn)
   121  				}
   122  
   123  				if isTest {
   124  					want[key{note.Args[0].(string), posn.Filename, posn.Line}] = expectation(shouldBeQuiet)
   125  				}
   126  			case "used":
   127  				if len(note.Args) != 2 {
   128  					t.Fatalf("malformed directive at %s", posn)
   129  				}
   130  
   131  				if !isTest {
   132  					var e expectation
   133  					if note.Args[1].(bool) {
   134  						e = shouldBeUsed
   135  					} else {
   136  						e = shouldBeUnused
   137  					}
   138  					want[key{note.Args[0].(string), posn.Filename, posn.Line}] = e
   139  				}
   140  			case "used_test":
   141  				if len(note.Args) != 2 {
   142  					t.Fatalf("malformed directive at %s", posn)
   143  				}
   144  
   145  				if isTest {
   146  					var e expectation
   147  					if note.Args[1].(bool) {
   148  						e = shouldBeUsed
   149  					} else {
   150  						e = shouldBeUnused
   151  					}
   152  					want[key{note.Args[0].(string), posn.Filename, posn.Line}] = expectation(e)
   153  				}
   154  			}
   155  		}
   156  	}
   157  
   158  	checkObjs := func(objs []Object, state expectation) {
   159  		for _, obj := range objs {
   160  			// if t, ok := obj.Type().(*types.Named); ok && t.TypeArgs().Len() != 0 {
   161  			// 	continue
   162  			// }
   163  			posn := obj.Position
   164  			if _, ok := files[posn.Filename]; !ok {
   165  				continue
   166  			}
   167  
   168  			// This key isn't great. Because of generics, multiple objects (instantiations of a generic type) exist at
   169  			// the same location. This only works because we ignore instantiations, but may lead to confusing test failures.
   170  			k := key{obj.ShortName, posn.Filename, posn.Line}
   171  			exp, ok := want[k]
   172  			if !ok {
   173  				t.Errorf("object at %s (%s) shouldn't exist but is %s (tests = %t)", relativePosition(posn), obj.ShortName, state, isTest)
   174  				continue
   175  			}
   176  			if false {
   177  				// Sometimes useful during debugging, but too noisy to have enabled for all test failures
   178  				t.Logf("%s handled by %q", k, obj)
   179  			}
   180  			delete(want, k)
   181  			if state != exp {
   182  				t.Errorf("object at %s (%s) should be %s but is %s (tests = %t)", relativePosition(posn), obj.ShortName, exp, state, isTest)
   183  			}
   184  		}
   185  	}
   186  	ures := res.Result.(Result)
   187  	checkObjs(ures.Used, shouldBeUsed)
   188  	checkObjs(ures.Unused, shouldBeUnused)
   189  	checkObjs(ures.Quiet, shouldBeQuiet)
   190  
   191  	for key, e := range want {
   192  		exp := e.String()
   193  		t.Errorf("object at %s:%d should be %s but wasn't seen", relativePath(key.file), key.line, exp)
   194  	}
   195  }
   196  
   197  func TestAll(t *testing.T) {
   198  	dirs, err := filepath.Glob(filepath.Join(analysistest.TestData(), "src", "example.com", "*"))
   199  	if err != nil {
   200  		t.Fatal(err)
   201  	}
   202  	for i, dir := range dirs {
   203  		dirs[i] = filepath.Join("example.com", filepath.Base(dir))
   204  	}
   205  
   206  	results := analysistest.Run(t, analysistest.TestData(), Analyzer.Analyzer, dirs...)
   207  	for _, res := range results {
   208  		check(t, res)
   209  	}
   210  }