talos/hack/docgen/main.go
Andrey Smirnov 4f044e4664
feat: implement strategic merge machine config patching
This format is much easier to understand when compared to JSON patches,
it allows for more patch validation, and it should provide better user
experience.

This just implements the config merge, but it doesn't yet hook it up to
any CLI utility, so no user-facing docs.

Signed-off-by: Andrey Smirnov <andrey.smirnov@talos-systems.com>
2022-07-05 17:37:24 +04:00

524 lines
11 KiB
Go

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package main
import (
"bytes"
"flag"
"fmt"
"go/ast"
"go/parser"
"go/token"
"log"
"os"
"path/filepath"
"reflect"
"strings"
"text/template"
yaml "gopkg.in/yaml.v3"
"mvdan.cc/gofumpt/format"
)
var tpl = `// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Code generated by hack/docgen tool. DO NOT EDIT.
package {{ .Package }}
import (
"github.com/talos-systems/talos/pkg/machinery/config/encoder"
)
{{ $tick := "` + "`" + `" -}}
var (
{{ range $struct := .Structs -}}
{{ $struct.Name }}Doc encoder.Doc
{{ end -}}
)
func init() {
{{ range $struct := .Structs -}}
{{ $docVar := printf "%v%v" $struct.Name "Doc" }}
{{ $docVar }}.Type = "{{ $struct.Name }}"
{{ $docVar }}.Comments[encoder.LineComment] = "{{ $struct.Text.Comment }}"
{{ $docVar }}.Description = "{{ $struct.Text.Description }}"
{{ range $example := $struct.Text.Examples }}
{{ if $example.Value }}
{{ $docVar }}.AddExample("{{ $example.Name }}", {{ $example.Value }})
{{ end -}}
{{ end -}}
{{ if $struct.AppearsIn -}}
{{ $docVar }}.AppearsIn = []encoder.Appearance{
{{ range $value := $struct.AppearsIn -}}
{
TypeName: "{{ $value.Struct.Name }}",
FieldName: "{{ $value.FieldName }}",
},
{{ end -}}
}
{{ end -}}
{{ $docVar }}.Fields = make([]encoder.Doc,{{ len $struct.Fields }})
{{ range $index, $field := $struct.Fields }}{{ if $field.Tag -}}
{{ $docVar }}.Fields[{{ $index }}].Name = "{{ $field.Tag }}"
{{ $docVar }}.Fields[{{ $index }}].Type = "{{ $field.Type }}"
{{ $docVar }}.Fields[{{ $index }}].Note = "{{ $field.Note }}"
{{ $docVar }}.Fields[{{ $index }}].Description = "{{ $field.Text.Description }}"
{{ $docVar }}.Fields[{{ $index }}].Comments[encoder.LineComment] = "{{ $field.Text.Comment }}"
{{ range $example := $field.Text.Examples }}
{{ if $example.Value }}
{{ $docVar }}.Fields[{{ $index }}].AddExample("{{ $example.Name }}", {{ $example.Value }})
{{ end -}}
{{ end -}}
{{ if $field.Text.Values -}}
{{ $docVar }}.Fields[{{ $index }}].Values = []string{
{{ range $value := $field.Text.Values -}}
"{{ $value }}",
{{ end -}}
}
{{ end -}}
{{ end -}}
{{- end }}
{{ end }}
}
{{ range $struct := .Structs -}}
func (_ {{ $struct.Name }}) Doc() *encoder.Doc {
return &{{ $struct.Name }}Doc
}
{{ end -}}
// Get{{ .Name }}Doc returns documentation for the file {{ .File }}.
func Get{{ .Name }}Doc() *encoder.FileDoc {
return &encoder.FileDoc{
Name: "{{ .Name }}",
Description: "{{ .Header }}",
Structs: []*encoder.Doc{
{{ range $struct := .Structs -}}
&{{ $struct.Name }}Doc,
{{ end -}}
},
}
}
`
type Doc struct {
Name string
Package string
Title string
Header string
File string
Structs []*Struct
}
type Struct struct {
Name string
Text *Text
Fields []*Field
AppearsIn []Appearance
}
type Appearance struct {
Struct *Struct
FieldName string
}
type Example struct {
Name string `yaml:"name"`
Value string `yaml:"value"`
}
type Field struct {
Name string
Type string
TypeRef string
Text *Text
Tag string
Note string
}
type Text struct {
Comment string `json:"-"`
Description string `json:"description"`
Examples []*Example `json:"examples"`
Values []string `json:"values"`
}
func in(p string) (string, error) {
return filepath.Abs(p)
}
func out(p string) (*os.File, error) {
abs, err := filepath.Abs(p)
if err != nil {
return nil, err
}
return os.Create(abs)
}
type structType struct {
name string
text *Text
pos token.Pos
node *ast.StructType
}
type aliasType struct {
fieldType string
fieldTypeRef string
}
func collectStructs(node ast.Node) ([]*structType, map[string]aliasType) {
structs := []*structType{}
aliases := map[string]aliasType{}
collectStructs := func(n ast.Node) bool {
g, ok := n.(*ast.GenDecl)
if !ok {
return true
}
isAlias := false
if g.Doc != nil {
for _, comment := range g.Doc.List {
if strings.Contains(comment.Text, "docgen:nodoc") {
return true
}
if strings.Contains(comment.Text, "docgen:alias") {
isAlias = true
}
}
}
for _, spec := range g.Specs {
t, ok := spec.(*ast.TypeSpec)
if !ok {
return true
}
if t.Type == nil {
return true
}
x, ok := t.Type.(*ast.StructType)
if !ok {
if isAlias {
aliases[t.Name.Name] = aliasType{
fieldType: formatFieldType(t.Type),
fieldTypeRef: getFieldType(t.Type),
}
}
return true
}
structName := t.Name.Name
text := &Text{}
if t.Doc != nil {
text = parseComment([]byte(t.Doc.Text()))
} else if g.Doc != nil {
text = parseComment([]byte(g.Doc.Text()))
}
s := &structType{
name: structName,
text: text,
node: x,
pos: x.Pos(),
}
structs = append(structs, s)
}
return true
}
ast.Inspect(node, collectStructs)
return structs, aliases
}
func parseComment(comment []byte) *Text {
text := &Text{}
if err := yaml.Unmarshal(comment, text); err != nil {
// not yaml, fallback
text.Description = string(comment)
// take only the first line from the Description for the comment
text.Comment = strings.Split(text.Description, "\n")[0]
// try to parse everything except for the first line as yaml
if err = yaml.Unmarshal([]byte(strings.Join(strings.Split(text.Description, "\n")[1:], "\n")), text); err == nil {
// if parsed, remove it from the description
text.Description = text.Comment
}
} else {
text.Description = strings.TrimSpace(text.Description)
// take only the first line from the Description for the comment
text.Comment = strings.Split(text.Description, "\n")[0]
}
text.Comment = escape(text.Comment)
text.Description = escape(text.Description)
for _, example := range text.Examples {
example.Name = escape(example.Name)
example.Value = strings.TrimSpace(example.Value)
}
return text
}
func getFieldType(p interface{}) string {
if m, ok := p.(*ast.MapType); ok {
return getFieldType(m.Value)
}
switch t := p.(type) {
case *ast.Ident:
return t.Name
case *ast.ArrayType:
return getFieldType(p.(*ast.ArrayType).Elt)
case *ast.StarExpr:
return getFieldType(t.X)
case *ast.SelectorExpr:
return getFieldType(t.Sel)
default:
return ""
}
}
func formatFieldType(p interface{}) string {
if m, ok := p.(*ast.MapType); ok {
return fmt.Sprintf("map[%s]%s", formatFieldType(m.Key), formatFieldType(m.Value))
}
switch t := p.(type) {
case *ast.Ident:
return t.Name
case *ast.ArrayType:
return "[]" + formatFieldType(p.(*ast.ArrayType).Elt)
case *ast.StructType:
return "struct"
case *ast.StarExpr:
return formatFieldType(t.X)
case *ast.SelectorExpr:
return formatFieldType(t.Sel)
default:
log.Printf("unknown: %#v", t)
return ""
}
}
func escape(value string) string {
value = strings.ReplaceAll(value, `"`, `\"`)
value = strings.ReplaceAll(value, "\n", `\n`)
return strings.TrimSpace(value)
}
func collectFields(s *structType, aliases map[string]aliasType) (fields []*Field) {
fields = []*Field{}
for _, f := range s.node.Fields.List {
if f.Names == nil {
// This is an embedded struct.
continue
}
name := f.Names[0].Name
if f.Doc == nil {
log.Fatalf("field %q is missing a documentation", name)
}
if strings.Contains(f.Doc.Text(), "docgen:nodoc") {
fields = append(fields, &Field{Type: "unknown"})
continue
}
if f.Tag == nil {
log.Fatalf("field %q is missing a tag", name)
}
fieldType := formatFieldType(f.Type)
if alias, ok := aliases[fieldType]; ok {
fieldType = alias.fieldType
}
fieldTypeRef := getFieldType(f.Type)
if alias, ok := aliases[fieldTypeRef]; ok {
fieldTypeRef = alias.fieldTypeRef
}
tag := reflect.StructTag(strings.Trim(f.Tag.Value, "`"))
yamlTag := tag.Get("yaml")
yamlTag = strings.Split(yamlTag, ",")[0]
if yamlTag == "" {
log.Fatalf("field %q is missing a `yaml` tag or name in it", name)
}
text := parseComment([]byte(f.Doc.Text()))
field := &Field{
Name: name,
Tag: yamlTag,
Type: fieldType,
TypeRef: fieldTypeRef,
Text: text,
}
if f.Comment != nil {
field.Note = escape(f.Comment.Text())
}
fields = append(fields, field)
}
return fields
}
func render(doc *Doc, dest string) {
t := template.Must(template.New("docfile.tpl").Parse(tpl))
buf := bytes.Buffer{}
err := t.Execute(&buf, doc)
if err != nil {
panic(err)
}
formatted, err := format.Source(buf.Bytes(), format.Options{})
if err != nil {
log.Printf("data: %s", buf.Bytes())
panic(err)
}
out, err := out(dest)
if err != nil {
panic(err)
}
defer out.Close()
_, err = out.Write(formatted)
if err != nil {
panic(err)
}
}
func processFile(inputFile, outputFile, typeName string) {
abs, err := in(inputFile)
if err != nil {
log.Fatal(err)
}
fmt.Printf("creating package file set: %q\n", abs)
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, abs, nil, parser.ParseComments)
if err != nil {
log.Fatal(err)
}
packageName := node.Name.Name
tokenFile := fset.File(node.Pos())
if tokenFile == nil {
log.Fatalf("No token")
}
fmt.Printf("parsing file in package %q: %s\n", packageName, tokenFile.Name())
structs, aliases := collectStructs(node)
if len(structs) == 0 {
log.Fatalf("failed to find types that could be documented in %s", abs)
}
doc := &Doc{
Package: packageName,
Structs: []*Struct{},
}
extraExamples := map[string][]*Example{}
backReferences := map[string][]Appearance{}
for _, s := range structs {
fmt.Printf("generating docs for type: %q\n", s.name)
fields := collectFields(s, aliases)
s := &Struct{
Name: s.name,
Text: s.text,
Fields: fields,
}
for _, field := range fields {
if field.TypeRef == "" {
continue
}
if len(field.Text.Examples) > 0 {
extraExamples[field.TypeRef] = append(extraExamples[field.TypeRef], field.Text.Examples...)
}
backReferences[field.TypeRef] = append(backReferences[field.TypeRef], Appearance{
Struct: s,
FieldName: field.Tag,
})
}
doc.Structs = append(doc.Structs, s)
}
for _, s := range doc.Structs {
if extra, ok := extraExamples[s.Name]; ok {
s.Text.Examples = append(s.Text.Examples, extra...)
}
if ref, ok := backReferences[s.Name]; ok {
s.AppearsIn = append(s.AppearsIn, ref...)
}
}
if err == nil {
doc.Package = node.Name.Name
doc.Name = typeName
if node.Doc != nil {
doc.Header = escape(node.Doc.Text())
}
}
doc.File = outputFile
render(doc, outputFile)
}
func main() {
flag.Parse()
if flag.NArg() != 3 {
log.Fatalf("expected 3 args, got %d", flag.NArg())
}
inputFile := flag.Arg(0)
outputFile := flag.Arg(1)
typeName := flag.Arg(2)
processFile(inputFile, outputFile, typeName)
}