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 }