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 }