From 9ed042499f7e101e5d45182bc1690bd5819bd573 Mon Sep 17 00:00:00 2001 From: Yuwen Ma Date: Sun, 12 Jan 2025 18:23:10 +0000 Subject: [PATCH] feat: add cmd to merge two go files (struct only) --- dev/tools/controllerbuilder/cmd/root.go | 2 + .../pkg/commands/apply/apply.go | 78 +++ dev/tools/controllerbuilder/pkg/io/cache.go | 45 ++ .../controllerbuilder/pkg/io/comments.go | 129 +++++ dev/tools/controllerbuilder/pkg/io/replace.go | 528 ++++++++++++++++++ 5 files changed, 782 insertions(+) create mode 100644 dev/tools/controllerbuilder/pkg/commands/apply/apply.go create mode 100644 dev/tools/controllerbuilder/pkg/io/cache.go create mode 100644 dev/tools/controllerbuilder/pkg/io/comments.go create mode 100644 dev/tools/controllerbuilder/pkg/io/replace.go diff --git a/dev/tools/controllerbuilder/cmd/root.go b/dev/tools/controllerbuilder/cmd/root.go index 17710a22f1..b8490727cf 100644 --- a/dev/tools/controllerbuilder/cmd/root.go +++ b/dev/tools/controllerbuilder/cmd/root.go @@ -18,6 +18,7 @@ import ( "fmt" "os" + "github.com/GoogleCloudPlatform/k8s-config-connector/dev/tools/controllerbuilder/pkg/commands/apply" "github.com/GoogleCloudPlatform/k8s-config-connector/dev/tools/controllerbuilder/pkg/commands/exportcsv" "github.com/GoogleCloudPlatform/k8s-config-connector/dev/tools/controllerbuilder/pkg/commands/generatecontroller" "github.com/GoogleCloudPlatform/k8s-config-connector/dev/tools/controllerbuilder/pkg/commands/generatedirectreconciler" @@ -44,6 +45,7 @@ func Execute() { rootCmd.AddCommand(updatetypes.BuildCommand(&generateOptions)) rootCmd.AddCommand(exportcsv.BuildCommand(&generateOptions)) rootCmd.AddCommand(exportcsv.BuildPromptCommand(&generateOptions)) + rootCmd.AddCommand(apply.BuildCommand(&generateOptions)) if err := rootCmd.Execute(); err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) diff --git a/dev/tools/controllerbuilder/pkg/commands/apply/apply.go b/dev/tools/controllerbuilder/pkg/commands/apply/apply.go new file mode 100644 index 0000000000..3411edfeaa --- /dev/null +++ b/dev/tools/controllerbuilder/pkg/commands/apply/apply.go @@ -0,0 +1,78 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apply + +import ( + "bytes" + "context" + "fmt" + "os" + + kccio "github.com/GoogleCloudPlatform/k8s-config-connector/dev/tools/controllerbuilder/pkg/io" + "github.com/GoogleCloudPlatform/k8s-config-connector/dev/tools/controllerbuilder/pkg/options" + "github.com/spf13/cobra" +) + +type ApplyOptions struct { + *options.GenerateOptions + SrcFile string + DestFile string +} + +func (o *ApplyOptions) BindFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.DestFile, "dest-file", o.DestFile, "destination file path to write the gocode to, default to ./apis/ to update the Config Connector types") + cmd.Flags().StringVar(&o.SrcFile, "src-file", o.SrcFile, "src file path to read the new gocode from") +} + +func BuildCommand(baseOptions *options.GenerateOptions) *cobra.Command { + opt := &ApplyOptions{ + GenerateOptions: baseOptions, + } + + if err := opt.InitDefaults(); err != nil { + fmt.Fprintf(os.Stderr, "Error initializing defaults: %v\n", err) + os.Exit(1) + } + + cmd := &cobra.Command{ + Use: "apply", + Short: "[ALPHA] Write go code from src dir to dest dir", + PreRunE: func(cmd *cobra.Command, args []string) error { + if opt.SrcFile == "" { + return fmt.Errorf("--src-file is required") + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + if err := Apply(ctx, opt); err != nil { + return err + } + return nil + }, + } + opt.BindFlags(cmd) + + return cmd +} + +func Apply(ctx context.Context, o *ApplyOptions) error { + rawData, err := os.ReadFile(o.SrcFile) + if err != nil { + return err + } + return kccio.UpdateGoFile(ctx, o.DestFile, bytes.NewBuffer(rawData)) + +} diff --git a/dev/tools/controllerbuilder/pkg/io/cache.go b/dev/tools/controllerbuilder/pkg/io/cache.go new file mode 100644 index 0000000000..1754d1adc8 --- /dev/null +++ b/dev/tools/controllerbuilder/pkg/io/cache.go @@ -0,0 +1,45 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package io + +import ( + "context" + "fmt" + "os" +) + +// WriteToCache write `out` to a temporary file under `dir`. +func WriteToCache(ctx context.Context, dir string, out string, namePattern string) (string, error) { + if _, err := os.Stat(dir); os.IsNotExist(err) { + if err := os.Mkdir(dir, 0755); err != nil { + return "", fmt.Errorf("create dir %s: %w", dir, err) + } + } + if namePattern == "" { + namePattern = "unnamed-*" + } + tmpFile, err := os.CreateTemp(dir, namePattern) + if err != nil { + return "", fmt.Errorf("create file under %s: %w", dir, err) + } + defer tmpFile.Close() + + fmt.Printf("out %s\n", out) + if _, err := tmpFile.WriteString(out); err != nil { + return "", fmt.Errorf("write to file %s: %w", tmpFile.Name(), err) + } + + return tmpFile.Name(), nil +} diff --git a/dev/tools/controllerbuilder/pkg/io/comments.go b/dev/tools/controllerbuilder/pkg/io/comments.go new file mode 100644 index 0000000000..58d89221f1 --- /dev/null +++ b/dev/tools/controllerbuilder/pkg/io/comments.go @@ -0,0 +1,129 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package io + +import ( + "go/ast" + "go/token" +) + +type StructComment struct { + Name *ast.Ident + Comment *ast.CommentGroup + Fields map[string]*ast.CommentGroup +} + +func (s *StructComment) SetCommentPos(offset token.Pos) token.Pos { + newComments := []*ast.Comment{} + if s.Comment == nil { + return offset + } + for _, c := range s.Comment.List { + c.Slash = offset + offset += c.End() + 1 + newComments = append(newComments, c) + } + s.Comment = &ast.CommentGroup{List: newComments} + return offset +} + +func MergeStructComments(oldComments, newComments map[string]*StructComment) map[string]*StructComment { + merged := make(map[string]*StructComment) + for k, v := range oldComments { + merged[k] = v + } + + // If the new Comment is non empty, override the old comment. + for k, v := range newComments { + merged[k] = v + } + return merged +} + +// ast Docs and comments are not binding with the Decl but the relative position of the Node, +// so we have to handle it specifically. +func MapGoComments(f ast.Node) map[string]*StructComment { + comments := make(map[string]*StructComment) + + ast.Inspect(f, func(n ast.Node) bool { + genDecl, ok := n.(*ast.GenDecl) + if !ok || genDecl.Tok != token.TYPE { + // TODO: support IMPORT, CONST, VAR + return true + } + c := mapStructComments(genDecl) + if c == nil { + return true + } + comments[c.Name.Name] = c + return true + }) + return comments +} + +func mapStructComments(g *ast.GenDecl) *StructComment { + if g == nil { + return nil + } + c := &StructComment{ + Fields: make(map[string]*ast.CommentGroup), + } + + if g.Doc != nil { + c.Comment = ResetCommentsPos(g.Doc) + } + + for _, spec := range g.Specs { + typeSpec, ok := spec.(*ast.TypeSpec) + if !ok { + continue + } + c.Name = typeSpec.Name + for _, field := range typeSpec.Type.(*ast.StructType).Fields.List { + if field.Doc == nil { + continue + } + var name *ast.Ident + + if field.Names == nil { + // Anonymous name + name = field.Type.(*ast.SelectorExpr).Sel + } else { + // Field could has multiple names like `A, B string`. Use the first one + name = field.Names[0] + } + c.Fields[name.Name] = field.Doc + c.Comment = ResetCommentsPos(field.Doc) + } + } + if c.Name == nil { + return nil + } + return c +} + +func ResetCommentsPos(comments *ast.CommentGroup) *ast.CommentGroup { + if comments == nil { + return nil + } + newComments := []*ast.Comment{} + for _, c := range comments.List { + c.Slash = token.NoPos + newComments = append(newComments, c) + } + return &ast.CommentGroup{ + List: newComments, + } +} diff --git a/dev/tools/controllerbuilder/pkg/io/replace.go b/dev/tools/controllerbuilder/pkg/io/replace.go new file mode 100644 index 0000000000..6f77079457 --- /dev/null +++ b/dev/tools/controllerbuilder/pkg/io/replace.go @@ -0,0 +1,528 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package io + +import ( + "bytes" + "context" + "errors" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "io" + "os" + "regexp" + "strings" + + "k8s.io/klog/v2" +) + +// UpdateGoFile update the `fileName` gofile with the content in `output`. +func UpdateGoFile(ctx context.Context, file string, output *bytes.Buffer) error { + klog := klog.FromContext(ctx) + + goCode := extractGoCode(output.Bytes()) + if goCode == "" { + return fmt.Errorf("no go code in %s", output) + } + klog.Info("update go file", "path", file) + // TODO: replace other go type like function, var, import.. + if err := rebuild(file, goCode); err != nil { + return err + } + return nil +} + +// The data is normally a combination of go code (with triple quote code blocks marker ```go ... ```) +// and wording explanation for the recommendation. +func extractGoCode(data []byte) string { + re := regexp.MustCompile("(?s)```go\n(.*?)```") + matches := re.FindStringSubmatch(string(data)) + if len(matches) == 2 { + return matches[1] + } + if len(matches) > 2 { + klog.Info("more than one gocode snippets found") + } + return "" +} + +// Rebuild the entire go file. +// Known problem: Comments in ast.Node are relative position, and can be misplaced by go/printer when refactoring go file. +// https://github.com/golang/go/issues/20744/. Potentially lib (we'd better not use) https://github.com/dave/dst +// To fix the problem, we can calculate the go file offset and re-assign token.Pos to each comment, using Printer.CommentedNode to +// specifically write each node with its new comments back to dest file. +func rebuild(destFilePath string, src string) error { + destFset := token.NewFileSet() + destFile, err := parser.ParseFile(destFset, destFilePath, nil, parser.ParseComments) + if err != nil { + return fmt.Errorf("parsing destination file: %w", err) + } + + // Make sure src package exists otherwise the parser will fail + _, _, found := strings.Cut(src, "package") + if !found { + src = "package " + destFile.Name.Name + "\n" + src + } + newFset := token.NewFileSet() + newFile, err := parser.ParseFile(newFset, "", src, parser.ParseComments) + if err != nil { + return fmt.Errorf("parsing src file: %w", err) + } + + // TODO: Parse the entire package (rather than a single `file`) to avoid redeclaration errors for structs + // defined in different files within the same package. + fwriter, err := os.Create(destFilePath) + if err != nil { + return fmt.Errorf("failed to create file: %w", err) + } + defer fwriter.Close() + + newDecls := []ast.Decl{} + + oldComments := MapGoComments(destFile) + newComments := MapGoComments(newFile) + mergedComments := MergeStructComments(oldComments, newComments) + + newStructs := mapStruct(newFile) + + oldFileComments := destFile.Comments + if oldFileComments == nil { + oldFileComments = []*ast.CommentGroup{} + } + destFile.Comments = []*ast.CommentGroup{} + offset := token.Pos(0) + if oldFileComments != nil { + if strings.Contains(oldFileComments[0].Text(), "Copyright") { + destFile.Comments = append(destFile.Comments, oldFileComments[0]) + offset += oldFileComments[0].End() + 1 + } + } + offset += destFile.Package + 1 + + var errs error + visited := map[string]struct{}{} + + // TODO: add comments for VAR, CONST, FUNC and IMPORT. + ast.Inspect(destFile, func(n ast.Node) bool { + switch x := n.(type) { + case *ast.GenDecl: + switch x.Tok { + case token.VAR, token.CONST: + /* + newVarDecl := &ast.GenDecl{ + Tok: x.Tok, + Specs: []ast.Spec{}, + } + for _, spec := range x.Specs { + valueSpec, ok := spec.(*ast.ValueSpec) + if !ok { + return true + } + newSpec := &ast.ValueSpec{ + Names: valueSpec.Names, + Values: valueSpec.Values, + Type: valueSpec.Type, + } + newVarDecl.Specs = append(newVarDecl.Specs, newSpec) + }*/ + delta := x.Specs[len(x.Specs)-1].End() - x.Specs[0].Pos() + offset += delta + 1 + newDecls = append(newDecls, x) + case token.IMPORT: + /* + newImportDecl := &ast.GenDecl{ + Tok: token.IMPORT, + Specs: []ast.Spec{}, + } + for _, spec := range x.Specs { + importSpec, ok := spec.(*ast.ImportSpec) + if !ok { + return true + } + newSpec := &ast.ImportSpec{ + Name: importSpec.Name, + Path: importSpec.Path, + } + newImportDecl.Specs = append(newImportDecl.Specs, newSpec) + }*/ + delta := x.Specs[len(x.Specs)-1].End() - x.Specs[0].Pos() + offset += delta + 1 + newDecls = append(newDecls, x) + case token.TYPE: + for _, spec := range x.Specs { + typeSpec, ok := spec.(*ast.TypeSpec) + if !ok { + return true + } + var newStructDecl *ast.GenDecl + newStruct, ok := newStructs[typeSpec.Name.Name] + comments := mergedComments[typeSpec.Name.Name] + + if !ok { + newStructDecl, err = copyStruct(x, comments) + if err != nil { + errors.Join(errs, err) + return false + } + } else { + newStructDecl, err = mergeStruct(x, newStruct, comments) + if err != nil { + errors.Join(errs, err) + return false + } + } + offset = comments.SetCommentPos(offset) + if comments.Comment != nil { + newStructDecl.Doc = comments.Comment + } + typeStruct := newStructDecl.Specs[0].(*ast.TypeSpec) + for _, field := range typeStruct.Type.(*ast.StructType).Fields.List { + // Add field comment on top of the field. + newCommentList := []*ast.Comment{} + if field.Doc != nil { + for _, c := range field.Doc.List { + c.Slash = offset + offset += c.End() + 1 + newCommentList = append(newCommentList, c) + } + field.Doc = &ast.CommentGroup{List: newCommentList} + } + delta := field.End() - field.Pos() + offset += delta + 1 + } + + newDecls = append(newDecls, newStructDecl) + visited[typeSpec.Name.Name] = struct{}{} + } + + } + case *ast.FuncDecl: + newFunDecl := &ast.FuncDecl{ + Name: x.Name, + Type: x.Type, + Body: x.Body, + } + newFields := []*ast.Field{} + if x.Recv != nil { + for _, r := range x.Recv.List { + newField := &ast.Field{ + Names: r.Names, + Type: r.Type, + Tag: r.Tag, + } + newFields = append(newFields, newField) + } + } + newFunDecl.Recv = &ast.FieldList{ + List: newFields, + } + // TODO: Add comments to funcDecl + newDecls = append(newDecls, newFunDecl) + delta := x.Body.End() - x.Body.Pos() + offset += delta + 1 + case *ast.BadDecl: + newBadDecl := &ast.BadDecl{ + From: x.From, + To: x.To, + } + newDecls = append(newDecls, newBadDecl) + } + return true + }) + if errs != nil { + return err + } + + // TODO Add non struct type from newFile + ast.Inspect(newFile, func(n ast.Node) bool { + x, ok := n.(*ast.GenDecl) + if !ok || x.Tok != token.TYPE { + return true + } + for _, spec := range x.Specs { + typeSpec, ok := spec.(*ast.TypeSpec) + if !ok { + continue + } + if _, ok := visited[typeSpec.Name.Name]; ok { + continue + } + newDecls = append(newDecls, x) + } + return true + }) + + destFile.Decls = newDecls + + /* + if err := RebuildCommentPos(fwriter, destFset, destFile, mergedComments, offset); err != nil { + return err + }*/ + err = format.Node(fwriter, destFset, destFile) + if err != nil { + return fmt.Errorf("error formatting code: %w", err) + } + return nil +} + +func RebuildCommentPos(fwriter io.Writer, fset *token.FileSet, f *ast.File, comments map[string]*StructComment, offset token.Pos) error { + ast.Inspect(f, func(n ast.Node) bool { + switch x := n.(type) { + case *ast.GenDecl: + switch x.Tok { + case token.VAR, token.CONST, token.IMPORT: + delta := x.Specs[len(x.Specs)-1].End() - x.Specs[0].Pos() + offset += delta + 1 + case token.TYPE: + newfieldComments := []*ast.CommentGroup{} + + for _, spec := range x.Specs { + typeSpec, ok := spec.(*ast.TypeSpec) + if !ok { + return true + } + comments := comments[typeSpec.Name.Name] + offset = comments.SetCommentPos(offset) + + if comments.Comment != nil { + // f.Comments = append(f.Comments, comments.Comment) + x.Doc = comments.Comment + newfieldComments = append(newfieldComments, x.Doc) + } + for _, field := range typeSpec.Type.(*ast.StructType).Fields.List { + // Add field comment on top of the field. + newCommentList := []*ast.Comment{} + if field.Doc != nil { + for _, c := range field.Doc.List { + c.Slash = offset + offset += c.End() + 1 + newCommentList = append(newCommentList, c) + } + newfieldComments = append(newfieldComments, &ast.CommentGroup{List: newCommentList}) + } + + delta := field.End() - field.Pos() + offset += delta + 1 + } + } + } + case *ast.FuncDecl: + delta := x.Body.End() - x.Body.Pos() + offset += delta + 1 + return true + } + return true + }) + return nil +} + +func mapStruct(f *ast.File) map[string]*ast.GenDecl { + visited := map[string]*ast.GenDecl{} + ast.Inspect(f, func(n ast.Node) bool { + x, ok := n.(*ast.GenDecl) + if !ok || x.Tok != token.TYPE { + return true + } + + for _, spec := range x.Specs { + // StructType has is a single Spec + typeSpec, ok := spec.(*ast.TypeSpec) + if !ok { + // TODO Support ImportSpec and ValueSpec + continue + } + visited[typeSpec.Name.Name] = x + } + return true + }) + return visited +} + +func mergeStruct(oldGenDecl, newGenDecl *ast.GenDecl, comments *StructComment) (*ast.GenDecl, error) { + if newGenDecl == nil { + return oldGenDecl, nil + } + if oldGenDecl.Tok != token.TYPE { + return nil, fmt.Errorf("oldGenDecl is not TYPE struct ") + } + if newGenDecl.Tok != token.TYPE { + return nil, fmt.Errorf("newGenDecl is not TYPE struct ") + } + + oldTypeSpec, ok := oldGenDecl.Specs[0].(*ast.TypeSpec) + if !ok { + return nil, fmt.Errorf("currentGendDecl should contain TypeSpec") + } + newTypeSpec, ok := newGenDecl.Specs[0].(*ast.TypeSpec) + if !ok { + return nil, fmt.Errorf("newGenDecl should contain TypeSpec") + } + + mergedGenDecl := &ast.GenDecl{ + Tok: token.TYPE, + } + + // Map the new fields + replacements := make(map[*ast.Ident]*ast.Field) + for _, newField := range newTypeSpec.Type.(*ast.StructType).Fields.List { + if newField.Names == nil { + // The `metav1.TypeMeta` and `metav1.ListMeta` fields normally follows the no-name convention. + replacements[newField.Type.(*ast.SelectorExpr).Sel] = newField + continue + } + if len(newField.Names) > 1 { + names := []string{} + for _, n := range newField.Names { + names = append(names, n.Name) + } + fmt.Printf("struct %s has multi-name field %s", newTypeSpec.Name.Name, names) + } + replacements[newField.Names[0]] = newField + } + + newFields := []*ast.Field{} + + // Merge fields + visited := make(map[string]struct{}) + for _, oldfield := range oldTypeSpec.Type.(*ast.StructType).Fields.List { + var key *ast.Ident + if oldfield.Names == nil { + // anonymous field + key = oldfield.Type.(*ast.SelectorExpr).Sel + } else { + key = oldfield.Names[0] + } + + var field *ast.Field + if newfield, ok := replacements[key]; ok { + field = MergeField(oldfield, newfield) + } else { + field = oldfield + } + // Add comment separately because it is a relative position to the struct. + if comments != nil { + field.Doc = comments.Fields[key.Name] + } + visited[key.Name] = struct{}{} + newFields = append(newFields, field) + } + + // Add new fields + for key, newfield := range replacements { + if _, ok := visited[key.Name]; !ok { + field := newfield + if comments != nil { + field.Doc = comments.Fields[key.Name] + } + newFields = append(newFields, field) + } + } + mergedGenDecl.Specs = []ast.Spec{ + &ast.TypeSpec{ + Name: oldTypeSpec.Name, + Type: &ast.StructType{ + Fields: &ast.FieldList{ + List: newFields, + }, + }, + }, + } + return mergedGenDecl, nil +} + +func copyStruct(origin *ast.GenDecl, comments *StructComment) (*ast.GenDecl, error) { + if origin == nil { + return nil, nil + } + copy := &ast.GenDecl{ + Tok: token.TYPE, + } + + newSpecs := []ast.Spec{} + for _, spec := range origin.Specs { + typeSpec, ok := spec.(*ast.TypeSpec) + if !ok { + // TODO Support other types + continue + } + newFields := &ast.FieldList{ + List: []*ast.Field{}, + } + structType := typeSpec.Type.(*ast.StructType) + for _, field := range structType.Fields.List { + var key *ast.Ident + if field.Names == nil { + // anonymous field + key = field.Type.(*ast.SelectorExpr).Sel + } else { + key = field.Names[0] + } + /* + field.Type = &ast.SelectorExpr{ + X: field.Type.(*ast.SelectorExpr).X, + Sel: &ast.Ident{ + Name: key.Name, + // Assign offset to Namepos is essential to calculate the entire StructType Comment Pos. + NamePos: offset, + Obj: field.Type.(*ast.SelectorExpr).Sel.Obj, + }, + } + offset = comments.SetFieldPos(key.Name, offset) + */ + if comments != nil { + field.Doc = comments.Fields[key.Name] + } + newFields.List = append(newFields.List, field) + + } + newStructType := &ast.StructType{ + Fields: newFields, + Struct: structType.Struct, + } + newTypeSpec := &ast.TypeSpec{ + Name: typeSpec.Name, + Type: newStructType, + } + // delta := newTypeSpec.End() - newTypeSpec.Pos() + // offset += delta + 1 + newSpecs = append(newSpecs, newTypeSpec) + } + + copy.Specs = newSpecs + return copy, nil +} + +func MergeField(oldfield, newfield *ast.Field) *ast.Field { + f := &ast.Field{} + if newfield.Type != nil { + f.Type = newfield.Type + } else { + f.Type = oldfield.Type + } + if newfield.Names != nil { + f.Names = newfield.Names + } else { + f.Names = oldfield.Names + } + if newfield.Tag != nil { + f.Tag = newfield.Tag + } else { + f.Tag = oldfield.Tag + } + return f +}