github.com/google/go-github/v68@v68.0.0/github/gen-stringify-test.go (about)

     1  // Copyright 2019 The go-github AUTHORS. All rights reserved.
     2  //
     3  // Use of this source code is governed by a BSD-style
     4  // license that can be found in the LICENSE file.
     5  
     6  //go:build ignore
     7  
     8  // gen-stringify-test generates test methods to test the String methods.
     9  //
    10  // These tests eliminate most of the code coverage problems so that real
    11  // code coverage issues can be more readily identified.
    12  //
    13  // It is meant to be used by go-github contributors in conjunction with the
    14  // go generate tool before sending a PR to GitHub.
    15  // Please see the CONTRIBUTING.md file for more information.
    16  package main
    17  
    18  import (
    19  	"bytes"
    20  	"flag"
    21  	"fmt"
    22  	"go/ast"
    23  	"go/format"
    24  	"go/parser"
    25  	"go/token"
    26  	"log"
    27  	"os"
    28  	"strings"
    29  	"text/template"
    30  )
    31  
    32  const (
    33  	ignoreFilePrefix1 = "gen-"
    34  	ignoreFilePrefix2 = "github-"
    35  	outputFileSuffix  = "-stringify_test.go"
    36  )
    37  
    38  var (
    39  	verbose = flag.Bool("v", false, "Print verbose log messages")
    40  
    41  	// skipStructMethods lists "struct.method" combos to skip.
    42  	skipStructMethods = map[string]bool{}
    43  	// skipStructs lists structs to skip.
    44  	skipStructs = map[string]bool{
    45  		"RateLimits": true,
    46  	}
    47  
    48  	funcMap = template.FuncMap{
    49  		"isNotLast": func(index int, slice []*structField) string {
    50  			if index+1 < len(slice) {
    51  				return ", "
    52  			}
    53  			return ""
    54  		},
    55  		"processZeroValue": func(v string) string {
    56  			switch v {
    57  			case "Ptr(false)":
    58  				return "false"
    59  			case "Ptr(0.0)":
    60  				return "0"
    61  			case "0", "Ptr(0)", "Ptr(int64(0))":
    62  				return "0"
    63  			case `""`, `Ptr("")`:
    64  				return `""`
    65  			case "Timestamp{}", "&Timestamp{}":
    66  				return "github.Timestamp{0001-01-01 00:00:00 +0000 UTC}"
    67  			case "nil":
    68  				return "map[]"
    69  			case `[]int{0}`:
    70  				return `[0]`
    71  			case `[]string{""}`:
    72  				return `[""]`
    73  			case "[]Scope{ScopeNone}":
    74  				return `["(no scope)"]`
    75  			}
    76  			log.Fatalf("Unhandled zero value: %q", v)
    77  			return ""
    78  		},
    79  	}
    80  
    81  	sourceTmpl = template.Must(template.New("source").Funcs(funcMap).Parse(source))
    82  )
    83  
    84  func main() {
    85  	flag.Parse()
    86  	fset := token.NewFileSet()
    87  
    88  	pkgs, err := parser.ParseDir(fset, ".", sourceFilter, 0)
    89  	if err != nil {
    90  		log.Fatal(err)
    91  		return
    92  	}
    93  
    94  	for pkgName, pkg := range pkgs {
    95  		t := &templateData{
    96  			filename:     pkgName + outputFileSuffix,
    97  			Year:         2019, // No need to change this once set (even in following years).
    98  			Package:      pkgName,
    99  			Imports:      map[string]string{"testing": "testing"},
   100  			StringFuncs:  map[string]bool{},
   101  			StructFields: map[string][]*structField{},
   102  		}
   103  		for filename, f := range pkg.Files {
   104  			logf("Processing %v...", filename)
   105  			if err := t.processAST(f); err != nil {
   106  				log.Fatal(err)
   107  			}
   108  		}
   109  		if err := t.dump(); err != nil {
   110  			log.Fatal(err)
   111  		}
   112  	}
   113  	logf("Done.")
   114  }
   115  
   116  func sourceFilter(fi os.FileInfo) bool {
   117  	return !strings.HasSuffix(fi.Name(), "_test.go") &&
   118  		!strings.HasPrefix(fi.Name(), ignoreFilePrefix1) &&
   119  		!strings.HasPrefix(fi.Name(), ignoreFilePrefix2)
   120  }
   121  
   122  type templateData struct {
   123  	filename     string
   124  	Year         int
   125  	Package      string
   126  	Imports      map[string]string
   127  	StringFuncs  map[string]bool
   128  	StructFields map[string][]*structField
   129  }
   130  
   131  type structField struct {
   132  	sortVal      string // Lower-case version of "ReceiverType.FieldName".
   133  	ReceiverVar  string // The one-letter variable name to match the ReceiverType.
   134  	ReceiverType string
   135  	FieldName    string
   136  	FieldType    string
   137  	ZeroValue    string
   138  	NamedStruct  bool // Getter for named struct.
   139  }
   140  
   141  func (t *templateData) processAST(f *ast.File) error {
   142  	for _, decl := range f.Decls {
   143  		fn, ok := decl.(*ast.FuncDecl)
   144  		if ok {
   145  			if fn.Recv != nil && len(fn.Recv.List) > 0 {
   146  				id, ok := fn.Recv.List[0].Type.(*ast.Ident)
   147  				if ok && fn.Name.Name == "String" {
   148  					logf("Got FuncDecl: Name=%q, id.Name=%#v", fn.Name.Name, id.Name)
   149  					t.StringFuncs[id.Name] = true
   150  				} else {
   151  					star, ok := fn.Recv.List[0].Type.(*ast.StarExpr)
   152  					if ok && fn.Name.Name == "String" {
   153  						id, ok := star.X.(*ast.Ident)
   154  						if ok {
   155  							logf("Got FuncDecl: Name=%q, id.Name=%#v", fn.Name.Name, id.Name)
   156  							t.StringFuncs[id.Name] = true
   157  						} else {
   158  							logf("Ignoring FuncDecl: Name=%q, Type=%T", fn.Name.Name, fn.Recv.List[0].Type)
   159  						}
   160  					} else {
   161  						logf("Ignoring FuncDecl: Name=%q, Type=%T", fn.Name.Name, fn.Recv.List[0].Type)
   162  					}
   163  				}
   164  			} else {
   165  				logf("Ignoring FuncDecl: Name=%q, fn=%#v", fn.Name.Name, fn)
   166  			}
   167  			continue
   168  		}
   169  
   170  		gd, ok := decl.(*ast.GenDecl)
   171  		if !ok {
   172  			logf("Ignoring AST decl type %T", decl)
   173  			continue
   174  		}
   175  
   176  		for _, spec := range gd.Specs {
   177  			ts, ok := spec.(*ast.TypeSpec)
   178  			if !ok {
   179  				continue
   180  			}
   181  			// Skip unexported identifiers.
   182  			if !ts.Name.IsExported() {
   183  				logf("Struct %v is unexported; skipping.", ts.Name)
   184  				continue
   185  			}
   186  			// Check if the struct should be skipped.
   187  			if skipStructs[ts.Name.Name] {
   188  				logf("Struct %v is in skip list; skipping.", ts.Name)
   189  				continue
   190  			}
   191  			st, ok := ts.Type.(*ast.StructType)
   192  			if !ok {
   193  				logf("Ignoring AST type %T, Name=%q", ts.Type, ts.Name.String())
   194  				continue
   195  			}
   196  			for _, field := range st.Fields.List {
   197  				if len(field.Names) == 0 {
   198  					continue
   199  				}
   200  
   201  				fieldName := field.Names[0]
   202  				if id, ok := field.Type.(*ast.Ident); ok {
   203  					t.addIdent(id, ts.Name.String(), fieldName.String())
   204  					continue
   205  				}
   206  
   207  				if at, ok := field.Type.(*ast.ArrayType); ok {
   208  					if id, ok := at.Elt.(*ast.Ident); ok {
   209  						t.addIdentSlice(id, ts.Name.String(), fieldName.String())
   210  						continue
   211  					}
   212  				}
   213  
   214  				se, ok := field.Type.(*ast.StarExpr)
   215  				if !ok {
   216  					logf("Ignoring type %T for Name=%q, FieldName=%q", field.Type, ts.Name.String(), fieldName.String())
   217  					continue
   218  				}
   219  
   220  				// Skip unexported identifiers.
   221  				if !fieldName.IsExported() {
   222  					logf("Field %v is unexported; skipping.", fieldName)
   223  					continue
   224  				}
   225  				// Check if "struct.method" should be skipped.
   226  				if key := fmt.Sprintf("%v.Get%v", ts.Name, fieldName); skipStructMethods[key] {
   227  					logf("Method %v is in skip list; skipping.", key)
   228  					continue
   229  				}
   230  
   231  				switch x := se.X.(type) {
   232  				case *ast.ArrayType:
   233  				case *ast.Ident:
   234  					t.addIdentPtr(x, ts.Name.String(), fieldName.String())
   235  				case *ast.MapType:
   236  				case *ast.SelectorExpr:
   237  				default:
   238  					logf("processAST: type %q, field %q, unknown %T: %+v", ts.Name, fieldName, x, x)
   239  				}
   240  			}
   241  		}
   242  	}
   243  	return nil
   244  }
   245  
   246  func (t *templateData) addMapType(receiverType, fieldName string) {
   247  	t.StructFields[receiverType] = append(t.StructFields[receiverType], newStructField(receiverType, fieldName, "map[]", "nil", false))
   248  }
   249  
   250  func (t *templateData) addIdent(x *ast.Ident, receiverType, fieldName string) {
   251  	var zeroValue string
   252  	var namedStruct = false
   253  	switch x.String() {
   254  	case "int":
   255  		zeroValue = "0"
   256  	case "int64":
   257  		zeroValue = "0"
   258  	case "float64":
   259  		zeroValue = "0.0"
   260  	case "string":
   261  		zeroValue = `""`
   262  	case "bool":
   263  		zeroValue = "false"
   264  	case "Timestamp":
   265  		zeroValue = "Timestamp{}"
   266  	default:
   267  		zeroValue = "nil"
   268  		namedStruct = true
   269  	}
   270  
   271  	t.StructFields[receiverType] = append(t.StructFields[receiverType], newStructField(receiverType, fieldName, x.String(), zeroValue, namedStruct))
   272  }
   273  
   274  func (t *templateData) addIdentPtr(x *ast.Ident, receiverType, fieldName string) {
   275  	var zeroValue string
   276  	var namedStruct = false
   277  	switch x.String() {
   278  	case "int":
   279  		zeroValue = "Ptr(0)"
   280  	case "int64":
   281  		zeroValue = "Ptr(int64(0))"
   282  	case "float64":
   283  		zeroValue = "Ptr(0.0)"
   284  	case "string":
   285  		zeroValue = `Ptr("")`
   286  	case "bool":
   287  		zeroValue = "Ptr(false)"
   288  	case "Timestamp":
   289  		zeroValue = "&Timestamp{}"
   290  	default:
   291  		zeroValue = "nil"
   292  		namedStruct = true
   293  	}
   294  
   295  	t.StructFields[receiverType] = append(t.StructFields[receiverType], newStructField(receiverType, fieldName, x.String(), zeroValue, namedStruct))
   296  }
   297  
   298  func (t *templateData) addIdentSlice(x *ast.Ident, receiverType, fieldName string) {
   299  	var zeroValue string
   300  	var namedStruct = false
   301  	switch x.String() {
   302  	case "int":
   303  		zeroValue = "[]int{0}"
   304  	case "int64":
   305  		zeroValue = "[]int64{0}"
   306  	case "float64":
   307  		zeroValue = "[]float64{0}"
   308  	case "string":
   309  		zeroValue = `[]string{""}`
   310  	case "bool":
   311  		zeroValue = "[]bool{false}"
   312  	case "Scope":
   313  		zeroValue = "[]Scope{ScopeNone}"
   314  	// case "Timestamp":
   315  	// 	zeroValue = "&Timestamp{}"
   316  	default:
   317  		zeroValue = "nil"
   318  		namedStruct = true
   319  	}
   320  
   321  	t.StructFields[receiverType] = append(t.StructFields[receiverType], newStructField(receiverType, fieldName, x.String(), zeroValue, namedStruct))
   322  }
   323  
   324  func (t *templateData) dump() error {
   325  	if len(t.StructFields) == 0 {
   326  		logf("No StructFields for %v; skipping.", t.filename)
   327  		return nil
   328  	}
   329  
   330  	// Remove unused structs.
   331  	var toDelete []string
   332  	for k := range t.StructFields {
   333  		if !t.StringFuncs[k] {
   334  			toDelete = append(toDelete, k)
   335  			continue
   336  		}
   337  	}
   338  	for _, k := range toDelete {
   339  		delete(t.StructFields, k)
   340  	}
   341  
   342  	var buf bytes.Buffer
   343  	if err := sourceTmpl.Execute(&buf, t); err != nil {
   344  		return err
   345  	}
   346  	clean, err := format.Source(buf.Bytes())
   347  	if err != nil {
   348  		log.Printf("failed-to-format source:\n%v", buf.String())
   349  		return err
   350  	}
   351  
   352  	logf("Writing %v...", t.filename)
   353  	if err := os.Chmod(t.filename, 0644); err != nil {
   354  		return fmt.Errorf("os.Chmod(%q, 0644): %v", t.filename, err)
   355  	}
   356  
   357  	if err := os.WriteFile(t.filename, clean, 0444); err != nil {
   358  		return err
   359  	}
   360  
   361  	if err := os.Chmod(t.filename, 0444); err != nil {
   362  		return fmt.Errorf("os.Chmod(%q, 0444): %v", t.filename, err)
   363  	}
   364  
   365  	return nil
   366  }
   367  
   368  func newStructField(receiverType, fieldName, fieldType, zeroValue string, namedStruct bool) *structField {
   369  	return &structField{
   370  		sortVal:      strings.ToLower(receiverType) + "." + strings.ToLower(fieldName),
   371  		ReceiverVar:  strings.ToLower(receiverType[:1]),
   372  		ReceiverType: receiverType,
   373  		FieldName:    fieldName,
   374  		FieldType:    fieldType,
   375  		ZeroValue:    zeroValue,
   376  		NamedStruct:  namedStruct,
   377  	}
   378  }
   379  
   380  func logf(fmt string, args ...interface{}) {
   381  	if *verbose {
   382  		log.Printf(fmt, args...)
   383  	}
   384  }
   385  
   386  const source = `// Copyright {{.Year}} The go-github AUTHORS. All rights reserved.
   387  //
   388  // Use of this source code is governed by a BSD-style
   389  // license that can be found in the LICENSE file.
   390  
   391  // Code generated by gen-stringify-tests; DO NOT EDIT.
   392  // Instead, please run "go generate ./..." as described here:
   393  // https://github.com/google/go-github/blob/master/CONTRIBUTING.md#submitting-a-patch
   394  
   395  package {{ $package := .Package}}{{$package}}
   396  {{with .Imports}}
   397  import (
   398    {{- range . -}}
   399    "{{.}}"
   400    {{end -}}
   401  )
   402  {{end}}
   403  {{range $key, $value := .StructFields}}
   404  func Test{{ $key }}_String(t *testing.T) {
   405    t.Parallel()
   406    v := {{ $key }}{ {{range .}}{{if .NamedStruct}}
   407      {{ .FieldName }}: &{{ .FieldType }}{},{{else}}
   408      {{ .FieldName }}: {{.ZeroValue}},{{end}}{{end}}
   409    }
   410   	want := ` + "`" + `{{ $package }}.{{ $key }}{{ $slice := . }}{
   411  {{- range $ind, $val := .}}{{if .NamedStruct}}{{ .FieldName }}:{{ $package }}.{{ .FieldType }}{}{{else}}{{ .FieldName }}:{{ processZeroValue .ZeroValue }}{{end}}{{ isNotLast $ind $slice }}{{end}}}` + "`" + `
   412  	if got := v.String(); got != want {
   413  		t.Errorf("{{ $key }}.String = %v, want %v", got, want)
   414  	}
   415  }
   416  {{end}}
   417  `