|
| 1 | +/* |
| 2 | +Copyright 2026 The Crossplane Authors. |
| 3 | +
|
| 4 | +Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | +you may not use this file except in compliance with the License. |
| 6 | +You may obtain a copy of the License at |
| 7 | +
|
| 8 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +
|
| 10 | +Unless required by applicable law or agreed to in writing, software |
| 11 | +distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | +See the License for the specific language governing permissions and |
| 14 | +limitations under the License. |
| 15 | +*/ |
| 16 | + |
| 17 | +package generator |
| 18 | + |
| 19 | +import ( |
| 20 | + "go/ast" |
| 21 | + "go/format" |
| 22 | + "go/parser" |
| 23 | + "go/token" |
| 24 | + "strings" |
| 25 | + |
| 26 | + "github.com/crossplane/crossplane-runtime/v2/pkg/errors" |
| 27 | +) |
| 28 | + |
| 29 | +// accessorReceiver is the receiver variable name used by generated accessor |
| 30 | +// methods. A single letter cannot collide with any generated package import |
| 31 | +// alias, which are all multi-letter. |
| 32 | +const accessorReceiver = "o" |
| 33 | + |
| 34 | +// addAccessors generates GetX/SetX accessor methods for every field of every |
| 35 | +// struct type declared in the given Go source. Getters return the field's |
| 36 | +// (pointer) type as-is and setters take the same type, so the generated methods |
| 37 | +// reference only types already present in the file and never require new |
| 38 | +// imports. Type aliases are skipped: their Type is not a struct literal, so they |
| 39 | +// share the underlying struct's method set for free. |
| 40 | +func addAccessors(code string) (string, error) { |
| 41 | + fset := token.NewFileSet() |
| 42 | + f, err := parser.ParseFile(fset, "", code, parser.ParseComments) |
| 43 | + if err != nil { |
| 44 | + return "", errors.Wrap(err, "failed to parse Go code for accessors") |
| 45 | + } |
| 46 | + |
| 47 | + // Collect the methods that already exist on each type, so we never emit a |
| 48 | + // GetX/SetX that collides with a method oapi-codegen already generated |
| 49 | + // (e.g. GetAdditionalProperties, or union As/From/Merge helpers). A |
| 50 | + // duplicate method would make the package fail to compile. |
| 51 | + existing := collectExistingMethods(f) |
| 52 | + |
| 53 | + var b strings.Builder |
| 54 | + // Walk declarations in source order so the generated output is stable. |
| 55 | + for _, decl := range f.Decls { |
| 56 | + gen, ok := decl.(*ast.GenDecl) |
| 57 | + if !ok || gen.Tok != token.TYPE { |
| 58 | + continue |
| 59 | + } |
| 60 | + for _, spec := range gen.Specs { |
| 61 | + ts, ok := spec.(*ast.TypeSpec) |
| 62 | + if !ok { |
| 63 | + continue |
| 64 | + } |
| 65 | + // Skip type aliases (`type Foo = Bar`); only generate accessors for |
| 66 | + // struct type definitions. |
| 67 | + if ts.Assign.IsValid() { |
| 68 | + continue |
| 69 | + } |
| 70 | + st, ok := ts.Type.(*ast.StructType) |
| 71 | + if !ok || st.Fields == nil { |
| 72 | + continue |
| 73 | + } |
| 74 | + writeStructAccessors(&b, fset, ts.Name.Name, st, existing[ts.Name.Name]) |
| 75 | + } |
| 76 | + } |
| 77 | + |
| 78 | + if b.Len() == 0 { |
| 79 | + return code, nil |
| 80 | + } |
| 81 | + |
| 82 | + combined := code + "\n" + b.String() |
| 83 | + formatted, err := format.Source([]byte(combined)) |
| 84 | + if err != nil { |
| 85 | + return "", errors.Wrap(err, "failed to format generated accessors") |
| 86 | + } |
| 87 | + return string(formatted), nil |
| 88 | +} |
| 89 | + |
| 90 | +// collectExistingMethods returns, per receiver type name, the set of method |
| 91 | +// names already declared in the file. |
| 92 | +func collectExistingMethods(f *ast.File) map[string]map[string]bool { |
| 93 | + existing := map[string]map[string]bool{} |
| 94 | + for _, decl := range f.Decls { |
| 95 | + fn, ok := decl.(*ast.FuncDecl) |
| 96 | + if !ok || fn.Recv == nil || len(fn.Recv.List) != 1 { |
| 97 | + continue |
| 98 | + } |
| 99 | + recv := receiverTypeName(fn.Recv.List[0].Type) |
| 100 | + if recv == "" { |
| 101 | + continue |
| 102 | + } |
| 103 | + if existing[recv] == nil { |
| 104 | + existing[recv] = map[string]bool{} |
| 105 | + } |
| 106 | + existing[recv][fn.Name.Name] = true |
| 107 | + } |
| 108 | + return existing |
| 109 | +} |
| 110 | + |
| 111 | +// receiverTypeName returns the bare type name of a method receiver, stripping a |
| 112 | +// leading pointer if present (e.g. `*Foo` -> `Foo`). |
| 113 | +func receiverTypeName(e ast.Expr) string { |
| 114 | + if star, ok := e.(*ast.StarExpr); ok { |
| 115 | + e = star.X |
| 116 | + } |
| 117 | + if id, ok := e.(*ast.Ident); ok { |
| 118 | + return id.Name |
| 119 | + } |
| 120 | + return "" |
| 121 | +} |
| 122 | + |
| 123 | +// writeStructAccessors appends a getter and setter for each named field of the |
| 124 | +// given struct to b. Any accessor whose name already exists in skip is omitted |
| 125 | +// to avoid colliding with methods oapi-codegen already generated. |
| 126 | +func writeStructAccessors(b *strings.Builder, fset *token.FileSet, typeName string, st *ast.StructType, skip map[string]bool) { |
| 127 | + for _, field := range st.Fields.List { |
| 128 | + // Skip embedded/anonymous fields; generated models don't use them. |
| 129 | + if len(field.Names) == 0 { |
| 130 | + continue |
| 131 | + } |
| 132 | + |
| 133 | + var typ strings.Builder |
| 134 | + if err := format.Node(&typ, fset, field.Type); err != nil { |
| 135 | + // format.Node only fails on malformed nodes, which cannot occur for |
| 136 | + // a node we just parsed; skip defensively rather than panic. |
| 137 | + continue |
| 138 | + } |
| 139 | + fieldType := typ.String() |
| 140 | + |
| 141 | + for _, name := range field.Names { |
| 142 | + fieldName := name.Name |
| 143 | + |
| 144 | + // Getter. |
| 145 | + if !skip["Get"+fieldName] { |
| 146 | + b.WriteString("\n// Get" + fieldName + " returns the " + fieldName + " field.\n") |
| 147 | + b.WriteString("func (" + accessorReceiver + " *" + typeName + ") Get" + fieldName + "() " + fieldType + " {\n") |
| 148 | + b.WriteString("\treturn " + accessorReceiver + "." + fieldName + "\n") |
| 149 | + b.WriteString("}\n") |
| 150 | + } |
| 151 | + |
| 152 | + // Setter. |
| 153 | + if !skip["Set"+fieldName] { |
| 154 | + b.WriteString("\n// Set" + fieldName + " sets the " + fieldName + " field.\n") |
| 155 | + b.WriteString("func (" + accessorReceiver + " *" + typeName + ") Set" + fieldName + "(v " + fieldType + ") {\n") |
| 156 | + b.WriteString("\t" + accessorReceiver + "." + fieldName + " = v\n") |
| 157 | + b.WriteString("}\n") |
| 158 | + } |
| 159 | + } |
| 160 | + } |
| 161 | +} |
0 commit comments