github.com/jlmeeker/kismatic@v1.10.1-0.20180612190640-57f9005a1f1a/cmd/gen-kismatic-ref-docs/main.go (about)

     1  package main
     2  
     3  import (
     4  	"flag"
     5  	"fmt"
     6  	"go/ast"
     7  	godoc "go/doc"
     8  	"go/parser"
     9  	"go/token"
    10  	"os"
    11  	"reflect"
    12  	"strings"
    13  )
    14  
    15  var output = flag.String("o", "", "the output mode")
    16  
    17  type doc struct {
    18  	property     string
    19  	propertyType string
    20  	description  string
    21  	defaultValue string
    22  	options      []string
    23  	required     bool
    24  	deprecated   bool
    25  }
    26  
    27  func main() {
    28  	flag.Parse()
    29  
    30  	if len(flag.Args()) != 2 || *output == "" {
    31  		fmt.Fprintf(os.Stderr, "usage: %s <path to go file> <type> -o <output type>\n", os.Args[0])
    32  		os.Exit(1)
    33  	}
    34  	file := flag.Arg(0)
    35  	typeName := flag.Arg(1)
    36  
    37  	var r renderer
    38  	switch *output {
    39  	case "markdown":
    40  		r = markdown{}
    41  	case "markdown-table":
    42  		r = markdownTable{}
    43  	default:
    44  		fmt.Fprintf(os.Stderr, "unknown output type: %s\n", *output)
    45  		os.Exit(1)
    46  	}
    47  
    48  	fset := token.NewFileSet()
    49  	m := make(map[string]*ast.File)
    50  
    51  	f, err := parser.ParseFile(fset, file, nil, parser.ParseComments)
    52  	if err != nil {
    53  		fmt.Fprintf(os.Stderr, "error parsing file: %v\n", err)
    54  		os.Exit(1)
    55  	}
    56  
    57  	m[file] = f
    58  	apkg, _ := ast.NewPackage(fset, m, nil, nil) // error deliberately ignored
    59  	pkgDoc := godoc.New(apkg, "", 0)
    60  
    61  	for _, t := range pkgDoc.Types {
    62  		if t.Name == typeName {
    63  			docs := docForType(typeName, pkgDoc.Types, "")
    64  			// Enforce non-empty documentation on all fields
    65  			for _, d := range docs {
    66  				if strings.TrimSpace(d.description) == "" {
    67  					fmt.Fprintf(os.Stderr, "property %s does not have documentation\n", d.property)
    68  					os.Exit(1)
    69  				}
    70  			}
    71  			r.render(docs)
    72  		}
    73  	}
    74  
    75  }
    76  
    77  type renderer interface {
    78  	render(docs []doc)
    79  }
    80  
    81  // performs a depth-first traversal of a type and returns a list of docs
    82  func docForType(typeName string, allTypes []*godoc.Type, parentFieldName string) []doc {
    83  	docs := []doc{}
    84  	for _, t := range allTypes {
    85  		if t.Name == typeName {
    86  			typeSpec := t.Decl.Specs[0].(*ast.TypeSpec)
    87  			switch tt := typeSpec.Type.(type) {
    88  			default:
    89  				panic(fmt.Sprintf("unhandled typespec type %s", typeSpec.Type))
    90  			case *ast.Ident:
    91  				// This case handles the type alias used for OptionalNodeGroup.
    92  				// Recurse to get the docs for NodeGroup
    93  				d := docForType(tt.Name, allTypes, parentFieldName)
    94  				docs = append(docs, d...)
    95  			case *ast.StructType:
    96  				for _, f := range tt.Fields.List {
    97  					fieldName := fieldName(parentFieldName, f)
    98  					var typeName string
    99  
   100  					// Figure out the type of the AST node
   101  					switch x := f.Type.(type) {
   102  					case *ast.StarExpr:
   103  						typeName = x.X.(*ast.Ident).Name
   104  						d, err := parseDoc(fieldName, typeName, f.Doc.Text())
   105  						if err != nil {
   106  							panic(err)
   107  						}
   108  						docs = append(docs, d)
   109  						if isStruct(typeName) {
   110  							docs = append(docs, docForType(typeName, allTypes, fieldName)...)
   111  						}
   112  
   113  					case *ast.Ident:
   114  						typeName = x.Name
   115  						d, err := parseDoc(fieldName, typeName, f.Doc.Text())
   116  						if err != nil {
   117  							panic(err)
   118  						}
   119  						docs = append(docs, d)
   120  						if isStruct(typeName) {
   121  							docs = append(docs, docForType(typeName, allTypes, fieldName)...)
   122  						}
   123  
   124  					// In the case of an array type, use []+typeName as the type
   125  					// Recurse if it is an array of non-basic types.
   126  					case *ast.ArrayType:
   127  						typeName = x.Elt.(*ast.Ident).Name
   128  						d, err := parseDoc(fieldName, "[]"+typeName, f.Doc.Text())
   129  						if err != nil {
   130  							panic(err)
   131  						}
   132  						docs = append(docs, d)
   133  						if isStruct(typeName) {
   134  							docs = append(docs, docForType(typeName, allTypes, fieldName)...)
   135  						}
   136  					case *ast.MapType:
   137  						typeName = fmt.Sprintf("map[%s]%s", x.Key.(*ast.Ident).Name, x.Value.(*ast.Ident).Name)
   138  						d, err := parseDoc(fieldName, typeName, f.Doc.Text())
   139  						if err != nil {
   140  							panic(err)
   141  						}
   142  						docs = append(docs, d)
   143  					default:
   144  						panic(fmt.Sprintf("unhandled typespec type: %q", reflect.TypeOf(x).Name()))
   145  					}
   146  				}
   147  			}
   148  		}
   149  	}
   150  	return docs
   151  }
   152  
   153  func fieldName(parentFieldName string, field *ast.Field) string {
   154  	var yamlTag string
   155  	if field.Tag != nil {
   156  		yamlTag = reflect.StructTag(field.Tag.Value[1 : len(field.Tag.Value)-1]).Get("yaml") // Delete first and last quotation
   157  	}
   158  	// Get the field name from the yaml tag (e.g. allow_package_installation,omitempty)
   159  	yamlTag = strings.Split(yamlTag, ",")[0]
   160  	if yamlTag == "" {
   161  		if field.Names != nil {
   162  			yamlTag = strings.ToLower(field.Names[0].Name)
   163  		} else {
   164  			yamlTag = strings.ToLower(field.Type.(*ast.Ident).Name)
   165  		}
   166  	}
   167  	if parentFieldName != "" {
   168  		return parentFieldName + "." + yamlTag
   169  	}
   170  	return yamlTag
   171  }
   172  
   173  func parseDoc(propertyName string, propertyType string, typeDocs string) (doc, error) {
   174  	d := doc{property: propertyName, propertyType: propertyType}
   175  	lines := strings.Split(typeDocs, "\n")
   176  	for _, l := range lines {
   177  		if strings.Contains(l, "+required") {
   178  			d.required = true
   179  			continue
   180  		}
   181  		if strings.Contains(l, "+default") {
   182  			d.defaultValue = strings.Split(l, "=")[1]
   183  			continue
   184  		}
   185  		if strings.Contains(l, "+options") {
   186  			optsString := strings.Split(l, "=")[1]
   187  			d.options = strings.Split(optsString, ",")
   188  			continue
   189  		}
   190  		if strings.Contains(l, "+deprecated") {
   191  			d.deprecated = true
   192  			continue
   193  		}
   194  		if strings.HasPrefix(l, "+") {
   195  			return d, fmt.Errorf("unknown special marker found in documentation of %q. line was: %q", propertyName, l)
   196  		}
   197  		d.description = d.description + " " + l
   198  	}
   199  	return d, nil
   200  }
   201  
   202  func isStruct(s string) bool {
   203  	return !basicTypes[s]
   204  }
   205  
   206  // not a comprehensive list, but works for now...
   207  var basicTypes = map[string]bool{
   208  	"bool":              true,
   209  	"int":               true,
   210  	"string":            true,
   211  	"map[string]string": true,
   212  }