fix(cmd): long-form flags, honest counts and safer inference
Assisted-by: GLM 5.3
This commit is contained in:
+108
-33
@@ -9,8 +9,10 @@ import (
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"io"
|
||||
"maps"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@@ -34,7 +36,9 @@ func runSchema(typeName, sourcePath string, stdout io.Writer) error {
|
||||
return fmt.Errorf("no struct type %q in %s", typeName, filepath.Base(sourcePath))
|
||||
}
|
||||
body := &strings.Builder{}
|
||||
writeSchemaFields(body, st, types, "")
|
||||
if err := writeSchemaFields(body, st, types, "", nil); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.WriteString(stdout, strings.TrimLeft(body.String(), "\n"))
|
||||
return err
|
||||
}
|
||||
@@ -74,9 +78,18 @@ type fieldMeta struct {
|
||||
// writeSchemaFields writes the fields of one struct level: the scalar lines
|
||||
// first, then the sections, so the template re-parses with every value under
|
||||
// the header it belongs to. prefix is the dotted path the nested headers
|
||||
// carry.
|
||||
func writeSchemaFields(w *strings.Builder, st *ast.StructType, types map[string]*ast.StructType, prefix string) {
|
||||
metas := metasOf(st, types)
|
||||
// carry. path holds the struct types of the levels currently being written,
|
||||
// so a type that reaches itself is reported as recursion instead of
|
||||
// exhausting the stack.
|
||||
func writeSchemaFields(w *strings.Builder, st *ast.StructType, types map[string]*ast.StructType, prefix string, path []*ast.StructType) error {
|
||||
if slices.Contains(path, st) {
|
||||
return recursionError(st, types)
|
||||
}
|
||||
path = append(path, st)
|
||||
metas, err := metasOf(st, types)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, m := range metas {
|
||||
if _, elemSt := elementStruct(m.typ, types); elemSt != nil {
|
||||
continue
|
||||
@@ -98,7 +111,9 @@ func writeSchemaFields(w *strings.Builder, st *ast.StructType, types map[string]
|
||||
writeComment(w, m.comment)
|
||||
fmt.Fprintf(w, "[%s%s]\n", prefix, m.key)
|
||||
if sub := structOf(m.typ, types); sub != nil {
|
||||
writeSchemaFields(w, sub, types, prefix+m.key+".")
|
||||
if err := writeSchemaFields(w, sub, types, prefix+m.key+".", path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
}
|
||||
@@ -109,9 +124,12 @@ func writeSchemaFields(w *strings.Builder, st *ast.StructType, types map[string]
|
||||
}
|
||||
writeComment(w, m.comment)
|
||||
fmt.Fprintf(w, "[[%s%s]]\n", prefix, m.key)
|
||||
writeSchemaFields(w, elemSt, types, "")
|
||||
if err := writeSchemaFields(w, elemSt, types, "", path); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeComment writes the comment lines above a binding.
|
||||
@@ -125,46 +143,103 @@ func writeComment(w *strings.Builder, text string) {
|
||||
}
|
||||
|
||||
// metasOf flattens the exported fields of a struct. The key comes from the
|
||||
// toml tag, or the lower-cased field name; a `-` key drops the field.
|
||||
func metasOf(st *ast.StructType, types map[string]*ast.StructType) []fieldMeta {
|
||||
// toml tag, or the lower-cased field name; a `-` key drops the field. An
|
||||
// embedded struct without a tag name flattens into its parent, the way the
|
||||
// library inlines it, while a tagged one keeps its own section.
|
||||
func metasOf(st *ast.StructType, types map[string]*ast.StructType) ([]fieldMeta, error) {
|
||||
return flattenMetas(st, types, nil)
|
||||
}
|
||||
|
||||
// flattenMetas is metasOf with the chain of struct types currently being
|
||||
// flattened, which stops a struct that embeds itself, directly or through
|
||||
// another embedded type.
|
||||
func flattenMetas(st *ast.StructType, types map[string]*ast.StructType, chain map[*ast.StructType]bool) ([]fieldMeta, error) {
|
||||
if chain[st] {
|
||||
return nil, recursionError(st, types)
|
||||
}
|
||||
// A copy per branch: the chain is the path being flattened now, not the
|
||||
// set ever visited, so a type embedded in two siblings is not mistaken
|
||||
// for recursion.
|
||||
chain = maps.Clone(chain)
|
||||
if chain == nil {
|
||||
chain = map[*ast.StructType]bool{}
|
||||
}
|
||||
chain[st] = true
|
||||
var out []fieldMeta
|
||||
for _, field := range st.Fields.List {
|
||||
if len(field.Names) == 0 {
|
||||
// An untagged embedded struct flattens into the parent.
|
||||
if ident, ok := baseType(field.Type).(*ast.Ident); ok {
|
||||
if inner, ok := types[ident.Name]; ok {
|
||||
out = append(out, metasOf(inner, types)...)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
name := field.Names[0].Name
|
||||
if !ast.IsExported(name) {
|
||||
continue
|
||||
}
|
||||
tagText := ""
|
||||
if field.Tag != nil {
|
||||
tagText, _ = strconv.Unquote(field.Tag.Value)
|
||||
}
|
||||
toml := reflect.StructTag(tagText).Get("toml")
|
||||
key, opts := "", ""
|
||||
if toml != "" {
|
||||
key, opts, _ = strings.Cut(toml, ",")
|
||||
key, opts, _ := strings.Cut(toml, ",")
|
||||
if len(field.Names) == 0 {
|
||||
if key == "" {
|
||||
// An untagged embedded struct flattens into its parent.
|
||||
if ident, ok := baseType(field.Type).(*ast.Ident); ok {
|
||||
if inner, ok := types[ident.Name]; ok {
|
||||
metas, err := flattenMetas(inner, types, chain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, metas...)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if key == "-" {
|
||||
continue
|
||||
}
|
||||
// A tagged embedded struct is a section of its own; the tag
|
||||
// name is the only name it has.
|
||||
out = append(out, fieldMeta{
|
||||
key: key,
|
||||
comment: tagOption(opts, "comment="),
|
||||
def: tagOption(opts, "default="),
|
||||
typ: field.Type,
|
||||
})
|
||||
continue
|
||||
}
|
||||
if key == "-" {
|
||||
continue
|
||||
}
|
||||
if key == "" {
|
||||
key = strings.ToLower(name)
|
||||
// A field list may name several fields of one type, `Min, Max int`;
|
||||
// each name is one TOML key.
|
||||
for _, name := range field.Names {
|
||||
if !ast.IsExported(name.Name) {
|
||||
continue
|
||||
}
|
||||
fieldKey := key
|
||||
if fieldKey == "" {
|
||||
fieldKey = strings.ToLower(name.Name)
|
||||
}
|
||||
out = append(out, fieldMeta{
|
||||
key: fieldKey,
|
||||
comment: tagOption(opts, "comment="),
|
||||
def: tagOption(opts, "default="),
|
||||
typ: field.Type,
|
||||
})
|
||||
}
|
||||
out = append(out, fieldMeta{
|
||||
key: key,
|
||||
comment: tagOption(opts, "comment="),
|
||||
def: tagOption(opts, "default="),
|
||||
typ: field.Type,
|
||||
})
|
||||
}
|
||||
return out
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// recursionError names the struct type that reached itself. Such a type has
|
||||
// no finite TOML template: every level would nest another copy of the same
|
||||
// shape.
|
||||
func recursionError(st *ast.StructType, types map[string]*ast.StructType) error {
|
||||
return fmt.Errorf("recursive type %s: the struct contains itself, so it has no finite template", typeName(st, types))
|
||||
}
|
||||
|
||||
// typeName names the declared struct type st refers to, and "anonymous
|
||||
// struct" for a literal one that no declaration names.
|
||||
func typeName(st *ast.StructType, types map[string]*ast.StructType) string {
|
||||
for name, t := range types {
|
||||
if t == st {
|
||||
return name
|
||||
}
|
||||
}
|
||||
return "anonymous struct"
|
||||
}
|
||||
|
||||
// tagOption returns the text a `name=` option carries in the option part of
|
||||
|
||||
Reference in New Issue
Block a user