github.com/oam-dev/kubevela@v1.9.11/references/cuegen/convert.go (about) 1 /* 2 Copyright 2023 The KubeVela 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 cuegen 18 19 import ( 20 "fmt" 21 goast "go/ast" 22 gotoken "go/token" 23 gotypes "go/types" 24 "strconv" 25 "strings" 26 27 cueast "cuelang.org/go/cue/ast" 28 cuetoken "cuelang.org/go/cue/token" 29 ) 30 31 func (g *Generator) convertDecls(x *goast.GenDecl) (decls []Decl, _ error) { 32 // TODO(iyear): currently only support 'type' 33 if x.Tok != gotoken.TYPE { 34 return decls, nil 35 } 36 37 for _, spec := range x.Specs { 38 typeSpec, ok := spec.(*goast.TypeSpec) 39 if !ok { 40 continue 41 } 42 43 if g.opts.typeFilter != nil && !g.opts.typeFilter(typeSpec) { 44 continue 45 } 46 47 // only process struct 48 typ := g.pkg.TypesInfo.TypeOf(typeSpec.Name) 49 50 if err := supportedType(nil, typ); err != nil { 51 return nil, fmt.Errorf("unsupported type %s: %w", typeSpec.Name.Name, err) 52 } 53 54 named, ok := typ.(*gotypes.Named) 55 if !ok { 56 continue 57 } 58 59 switch t := named.Underlying().(type) { 60 case *gotypes.Struct: 61 lit, err := g.convert(t) 62 if err != nil { 63 return nil, err 64 } 65 66 decls = append(decls, &Struct{CommonFields: CommonFields{ 67 Expr: lit, 68 Name: typeSpec.Name.Name, 69 Doc: x.Doc, 70 Pos: cuetoken.Newline.Pos(), 71 }}) 72 default: 73 continue 74 } 75 } 76 77 return decls, nil 78 } 79 80 func (g *Generator) convert(typ gotypes.Type) (cueast.Expr, error) { 81 // if type is registered as special type, use it directly 82 if t, ok := g.opts.types[typ.String()]; ok { 83 switch t { 84 case TypeAny: 85 return Ident("_", false), nil 86 case TypeEllipsis: 87 return &cueast.StructLit{Elts: []cueast.Decl{&cueast.Ellipsis{}}}, nil 88 default: 89 return nil, fmt.Errorf("unsupported special cue type: %v", t) 90 } 91 } 92 93 switch t := typ.(type) { 94 case *gotypes.Basic: 95 return basicType(t), nil 96 case *gotypes.Named: 97 return g.convert(t.Underlying()) 98 case *gotypes.Struct: 99 return g.makeStructLit(t) 100 case *gotypes.Pointer: 101 expr, err := g.convert(t.Elem()) 102 if err != nil { 103 return nil, err 104 } 105 106 // generate null enum for pointer type 107 if g.opts.nullable { 108 return &cueast.BinaryExpr{ 109 X: cueast.NewNull(), 110 Op: cuetoken.OR, 111 Y: expr, 112 }, nil 113 } 114 return expr, nil 115 case *gotypes.Slice: 116 if t.Elem().String() == "byte" { 117 return Ident("bytes", false), nil 118 } 119 expr, err := g.convert(t.Elem()) 120 if err != nil { 121 return nil, err 122 } 123 return cueast.NewList(&cueast.Ellipsis{Type: expr}), nil 124 case *gotypes.Array: 125 if t.Elem().String() == "byte" { 126 // TODO: no way to constraint lengths of bytes for now, as regexps 127 // operate on Unicode, not bytes. So we need 128 // fmt.Fprint(e.w, fmt.Sprintf("=~ '^\C{%d}$'", x.Len())), 129 // but regexp does not support that. 130 // But translate to bytes, instead of [...byte] to be consistent. 131 return Ident("bytes", false), nil 132 } 133 134 expr, err := g.convert(t.Elem()) 135 if err != nil { 136 return nil, err 137 } 138 return &cueast.BinaryExpr{ 139 X: &cueast.BasicLit{ 140 Kind: cuetoken.INT, 141 Value: strconv.Itoa(int(t.Len())), 142 }, 143 Op: cuetoken.MUL, 144 Y: cueast.NewList(expr), 145 }, nil 146 case *gotypes.Map: 147 // cue map only support string as key 148 if b, ok := t.Key().Underlying().(*gotypes.Basic); !ok || b.Kind() != gotypes.String { 149 return nil, fmt.Errorf("unsupported map key type %s of %s", t.Key(), t) 150 } 151 152 expr, err := g.convert(t.Elem()) 153 if err != nil { 154 return nil, err 155 } 156 157 f := &cueast.Field{ 158 Label: cueast.NewList(Ident("string", false)), 159 Value: expr, 160 } 161 return &cueast.StructLit{ 162 Elts: []cueast.Decl{f}, 163 }, nil 164 case *gotypes.Interface: 165 // we don't process interface 166 return Ident("_", false), nil 167 } 168 169 return nil, fmt.Errorf("unsupported type %s", typ) 170 } 171 172 func (g *Generator) makeStructLit(x *gotypes.Struct) (*cueast.StructLit, error) { 173 st := &cueast.StructLit{ 174 Elts: make([]cueast.Decl, 0), 175 } 176 177 // if num of fields is 1, we don't need braces. Keep it simple. 178 if x.NumFields() > 1 { 179 st.Lbrace = cuetoken.Blank.Pos() 180 st.Rbrace = cuetoken.Newline.Pos() 181 } 182 183 err := g.addFields(st, x, map[string]struct{}{}) 184 if err != nil { 185 return nil, err 186 } 187 188 return st, nil 189 } 190 191 // addFields converts fields of go struct to CUE fields and add them to cue StructLit. 192 func (g *Generator) addFields(st *cueast.StructLit, x *gotypes.Struct, names map[string]struct{}) error { 193 comments := g.fieldComments(x) 194 195 for i := 0; i < x.NumFields(); i++ { 196 field := x.Field(i) 197 198 // skip unexported fields 199 if !field.Exported() { 200 continue 201 } 202 203 // TODO(iyear): support more complex tags and usages 204 opts := g.parseTag(x.Tag(i)) 205 206 // skip fields with "-" tag 207 if opts.Name == "-" { 208 continue 209 } 210 211 // if field name tag is empty, use Go field name 212 if opts.Name == "" { 213 opts.Name = field.Name() 214 } 215 216 // can't decl same field in the same scope 217 if _, ok := names[opts.Name]; ok { 218 return fmt.Errorf("field '%s' already exists, can not declare duplicate field name", opts.Name) 219 } 220 names[opts.Name] = struct{}{} 221 222 // process anonymous field with inline tag 223 if field.Anonymous() && opts.Inline { 224 if t, ok := field.Type().Underlying().(*gotypes.Struct); ok { 225 if err := g.addFields(st, t, names); err != nil { 226 return err 227 } 228 } 229 continue 230 } 231 232 var ( 233 expr cueast.Expr 234 err error 235 ) 236 switch { 237 // process field with enum tag 238 case opts.Enum != nil && len(opts.Enum) > 0: 239 expr, err = g.enumField(field.Type(), opts) 240 // process normal field 241 default: 242 expr, err = g.normalField(field.Type(), opts) 243 } 244 if err != nil { 245 return fmt.Errorf("field '%s': %w", opts.Name, err) 246 } 247 248 f := &cueast.Field{ 249 Label: Ident(opts.Name, false), 250 Value: expr, 251 } 252 253 // process field with optional tag(omitempty in json tag) 254 if opts.Optional { 255 f.Token = cuetoken.COLON 256 f.Optional = cuetoken.Blank.Pos() 257 } 258 259 makeComments(f, comments[i]) 260 261 st.Elts = append(st.Elts, f) 262 } 263 264 return nil 265 } 266 267 func (g *Generator) enumField(typ gotypes.Type, opts *tagOptions) (cueast.Expr, error) { 268 tt, ok := typ.(*gotypes.Basic) 269 if !ok { 270 // TODO(iyear): support more types 271 return nil, fmt.Errorf("enum value only support [int, float, string, bool]") 272 } 273 274 expr, err := basicLabel(tt, opts.Enum[0]) 275 if err != nil { 276 return nil, err 277 } 278 279 for _, v := range opts.Enum[1:] { 280 enumExpr, err := basicLabel(tt, v) 281 if err != nil { 282 return nil, err 283 } 284 285 // default value should be marked with * 286 if opts.Default != nil && *opts.Default == v { 287 enumExpr = &cueast.UnaryExpr{Op: cuetoken.MUL, X: enumExpr} 288 } 289 290 expr = &cueast.BinaryExpr{ 291 X: expr, 292 Op: cuetoken.OR, 293 Y: enumExpr, 294 } 295 } 296 297 return expr, nil 298 } 299 300 func (g *Generator) normalField(typ gotypes.Type, opts *tagOptions) (cueast.Expr, error) { 301 expr, err := g.convert(typ) 302 if err != nil { 303 return nil, err 304 } 305 306 // process field with default tag 307 if opts.Default != nil { 308 tt, ok := typ.(*gotypes.Basic) 309 if !ok { 310 // TODO(iyear): support more types 311 return nil, fmt.Errorf("default value only support [int, float, string, bool]") 312 } 313 314 defaultExpr, err := basicLabel(tt, *opts.Default) 315 if err != nil { 316 return nil, err 317 } 318 expr = &cueast.BinaryExpr{ 319 // default value should be marked with * 320 X: &cueast.UnaryExpr{Op: cuetoken.MUL, X: defaultExpr}, 321 Op: cuetoken.OR, 322 Y: expr, 323 } 324 } 325 326 return expr, nil 327 } 328 329 func supportedType(stack []gotypes.Type, t gotypes.Type) error { 330 // we expand structures recursively, so we can't support recursive types 331 for _, t0 := range stack { 332 if t0 == t { 333 return fmt.Errorf("recursive type %s", t) 334 } 335 } 336 stack = append(stack, t) 337 338 t = t.Underlying() 339 switch x := t.(type) { 340 case *gotypes.Basic: 341 if x.String() != "invalid type" { 342 return nil 343 } 344 return fmt.Errorf("unsupported type %s", t) 345 case *gotypes.Named: 346 return nil 347 case *gotypes.Pointer: 348 return supportedType(stack, x.Elem()) 349 case *gotypes.Slice: 350 return supportedType(stack, x.Elem()) 351 case *gotypes.Array: 352 return supportedType(stack, x.Elem()) 353 case *gotypes.Map: 354 if b, ok := x.Key().Underlying().(*gotypes.Basic); !ok || b.Kind() != gotypes.String { 355 return fmt.Errorf("unsupported map key type %s of %s", x.Key(), t) 356 } 357 return supportedType(stack, x.Elem()) 358 case *gotypes.Struct: 359 // Eliminate structs with fields for which all fields are filtered. 360 if x.NumFields() == 0 { 361 return nil 362 } 363 for i := 0; i < x.NumFields(); i++ { 364 f := x.Field(i) 365 if f.Exported() { 366 if err := supportedType(stack, f.Type()); err != nil { 367 return err 368 } 369 } 370 } 371 return nil 372 case *gotypes.Interface: 373 return nil 374 } 375 return fmt.Errorf("unsupported type %s", t) 376 } 377 378 // ----------comment---------- 379 380 type commentUnion struct { 381 comment *goast.CommentGroup 382 doc *goast.CommentGroup 383 } 384 385 // fieldComments returns the comments for each field in a go struct. 386 // 387 // The comments are same order as the fields. 388 func (g *Generator) fieldComments(x *gotypes.Struct) []*commentUnion { 389 comments := make([]*commentUnion, x.NumFields()) 390 391 st, ok := g.types[x] 392 if !ok { 393 return comments 394 } 395 396 for i, field := range st.Fields.List { 397 comments[i] = &commentUnion{comment: field.Comment, doc: field.Doc} 398 } 399 400 return comments 401 } 402 403 // makeComments adds comments to a cue node. 404 // 405 // go docs/comments are converted to cue comments. 406 func makeComments(node cueast.Node, c *commentUnion) { 407 if c == nil { 408 return 409 } 410 cg := make([]*cueast.Comment, 0) 411 412 if comment := makeComment(c.comment); comment != nil && len(comment.List) > 0 { 413 cg = append(cg, comment.List...) 414 } 415 if doc := makeComment(c.doc); doc != nil && len(doc.List) > 0 { 416 cg = append(cg, doc.List...) 417 } 418 419 // avoid nil comment groups which will cause panics 420 if len(cg) > 0 { 421 cueast.AddComment(node, &cueast.CommentGroup{List: cg}) 422 } 423 } 424 425 // makeComment converts a go CommentGroup to a cue CommentGroup. 426 // 427 // All /*-style comments are converted to //-style comments. 428 func makeComment(cg *goast.CommentGroup) *cueast.CommentGroup { 429 if cg == nil { 430 return nil 431 } 432 433 var comments []*cueast.Comment 434 435 for _, comment := range cg.List { 436 c := comment.Text 437 438 if len(c) < 2 { 439 continue 440 } 441 442 // Remove comment markers. 443 // The parser has given us exactly the comment text. 444 switch c[1] { 445 case '/': 446 // -style comment (no newline at the end) 447 comments = append(comments, &cueast.Comment{Text: c}) 448 449 case '*': 450 /*-style comment */ 451 c = c[2 : len(c)-2] 452 if len(c) > 0 && c[0] == '\n' { 453 c = c[1:] 454 } 455 456 lines := strings.Split(c, "\n") 457 458 // Find common space prefix 459 i := 0 460 line := lines[0] 461 for ; i < len(line); i++ { 462 if c := line[i]; c != ' ' && c != '\t' { 463 break 464 } 465 } 466 467 for _, l := range lines { 468 for j := 0; j < i && j < len(l); j++ { 469 if line[j] != l[j] { 470 i = j 471 break 472 } 473 } 474 } 475 476 // Strip last line if empty. 477 if n := len(lines); n > 1 && len(lines[n-1]) < i { 478 lines = lines[:n-1] 479 } 480 481 // Print lines. 482 for _, l := range lines { 483 if i >= len(l) { 484 comments = append(comments, &cueast.Comment{Text: "//"}) 485 continue 486 } 487 comments = append(comments, &cueast.Comment{Text: "// " + l[i:]}) 488 } 489 } 490 } 491 492 return &cueast.CommentGroup{List: comments} 493 }