github.com/octohelm/cuemod@v0.9.4/pkg/cueify/golang/pkg_extractor.go (about) 1 package golang 2 3 import ( 4 "context" 5 "go/ast" 6 "go/build" 7 "go/constant" 8 "go/parser" 9 "go/token" 10 "go/types" 11 "os" 12 "path/filepath" 13 "reflect" 14 "sort" 15 "strconv" 16 "strings" 17 "unicode" 18 19 cueast "cuelang.org/go/cue/ast" 20 cuetoken "cuelang.org/go/cue/token" 21 "github.com/go-courier/logr" 22 "github.com/pkg/errors" 23 "k8s.io/apimachinery/pkg/runtime/schema" 24 25 "github.com/octohelm/cuemod/pkg/cueify/golang/std" 26 "github.com/octohelm/cuemod/pkg/cuemod/builtin" 27 ) 28 29 type pkgExtractor struct { 30 *build.Package 31 32 fset *token.FileSet 33 Syntax []*ast.File 34 35 Types *types.Package 36 TypesInfo types.Info 37 38 CueTypes map[types.Type]cueast.Expr 39 40 cueTypes map[ast.Expr]cueast.Expr 41 42 GroupVersion *schema.GroupVersion 43 } 44 45 func (e *pkgExtractor) Extract(ctx context.Context) ([]*cueast.File, error) { 46 if err := e.load(ctx, token.NewFileSet()); err != nil { 47 return nil, err 48 } 49 50 files := make([]*cueast.File, 0) 51 52 for i := range e.GoFiles { 53 f := e.extractGoFile(ctx, e.GoFiles[i], e.Syntax[i]) 54 if f != nil { 55 files = append(files, f) 56 } 57 } 58 59 return files, nil 60 } 61 62 func (e *pkgExtractor) load(ctx context.Context, fset *token.FileSet) error { 63 e.fset = fset 64 e.Syntax = make([]*ast.File, len(e.GoFiles)) 65 66 for i := range e.GoFiles { 67 gofile := filepath.Join(e.Dir, e.GoFiles[i]) 68 data, err := os.ReadFile(gofile) 69 if err != nil { 70 return err 71 } 72 f, err := parser.ParseFile(e.fset, gofile, data, parser.ParseComments|parser.AllErrors) 73 if err != nil { 74 return err 75 } 76 e.Syntax[i] = f 77 } 78 79 conf := types.Config{ 80 Importer: newFakeImporter(), 81 IgnoreFuncBodies: true, 82 DisableUnusedImportCheck: true, 83 Error: func(err error) { 84 }, 85 } 86 87 e.TypesInfo.Defs = map[*ast.Ident]types.Object{} 88 e.TypesInfo.Uses = map[*ast.Ident]types.Object{} 89 e.TypesInfo.Types = map[ast.Expr]types.TypeAndValue{} 90 91 pkgTypes, _ := conf.Check(e.ImportPath, e.fset, e.Syntax, &e.TypesInfo) 92 //if err != nil { 93 // logr.FromContext(ctx).Debug("type checking error: %s", err) 94 //} 95 e.Types = pkgTypes 96 97 // GroupVersion 98 for i := range e.TypesInfo.Defs { 99 if i.Name == "SchemeGroupVersion" && i.Obj != nil { 100 if valueSpec, ok := i.Obj.Decl.(*ast.ValueSpec); ok { 101 if len(valueSpec.Values) == 1 { 102 if compositeLit, ok := valueSpec.Values[0].(*ast.CompositeLit); ok { 103 if selectorExpr, ok := compositeLit.Type.(*ast.SelectorExpr); ok { 104 if selectorExpr.Sel.Name == "GroupVersion" && len(compositeLit.Elts) == 2 { 105 for _, elt := range compositeLit.Elts { 106 if keyValueExpr, ok := elt.(*ast.KeyValueExpr); ok { 107 if key, ok := keyValueExpr.Key.(*ast.Ident); ok { 108 if tv, ok := e.TypesInfo.Types[keyValueExpr.Value]; ok { 109 v, _ := strconv.Unquote(tv.Value.String()) 110 111 if e.GroupVersion == nil { 112 e.GroupVersion = &schema.GroupVersion{} 113 } 114 115 switch key.Name { 116 case "Group": 117 e.GroupVersion.Group = v 118 case "Version": 119 e.GroupVersion.Version = v 120 } 121 } 122 } 123 } 124 } 125 } 126 } 127 } 128 } 129 } 130 } 131 } 132 133 return nil 134 } 135 136 func (e *pkgExtractor) extractGoFile(ctx context.Context, filename string, f *ast.File) *cueast.File { 137 file := &cueast.File{} 138 file.Filename = strings.Replace(filepath.Base(filename), filepath.Ext(filename), "_go_gen.cue", -1) 139 pkgDecl := &cueast.Package{Name: cueast.NewIdent(e.Name)} 140 141 genDecls := make([]cueast.Decl, 0) 142 143 addDecl := func(decl cueast.Decl) { 144 if decl == nil { 145 return 146 } 147 genDecls = append(genDecls, decl) 148 } 149 150 imports := importPaths{} 151 152 ctx = withImportPaths(ctx, imports) 153 154 for _, d := range f.Decls { 155 switch decl := d.(type) { 156 case *ast.GenDecl: 157 for i := range decl.Specs { 158 s := decl.Specs[i] 159 160 newline := i == 0 161 162 switch spec := s.(type) { 163 case *ast.TypeSpec: 164 if !ast.IsExported(spec.Name.Name) { 165 continue 166 } 167 168 if spec.Doc == nil { 169 spec.Doc = decl.Doc 170 } 171 172 addDecl( 173 e.def( 174 spec.Name.Name, 175 e.cueTypeFromAstType(ctx, spec.Type, spec.Name), 176 newline, 177 e.cueCommentGroup(spec.Doc, true), 178 ), 179 ) 180 case *ast.ValueSpec: 181 if decl.Tok != token.CONST { 182 continue 183 } 184 185 for i, name := range spec.Names { 186 if !ast.IsExported(name.Name) { 187 continue 188 } 189 190 if obj := e.objectOf(name); obj != nil { 191 var astValue ast.Expr 192 193 if len(spec.Values) != 0 { 194 astValue = spec.Values[i] 195 } 196 197 if c, ok := obj.(*types.Const); ok { 198 if v := e.cueValue(ctx, c, astValue); v != nil { 199 200 if len(spec.Names) == 1 && spec.Doc == nil { 201 spec.Doc = decl.Doc 202 } 203 204 addDecl( 205 e.def( 206 name.Name, 207 v, 208 newline, // new line when last 209 e.cueCommentGroup(spec.Doc, true), 210 e.cueCommentGroup(spec.Comment, false), 211 ), 212 ) 213 } 214 } 215 } 216 } 217 } 218 } 219 } 220 } 221 222 if len(genDecls) == 0 { 223 return nil 224 } 225 226 file.Decls = []cueast.Decl{pkgDecl} 227 228 if importDecl := imports.toImportDecl(); len(importDecl.Specs) > 0 { 229 file.Decls = append(file.Decls, importDecl) 230 } 231 232 file.Decls = append(file.Decls, genDecls...) 233 234 return file 235 } 236 237 func (e *pkgExtractor) cueTypeFromAstType(ctx context.Context, astType ast.Expr, defined *ast.Ident) cueast.Expr { 238 if e.cueTypes == nil { 239 e.cueTypes = map[ast.Expr]cueast.Expr{} 240 } 241 242 if tpe, ok := e.cueTypes[astType]; ok { 243 return tpe 244 } 245 246 tpe := e.makeRootType(ctx, astType, defined) 247 e.cueTypes[astType] = tpe 248 return tpe 249 } 250 251 func (e *pkgExtractor) makeRootType(ctx context.Context, astType ast.Expr, defined *ast.Ident) cueast.Expr { 252 if defined != nil { 253 if e.GroupVersion != nil { 254 ctx = withGroupVersionKind(ctx, &schema.GroupVersionKind{ 255 Group: e.GroupVersion.Group, 256 Version: e.GroupVersion.Version, 257 Kind: defined.Name, 258 }) 259 } 260 261 if obj := e.objectOf(defined); obj != nil { 262 tpe := e.makeTypeFromNamed(ctx, obj.Type().(*types.Named)) 263 264 if tpe != nil { 265 return tpe 266 } 267 } 268 } 269 270 return e.makeTypeFromAstType(ctx, astType) 271 } 272 273 func (e *pkgExtractor) makeTypeFromNamed(ctx context.Context, named *types.Named) cueast.Expr { 274 if altType := e.altType(ctx, named); altType != nil { 275 return altType 276 } 277 278 if enums := e.enumsOf(named); len(enums) > 0 { 279 return oneOf(enums...) 280 } 281 282 return nil 283 } 284 285 func (e *pkgExtractor) makeTypeFromAstType(ctx context.Context, astType ast.Expr) cueast.Expr { 286 switch x := (astType).(type) { 287 case *ast.SelectorExpr: 288 return e.ref(ctx, x) 289 case *ast.Ident: 290 if tv, ok := e.TypesInfo.Types[astType]; ok { 291 switch tv.Type.(type) { 292 case *types.Interface: 293 return any() 294 case *types.Basic: 295 return e.ident(x.Name, false) 296 case *types.Named: 297 return e.ident(x.Name, true) 298 } 299 } 300 case *ast.ArrayType: 301 if elm, ok := x.Elt.(*ast.Ident); ok { 302 if elm.Name == "byte" { 303 return e.ident("bytes", false) 304 } 305 } 306 307 elmType := e.cueTypeFromAstType(ctx, x.Elt, nil) 308 309 if elmType == nil { 310 return nil 311 } 312 313 // array 314 if x.Len != nil { 315 n, _ := strconv.Atoi(x.Len.(*ast.BasicLit).Value) 316 317 return cueast.NewBinExpr( 318 cuetoken.MUL, 319 newInt(n), 320 cueast.NewList(elmType), 321 ) 322 } 323 // slice 324 return cueast.NewList(&cueast.Ellipsis{Type: elmType}) 325 case *ast.MapType: 326 propType := e.cueTypeFromAstType(ctx, x.Key, nil) 327 elemType := e.cueTypeFromAstType(ctx, x.Value, nil) 328 329 if propType == nil || elemType == nil { 330 return nil 331 } 332 333 f := &cueast.Field{ 334 Label: cueast.NewList(propType), 335 Value: elemType, 336 } 337 338 cueast.SetRelPos(f, cuetoken.Blank) 339 340 s := cueast.NewStruct(f) 341 s.Lbrace = cuetoken.Blank.Pos() 342 s.Rbrace = cuetoken.Blank.Pos() 343 344 return s 345 case *ast.StarExpr: 346 // ptr 347 underlying := e.cueTypeFromAstType(ctx, x.X, nil) 348 if underlying == nil { 349 return nil 350 } 351 return oneOf(cueast.NewNull(), underlying) 352 case *ast.FuncType: 353 return nil 354 case *ast.InterfaceType: 355 return any() 356 case *ast.StructType: 357 st := &cueast.StructLit{ 358 Lbrace: cuetoken.Blank.Pos(), 359 Rbrace: cuetoken.Newline.Pos(), 360 } 361 e.addFieldsFromAstStructType(ctx, x, st) 362 363 if len(st.Elts) == 0 { 364 return nil 365 } 366 367 return st 368 default: 369 logr.FromContext(ctx).Warn(errors.Errorf("unsupported ast type %#v", x)) 370 } 371 372 return nil 373 } 374 375 func (e *pkgExtractor) addFieldsFromAstStructType(ctx context.Context, x *ast.StructType, st *cueast.StructLit) { 376 add := func(x cueast.Decl) { 377 st.Elts = append(st.Elts, x) 378 } 379 380 indirect := func(tpe ast.Expr) ast.Expr { 381 for { 382 p, ok := tpe.(*ast.StarExpr) 383 if !ok { 384 break 385 } 386 tpe = p.X 387 } 388 return tpe 389 } 390 391 for i := range x.Fields.List { 392 astField := x.Fields.List[i] 393 fieldType := astField.Type 394 395 tag := "" 396 if astField.Tag != nil { 397 tag, _ = strconv.Unquote(astField.Tag.Value) 398 } 399 400 names := make([]string, 0) 401 402 for _, name := range astField.Names { 403 if name.IsExported() { 404 names = append(names, name.Name) 405 } 406 } 407 408 anonymous := len(astField.Names) == 0 409 410 if anonymous { 411 switch x := fieldType.(type) { 412 case *ast.Ident: 413 if x.IsExported() { 414 names = append(names, x.Name) 415 } 416 case *ast.SelectorExpr: 417 if x.Sel.IsExported() { 418 names = append(names, x.Sel.Name) 419 } 420 } 421 } 422 423 for _, goFieldName := range names { 424 fieldName, omitempty, hasNamedTag := getName(goFieldName, tag) 425 426 if fieldName == "-" { 427 continue 428 } 429 430 if anonymous && (!hasNamedTag || isInline(tag)) { 431 typ := indirect(fieldType) 432 433 switch x := typ.(type) { 434 case *ast.StructType: 435 e.addFieldsFromAstStructType(ctx, x, st) 436 case *ast.Ident, *ast.SelectorExpr: 437 sel := e.ref(ctx, x) 438 if sel != nil { 439 embed := &cueast.EmbedDecl{Expr: sel} 440 if i > 0 { 441 cueast.SetRelPos(embed, cuetoken.NewSection) 442 } 443 add(embed) 444 } 445 default: 446 logr.FromContext(ctx).Warn(errors.Errorf("unimplemented embedding %s for type %T", goFieldName, x)) 447 } 448 449 continue 450 } 451 452 kind := cuetoken.COLON 453 if omitempty { 454 kind = cuetoken.OPTION 455 fieldType = indirect(fieldType) 456 } 457 458 typ := e.cueTypeFromAstType(ctx, fieldType, nil) 459 460 if typ == nil { 461 logr.FromContext(ctx).Warn(errors.Errorf("drop field %s, unsupport type %T", goFieldName, fieldType)) 462 continue 463 } 464 465 label := cueast.NewString(fieldName) 466 467 field := &cueast.Field{Label: label, Value: typ} 468 469 addComments(field, e.cueCommentGroup(astField.Doc, true), e.cueCommentGroup(astField.Comment, false)) 470 471 if kind == cuetoken.OPTION { 472 field.Constraint = cuetoken.OPTION 473 } 474 475 add(field) 476 } 477 } 478 } 479 480 func (e *pkgExtractor) objectOf(ident *ast.Ident) types.Object { 481 for i, obj := range e.TypesInfo.Defs { 482 if i == ident { 483 return obj 484 } 485 } 486 return nil 487 } 488 489 func (e *pkgExtractor) cueCommentGroup(c *ast.CommentGroup, doc bool) *cueast.CommentGroup { 490 if c == nil { 491 return nil 492 } 493 494 cg := &cueast.CommentGroup{ 495 Doc: doc, 496 Line: !doc, 497 } 498 499 if cg.Line { 500 cg.Position = 3 // after value 501 } 502 503 for _, comment := range c.List { 504 cg.List = append(cg.List, &cueast.Comment{ 505 Text: comment.Text, 506 }) 507 } 508 509 return cg 510 } 511 512 func (e *pkgExtractor) cueValue(ctx context.Context, c *types.Const, value ast.Expr) cueast.Expr { 513 v := c.Val() 514 515 switch v.Kind() { 516 case constant.Unknown: 517 return e.ref(ctx, value) 518 case constant.String: 519 return cueast.NewLit(cuetoken.STRING, v.String()) 520 case constant.Int: 521 return cueast.NewLit(cuetoken.INT, v.String()) 522 case constant.Float: 523 return cueast.NewLit(cuetoken.FLOAT, v.String()) 524 case constant.Bool: 525 b, _ := strconv.ParseBool(v.String()) 526 return cueast.NewBool(b) 527 } 528 529 logr.FromContext(ctx).Warn(errors.Errorf("invalid const value: %d %#v", v.Kind(), c)) 530 531 return nil 532 } 533 534 func (e *pkgExtractor) enumsOf(tpe *types.Named) []cueast.Expr { 535 names := make([]string, 0) 536 537 for ident, def := range e.TypesInfo.Defs { 538 c, ok := def.(*types.Const) 539 if !ok { 540 continue 541 } 542 543 if c.Type() != tpe { 544 continue 545 } 546 547 // skip private 548 if !ident.IsExported() { 549 continue 550 } 551 552 name := ident.Name 553 554 // skip hidden 555 if name[0] == '_' { 556 continue 557 } 558 559 names = append(names, name) 560 } 561 562 sort.Strings(names) 563 564 ids := make([]cueast.Expr, len(names)) 565 for i, name := range names { 566 ids[i] = e.ident(name, true) 567 } 568 return ids 569 } 570 571 func (e *pkgExtractor) altType(ctx context.Context, typ *types.Named) cueast.Expr { 572 methods := map[string]*types.Func{} 573 574 for i := 0; i < typ.NumMethods(); i++ { 575 fn := typ.Method(i) 576 577 if !fn.Exported() { 578 continue 579 } 580 581 methods[fn.Name()] = fn 582 } 583 584 for name, stringInterface := range stringInterfaces { 585 if fn, ok := methods[name]; ok { 586 if stringInterface.Equal(fn) { 587 return cueast.NewIdent("string") 588 } 589 } 590 } 591 592 for name, topInterface := range topInterfaces { 593 if fn, ok := methods[name]; ok { 594 if topInterface.Equal(fn) { 595 return any() 596 } 597 } 598 } 599 600 return nil 601 } 602 603 func (e *pkgExtractor) sel(ctx context.Context, name string, pkgName string, importPath string) cueast.Expr { 604 if std.IsStd(importPath) { 605 // only builtin pkg can be select 606 if builtin.IsBuiltIn(importPath) { 607 pkgNameAlias := importPathsFromContext(ctx).add(importPath) 608 // std & builtin pkg don't use #prefix 609 return &cueast.SelectorExpr{X: e.ident(pkgNameAlias, false), Sel: e.ident(name, false)} 610 } 611 return nil 612 } 613 614 pkgNameAlias := importPathsFromContext(ctx).add(importPath) 615 616 sel := &cueast.SelectorExpr{X: e.ident(pkgNameAlias, false), Sel: e.ident(name, true)} 617 618 if gvk := groupVersionKindFromContext(ctx); gvk != nil { 619 if importPath == "k8s.io/apimachinery/pkg/apis/meta/v1" && name == "TypeMeta" { 620 return allOf(sel, cueast.NewStruct( 621 &cueast.Field{ 622 Label: cueast.NewString("apiVersion"), 623 Value: cueast.NewString(gvk.GroupVersion().String()), 624 }, 625 &cueast.Field{ 626 Label: cueast.NewString("kind"), 627 Value: cueast.NewString(gvk.Kind), 628 }, 629 )) 630 } 631 } 632 633 return sel 634 } 635 636 func (e *pkgExtractor) ref(ctx context.Context, expr ast.Expr) cueast.Expr { 637 switch x := expr.(type) { 638 case *ast.Ident: 639 return e.ident(x.Name, true) 640 case *ast.SelectorExpr: 641 from := x.X.(*ast.Ident) 642 if o, ok := e.TypesInfo.Uses[from]; ok { 643 if pkgName, ok := o.(*types.PkgName); ok { 644 return e.sel(ctx, x.Sel.Name, pkgName.Name(), pkgName.Imported().Path()) 645 } 646 } 647 } 648 return nil 649 } 650 651 func isInline(tag string) bool { 652 return hasFlag(tag, "json", "inline", 1) || hasFlag(tag, "yaml", "inline", 1) 653 } 654 655 func hasFlag(tag, key, flag string, offset int) bool { 656 if t := reflect.StructTag(tag).Get(key); t != "" { 657 split := strings.Split(t, ",") 658 if offset >= len(split) { 659 return false 660 } 661 for _, str := range split[offset:] { 662 if str == flag { 663 return true 664 } 665 } 666 } 667 return false 668 } 669 670 func getName(name string, tag string) (n string, omitempty bool, hasTag bool) { 671 tags := reflect.StructTag(tag) 672 for _, s := range []string{"json", "yaml"} { 673 if tag, ok := tags.Lookup(s); ok { 674 omitempty := false 675 676 if p := strings.Index(tag, ","); p >= 0 { 677 omitempty = strings.Contains(tag, "omitempty") 678 tag = tag[:p] 679 } 680 if tag != "" { 681 return tag, omitempty, true 682 } 683 } 684 } 685 return name, false, false 686 } 687 688 func (e *pkgExtractor) ident(name string, isDef bool) *cueast.Ident { 689 if isDef { 690 r := []rune(name)[0] 691 name = "#" + name 692 if !unicode.Is(unicode.Lu, r) { 693 name = "_" + name 694 } 695 } 696 return cueast.NewIdent(name) 697 } 698 699 func (e *pkgExtractor) def(name string, valueOrType cueast.Expr, newline bool, comments ...*cueast.CommentGroup) cueast.Decl { 700 if valueOrType == nil { 701 return nil 702 } 703 704 f := &cueast.Field{ 705 Label: e.ident(name, true), 706 Value: valueOrType, 707 } 708 709 addComments(f, comments...) 710 711 if newline { 712 cueast.SetRelPos(f, cuetoken.NewSection) 713 } 714 715 return f 716 }