/
main.go
304 lines (248 loc) · 7.75 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
// A script that tries to make sure that all API clients (structs called
// `Client`) defined throughout all subpackages are included in the master list
// as a field on the `client.API` type. Adding a new client to `client.API` has
// historically been something that's easily forgotten.
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
)
func main() {
//
// DEBUGGING
//
// As you can see, working with Go ASTs is quite verbose, and made not
// great by all the type casting that's going on. The official docs for the
// packages provide only the most basic information about how to use them.
//
// BY FAR the easiest way to debug something is to just print the AST node
// you're interested in (it takes in an `interface{}`). The output is
// verbose, detailed, and extremely informative. For example:
//
// ast.Print(fset, f)
//
fset := token.NewFileSet()
//
// Step 1: See what clients are in `client.API`
//
// Returned as a map to facilitate fast set checking.
packagesInClientAPI, err := getClientAPIPackages(fset)
if err != nil {
exitWithError(err)
}
//
// Step 2: See what clients are in `client.API`
//
var packagesWithClient []string
err = filepath.Walk(".", func(path string, f os.FileInfo, _ error) error {
if filepath.Ext(path) != ".go" {
return nil
}
if strings.HasSuffix(filepath.Base(path), "_test.go") {
return nil
}
packageName, err := findClientType(fset, path)
if err != nil {
exitWithError(err)
}
if packageName == nil {
return nil
}
// Prepend a directory so that we know where nested packages are
// nested.
//
// For example, `session` will become `checkout/session`.
relativeDir := filepath.Dir(path)
// We're not interested in the immediate parent directory because that
// has the same name as the package itself, so strip that off the end.
relativeDir = strings.TrimSuffix(relativeDir, *packageName)
if relativeDir != "" {
relativePackageName := relativeDir + *packageName
packageName = &relativePackageName
}
packagesWithClient = append(packagesWithClient, *packageName)
return nil
})
if err != nil {
exitWithError(err)
}
if len(packagesWithClient) < 1 {
panic("Found no packages with clients; something went wrong " +
"(maybe check the working directory?)")
}
sort.Strings(packagesWithClient)
var anyMissing bool
for _, packageName := range packagesWithClient {
_, ok := packagesInClientAPI[packageName]
if !ok {
if !anyMissing {
fmt.Fprintf(os.Stderr, "!!! the following clients are missing from client.%s in %s\n",
typeNameAPI, clientAPIPath)
anyMissing = true
}
fmt.Fprintf(os.Stderr, "%s.Client\n", packageName)
}
}
if anyMissing {
os.Exit(1)
}
}
//
// Private
//
const (
// Names of some of the Go types in stripe-go that we're interested in.
typeNameAPI = "API"
typeNameClient = "Client"
// Path to file containing the `API` type.
clientAPIPath = "./client/api.go"
)
func exitWithError(err error) {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
// findType looks for a Go type defined in the given AST and returns its
// specification.
func findType(f *ast.File, typeName string) *ast.TypeSpec {
for _, decl := range f.Decls {
genDecl, ok := decl.(*ast.GenDecl)
// We're only looking for `Client` structs which are always `GenDecl`
// of type `TYPE`.
if !ok || genDecl.Tok != token.TYPE {
continue
}
if len(genDecl.Specs) > 1 {
panic("Expected only a single ast.Spec for GenDecl with Tok of Type")
}
typeSpec, ok := genDecl.Specs[0].(*ast.TypeSpec)
if !ok {
panic("Expected ast.TypeSpec in GenDecl with Tok of Type")
}
if typeSpec.Name.Name != typeName {
continue
}
// Type names are unique with packages, so return early.
return typeSpec
}
return nil
}
// findClientType looks for a type called `Client` in the `.go` file at the
// target path. If found, it returns the name of the file's defined package.
func findClientType(fset *token.FileSet, path string) (*string, error) {
source, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
f, err := parser.ParseFile(fset, "", source, 0)
if err != nil {
return nil, err
}
packageName := f.Name.Name
typeSpec := findType(f, typeNameClient)
if typeSpec == nil {
return nil, nil
}
return &packageName, nil
}
// getClientAPIPackages parses the master `API` type found in `client/api.go`,
// looks for all fields that have a type named `Client`, and returns a map
// containing the packages of those clients.
func getClientAPIPackages(fset *token.FileSet) (map[string]struct{}, error) {
packagesInClientAPI := make(map[string]struct{})
source, err := ioutil.ReadFile(clientAPIPath)
if err != nil {
return nil, err
}
f, err := parser.ParseFile(fset, "", source, 0)
if err != nil {
return nil, err
}
// First we need to make a map of any packages that are being imported
// in ways that don't map perfectly well with the package paths that we'll
// extract by looking for clients in `.go` files.
//
// The first case are packages imported under aliases. We often do this for
// namespaced packages that have a high probability of collision with
// something else. For example, `issuing/card` gets imported as
// `issuingcard`.
//
// The second case are nested packages that are referenced are still
// referenced by their local package name. For example,
// `reporting/reportrun` might have been aliased as `reportingreportrun` if
// things were perfectly consisted, but its package name is already unique
// enough that no one bothered to do so.
importAliases := make(map[string]string)
for _, importSpec := range f.Imports {
path := importSpec.Path.Value
// The import path is quoted, so trim quotes off either ended.
path = strings.TrimPrefix(path, `"`)
path = strings.TrimSuffix(path, `"`)
// Trim the fully qualified prefix off the front of the path to make
// translation easier for us after.
path = strings.TrimPrefix(path, "github.com/amazingmarvin/stripe-go/")
// A non-nil `Name` is an alias. Save the alias to our map with the
// relative package path.
if importSpec.Name != nil {
importAliases[importSpec.Name.Name] = path
continue
}
parts := strings.Split(path, "/")
// A top-level packaage. No need to keep an alias around for it.
if len(parts) == 1 {
continue
}
// Otherwise, store the alias as the last component of the path keyed
// to the entire relative path.
importAliases[parts[len(parts)-1]] = path
}
typeSpec := findType(f, typeNameAPI)
if typeSpec == nil {
return nil, fmt.Errorf("No 'API' type found in '%s'", clientAPIPath)
}
structType, ok := typeSpec.Type.(*ast.StructType)
if !ok {
panic(fmt.Sprintf("Expected %s type to be a struct", typeNameAPI))
}
for _, field := range structType.Fields.List {
// A "StarExpr" is a pointer like `*charge.Client`. The type
// `charge.Client` is wrapped within it.
//
// We expect all clients to be pointers, so skip any defined fields
// that aren't one.
starExpr, ok := field.Type.(*ast.StarExpr)
if !ok {
continue
}
selectorExpr, ok := starExpr.X.(*ast.SelectorExpr)
if !ok {
continue
}
// Only look for fields with types named 'Client'
if selectorExpr.Sel.Name != typeNameClient {
continue
}
ident, ok := selectorExpr.X.(*ast.Ident)
if !ok {
return nil, fmt.Errorf("Expected client field with type '%s' in '%s' "+
"to be proceeded by an *ast.Ident", typeNameClient, typeNameAPI)
}
packageName := ident.Name
packagesInClientAPI[packageName] = struct{}{}
}
for alias, packageName := range importAliases {
_, ok := packagesInClientAPI[alias]
if !ok {
continue
}
delete(packagesInClientAPI, alias)
packagesInClientAPI[packageName] = struct{}{}
}
return packagesInClientAPI, nil
}