github.com/unionj-cloud/go-doudou@v1.3.8-0.20221011095552-0088008e5b31/cmd/internal/astutils/funcs.go (about) 1 package astutils 2 3 import ( 4 "bufio" 5 "bytes" 6 "encoding/json" 7 "fmt" 8 "github.com/sirupsen/logrus" 9 "github.com/unionj-cloud/go-doudou/toolkit/constants" 10 "github.com/unionj-cloud/go-doudou/toolkit/stringutils" 11 "go/ast" 12 "go/format" 13 "golang.org/x/tools/imports" 14 "io/ioutil" 15 "os" 16 "path/filepath" 17 "regexp" 18 "strings" 19 "text/template" 20 "unicode" 21 ) 22 23 func GetImportStatements(input []byte) []byte { 24 reg := regexp.MustCompile("(?s)import \\((.*?)\\)") 25 if !reg.Match(input) { 26 return nil 27 } 28 matches := reg.FindSubmatch(input) 29 return matches[1] 30 } 31 32 func AppendImportStatements(src []byte, appendImports []byte) []byte { 33 reg := regexp.MustCompile("(?s)import \\((.*?)\\)") 34 if !reg.Match(src) { 35 return src 36 } 37 matches := reg.FindSubmatch(src) 38 old := matches[1] 39 re := regexp.MustCompile(`[\r\n]+`) 40 splits := re.Split(string(old), -1) 41 oldmap := make(map[string]struct{}) 42 for _, item := range splits { 43 oldmap[strings.TrimSpace(item)] = struct{}{} 44 } 45 splits = re.Split(string(appendImports), -1) 46 var newimps []string 47 for _, item := range splits { 48 key := strings.TrimSpace(item) 49 if _, ok := oldmap[key]; !ok { 50 newimps = append(newimps, "\t"+key) 51 } 52 } 53 if len(newimps) == 0 { 54 return src 55 } 56 appendImports = []byte(constants.LineBreak + strings.Join(newimps, constants.LineBreak) + constants.LineBreak) 57 return reg.ReplaceAllFunc(src, func(i []byte) []byte { 58 old = append([]byte("import ("), old...) 59 old = append(old, appendImports...) 60 old = append(old, []byte(")")...) 61 return old 62 }) 63 } 64 65 func GrpcRelatedModify(src []byte, metaName string, grpcSvcName string) []byte { 66 expr := fmt.Sprintf(`type %sImpl struct {`, metaName) 67 reg := regexp.MustCompile(expr) 68 unimpl := fmt.Sprintf("pb.Unimplemented%sServer", grpcSvcName) 69 if !strings.Contains(string(src), unimpl) { 70 appendUnimpl := []byte(constants.LineBreak + unimpl + constants.LineBreak) 71 src = reg.ReplaceAllFunc(src, func(i []byte) []byte { 72 return append([]byte(expr), appendUnimpl...) 73 }) 74 } 75 var_pb := fmt.Sprintf("var _ pb.%sServer = (*%sImpl)(nil)", grpcSvcName, metaName) 76 if !strings.Contains(string(src), var_pb) { 77 appendVarPb := []byte(constants.LineBreak + var_pb + constants.LineBreak) 78 src = reg.ReplaceAllFunc(src, func(i []byte) []byte { 79 return append(appendVarPb, []byte(expr)...) 80 }) 81 } 82 return src 83 } 84 85 func RestRelatedModify(src []byte, metaName string) []byte { 86 expr := fmt.Sprintf(`type %sImpl struct {`, metaName) 87 reg := regexp.MustCompile(expr) 88 var_ := fmt.Sprintf("var _ %s = (*%sImpl)(nil)", metaName, metaName) 89 if !strings.Contains(string(src), var_) { 90 appendVarPb := []byte(constants.LineBreak + var_ + constants.LineBreak) 91 src = reg.ReplaceAllFunc(src, func(i []byte) []byte { 92 return append(appendVarPb, []byte(expr)...) 93 }) 94 } 95 return src 96 } 97 98 // FixImport format source code and add missing import syntax automatically 99 func FixImport(src []byte, file string) { 100 var ( 101 res []byte 102 err error 103 ) 104 if res, err = imports.Process(file, src, &imports.Options{ 105 TabWidth: 8, 106 TabIndent: true, 107 Comments: true, 108 Fragment: true, 109 }); err == nil { 110 _ = ioutil.WriteFile(file, res, os.ModePerm) 111 return 112 } 113 logrus.Error(err) 114 _ = ioutil.WriteFile(file, src, os.ModePerm) 115 } 116 117 // GetMethodMeta get method name then new MethodMeta struct from *ast.FuncDecl 118 func GetMethodMeta(spec *ast.FuncDecl) MethodMeta { 119 methodName := ExprString(spec.Name) 120 mm := NewMethodMeta(spec.Type, ExprString) 121 mm.Name = methodName 122 return mm 123 } 124 125 // NewMethodMeta new MethodMeta struct from *ast.FuncDecl 126 func NewMethodMeta(ft *ast.FuncType, exprString func(ast.Expr) string) MethodMeta { 127 var params, results []FieldMeta 128 for _, param := range ft.Params.List { 129 pt := exprString(param.Type) 130 if len(param.Names) > 0 { 131 for _, name := range param.Names { 132 params = append(params, FieldMeta{ 133 Name: name.Name, 134 Type: pt, 135 Tag: "", 136 }) 137 } 138 continue 139 } 140 params = append(params, FieldMeta{ 141 Name: "", 142 Type: pt, 143 Tag: "", 144 }) 145 } 146 if ft.Results != nil { 147 for _, result := range ft.Results.List { 148 rt := exprString(result.Type) 149 if len(result.Names) > 0 { 150 for _, name := range result.Names { 151 results = append(results, FieldMeta{ 152 Name: name.Name, 153 Type: rt, 154 Tag: "", 155 }) 156 } 157 continue 158 } 159 results = append(results, FieldMeta{ 160 Name: "", 161 Type: rt, 162 Tag: "", 163 }) 164 } 165 } 166 return MethodMeta{ 167 Params: params, 168 Results: results, 169 } 170 } 171 172 // NewStructMeta new StructMeta from *ast.StructType 173 func NewStructMeta(structType *ast.StructType, exprString func(ast.Expr) string) StructMeta { 174 var fields []FieldMeta 175 re := regexp.MustCompile(`json:"(.*?)"`) 176 for _, field := range structType.Fields.List { 177 var fieldComments []string 178 if field.Doc != nil { 179 for _, comment := range field.Doc.List { 180 fieldComments = append(fieldComments, strings.TrimSpace(strings.TrimPrefix(comment.Text, "//"))) 181 } 182 } 183 184 fieldType := exprString(field.Type) 185 186 var tag string 187 var docName string 188 if field.Tag != nil { 189 tag = strings.Trim(field.Tag.Value, "`") 190 if re.MatchString(tag) { 191 docName = strings.TrimSuffix(re.FindStringSubmatch(tag)[1], ",omitempty") 192 } 193 } 194 195 if len(field.Names) > 0 { 196 for _, name := range field.Names { 197 _docName := docName 198 if stringutils.IsEmpty(_docName) { 199 _docName = name.Name 200 } 201 fields = append(fields, FieldMeta{ 202 Name: name.Name, 203 Type: fieldType, 204 Tag: tag, 205 Comments: fieldComments, 206 IsExport: unicode.IsUpper(rune(name.Name[0])), 207 DocName: _docName, 208 }) 209 } 210 } else { 211 splits := strings.Split(fieldType, ".") 212 name := splits[len(splits)-1] 213 fieldType = "embed:" + fieldType 214 _docName := docName 215 if stringutils.IsEmpty(_docName) { 216 _docName = name 217 } 218 fields = append(fields, FieldMeta{ 219 Name: name, 220 Type: fieldType, 221 Tag: tag, 222 Comments: fieldComments, 223 IsExport: unicode.IsUpper(rune(name[0])), 224 DocName: _docName, 225 }) 226 } 227 } 228 return StructMeta{ 229 Fields: fields, 230 } 231 } 232 233 // PackageMeta wraps package info 234 type PackageMeta struct { 235 Name string 236 } 237 238 // FieldMeta wraps field info 239 type FieldMeta struct { 240 Name string 241 Type string 242 Tag string 243 Comments []string 244 IsExport bool 245 // used in OpenAPI 3.0 spec as property name 246 DocName string 247 // Annotations of the field 248 Annotations []Annotation 249 // ValidateTag based on https://github.com/go-playground/validator 250 // please refer to its documentation https://pkg.go.dev/github.com/go-playground/validator/v10 251 ValidateTag string 252 } 253 254 // StructMeta wraps struct info 255 type StructMeta struct { 256 Name string 257 Fields []FieldMeta 258 Comments []string 259 Methods []MethodMeta 260 IsExport bool 261 // go-doudou version 262 Version string 263 } 264 265 // EnumMeta wraps struct info 266 type EnumMeta struct { 267 Name string 268 Values []string 269 } 270 271 // ExprString return string representation from ast.Expr 272 func ExprString(expr ast.Expr) string { 273 switch _expr := expr.(type) { 274 case *ast.Ident: 275 return _expr.Name 276 case *ast.StarExpr: 277 return "*" + ExprString(_expr.X) 278 case *ast.SelectorExpr: 279 return ExprString(_expr.X) + "." + _expr.Sel.Name 280 case *ast.InterfaceType: 281 return "interface{}" 282 case *ast.ArrayType: 283 if _expr.Len == nil { 284 return "[]" + ExprString(_expr.Elt) 285 } 286 return "[" + ExprString(_expr.Len) + "]" + ExprString(_expr.Elt) 287 case *ast.BasicLit: 288 return _expr.Value 289 case *ast.MapType: 290 return "map[" + ExprString(_expr.Key) + "]" + ExprString(_expr.Value) 291 case *ast.StructType: 292 structmeta := NewStructMeta(_expr, ExprString) 293 b, _ := json.Marshal(structmeta) 294 return "anonystruct«" + string(b) + "»" 295 case *ast.FuncType: 296 return NewMethodMeta(_expr, ExprString).String() 297 case *ast.ChanType: 298 var result string 299 if _expr.Dir == ast.SEND { 300 result += "chan<- " 301 } else if _expr.Dir == ast.RECV { 302 result += "<-chan " 303 } else { 304 result += "chan " 305 } 306 return result + ExprString(_expr.Value) 307 case *ast.Ellipsis: 308 if _expr.Ellipsis.IsValid() { 309 return "..." + ExprString(_expr.Elt) 310 } 311 panic(fmt.Sprintf("invalid ellipsis expression: %+v\n", expr)) 312 default: 313 panic(fmt.Sprintf("not support expression: %+v\n", expr)) 314 } 315 } 316 317 type Annotation struct { 318 Name string 319 Params []string 320 } 321 322 var reAnno = regexp.MustCompile(`@(\S+?)\((.*?)\)`) 323 324 func GetAnnotations(text string) []Annotation { 325 if !reAnno.MatchString(text) { 326 return nil 327 } 328 var annotations []Annotation 329 matches := reAnno.FindAllStringSubmatch(text, -1) 330 for _, item := range matches { 331 name := fmt.Sprintf(`@%s`, item[1]) 332 var params []string 333 if stringutils.IsNotEmpty(item[2]) { 334 params = strings.Split(strings.TrimSpace(item[2]), ",") 335 } 336 annotations = append(annotations, Annotation{ 337 Name: name, 338 Params: params, 339 }) 340 } 341 return annotations 342 } 343 344 // MethodMeta represents an api 345 type MethodMeta struct { 346 // Recv method receiver 347 Recv string 348 // Name method name 349 Name string 350 // Params when generate client code from openapi3 spec json file, Params holds all method input parameters. 351 // when generate client code from service interface in svc.go file, if there is struct type param, this struct type param will put into request body, 352 // then others will be put into url as query string. if there is no struct type param and the api is a get request, all will be put into url as query string. 353 // if there is no struct type param and the api is Not a get request, all will be put into request body as application/x-www-form-urlencoded data. 354 // specially, if there is one or more v3.FileModel or []v3.FileModel params, 355 // all will be put into request body as multipart/form-data data. 356 Params []FieldMeta 357 // Results response 358 Results []FieldMeta 359 // PathVars not support when generate client code from service interface in svc.go file 360 // when generate client code from openapi3 spec json file, PathVars is parameters in url as path variable. 361 PathVars []FieldMeta 362 // HeaderVars not support when generate client code from service interface in svc.go file 363 // when generate client code from openapi3 spec json file, HeaderVars is parameters in header. 364 HeaderVars []FieldMeta 365 // BodyParams not support when generate client code from service interface in svc.go file 366 // when generate client code from openapi3 spec json file, BodyParams is parameters in request body as query string. 367 BodyParams *FieldMeta 368 // BodyJSON not support when generate client code from service interface in svc.go file 369 // when generate client code from openapi3 spec json file, BodyJSON is parameters in request body as json. 370 BodyJSON *FieldMeta 371 // Files not support when generate client code from service interface in svc.go file 372 // when generate client code from openapi3 spec json file, Files is parameters in request body as multipart file. 373 Files []FieldMeta 374 // Comments of the method 375 Comments []string 376 // Path api path 377 // not support when generate client code from service interface in svc.go file 378 Path string 379 // QueryParams not support when generate client code from service interface in svc.go file 380 // when generate client code from openapi3 spec json file, QueryParams is parameters in url as query string. 381 QueryParams *FieldMeta 382 // Annotations of the method 383 Annotations []Annotation 384 } 385 386 const methodTmpl = `func {{ if .Recv }}(receiver {{.Recv}}){{ end }} {{.Name}}({{- range $i, $p := .Params}} 387 {{- if $i}},{{end}} 388 {{- $p.Name}} {{$p.Type}} 389 {{- end }}) ({{- range $i, $r := .Results}} 390 {{- if $i}},{{end}} 391 {{- $r.Name}} {{$r.Type}} 392 {{- end }})` 393 394 func (mm MethodMeta) String() string { 395 if stringutils.IsNotEmpty(mm.Recv) && stringutils.IsEmpty(mm.Name) { 396 panic("not valid code") 397 } 398 var isAnony bool 399 if stringutils.IsEmpty(mm.Name) { 400 isAnony = true 401 mm.Name = "placeholder" 402 } 403 t, _ := template.New("method.tmpl").Parse(methodTmpl) 404 var buf bytes.Buffer 405 _ = t.Execute(&buf, mm) 406 var res []byte 407 res, _ = format.Source(buf.Bytes()) 408 result := string(res) 409 if isAnony { 410 return strings.Replace(result, "func placeholder(", "func(", 1) 411 } 412 return result 413 } 414 415 // InterfaceMeta wraps interface info 416 type InterfaceMeta struct { 417 Name string 418 Methods []MethodMeta 419 Comments []string 420 } 421 422 // Visit visit each files 423 func Visit(files *[]string) filepath.WalkFunc { 424 return func(path string, info os.FileInfo, err error) error { 425 if err != nil { 426 logrus.Panicln(err) 427 } 428 if !info.IsDir() { 429 *files = append(*files, path) 430 } 431 return nil 432 } 433 } 434 435 // GetMod get module name from go.mod file 436 func GetMod() string { 437 var ( 438 f *os.File 439 err error 440 firstLine string 441 ) 442 dir, _ := os.Getwd() 443 mod := filepath.Join(dir, "go.mod") 444 if f, err = os.Open(mod); err != nil { 445 panic(err) 446 } 447 reader := bufio.NewReader(f) 448 firstLine, _ = reader.ReadString('\n') 449 return strings.TrimSpace(strings.TrimPrefix(firstLine, "module")) 450 } 451 452 // GetImportPath get import path of pkg from dir 453 func GetImportPath(dir string) string { 454 wd, _ := os.Getwd() 455 return GetMod() + strings.ReplaceAll(strings.TrimPrefix(dir, wd), `\`, `/`) 456 }