github.com/stripe/stripe-go/v76@v76.25.0/scripts/check_api_clients/main.go (about)

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