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  }