2022-09-14 10:15:09 -04:00
package codegen
import (
"bytes"
"fmt"
2022-11-10 15:36:40 -05:00
"go/ast"
2022-09-14 10:15:09 -04:00
"go/format"
"go/parser"
"go/token"
"os"
"path/filepath"
2022-11-10 15:36:40 -05:00
"regexp"
2022-09-14 10:15:09 -04:00
"strings"
2022-12-22 07:20:02 -05:00
"github.com/dave/dst"
"github.com/dave/dst/dstutil"
2022-09-29 12:01:02 -04:00
"golang.org/x/tools/go/ast/astutil"
2022-09-14 10:15:09 -04:00
"golang.org/x/tools/imports"
)
type genGoFile struct {
path string
2022-09-29 12:01:02 -04:00
walker astutil . ApplyFunc
2022-09-14 10:15:09 -04:00
in [] byte
}
func postprocessGoFile ( cfg genGoFile ) ([] byte , error ) {
fname := filepath . Base ( cfg . path )
buf := new ( bytes . Buffer )
fset := token . NewFileSet ()
gf , err := parser . ParseFile ( fset , fname , string ( cfg . in ), parser . ParseComments )
if err != nil {
return nil , fmt . Errorf ( "error parsing generated file: %w" , err )
}
if cfg . walker != nil {
2022-09-29 12:01:02 -04:00
astutil . Apply ( gf , cfg . walker , nil )
2022-09-14 10:15:09 -04:00
err = format . Node ( buf , fset , gf )
if err != nil {
return nil , fmt . Errorf ( "error formatting Go AST: %w" , err )
}
} else {
buf = bytes . NewBuffer ( cfg . in )
}
byt , err := imports . Process ( fname , buf . Bytes (), nil )
if err != nil {
return nil , fmt . Errorf ( "goimports processing failed: %w" , err )
}
// Compare imports before and after; warn about performance if some were added
gfa , _ := parser . ParseFile ( fset , fname , string ( byt ), parser . ParseComments )
imap := make ( map [ string ] bool )
for _ , im := range gf . Imports {
imap [ im . Path . Value ] = true
}
var added [] string
for _ , im := range gfa . Imports {
if ! imap [ im . Path . Value ] {
added = append ( added , im . Path . Value )
}
}
if len ( added ) != 0 {
// TODO improve the guidance in this error if/when we better abstract over imports to generate
fmt . Fprintf ( os . Stderr , "The following imports were added by goimports while generating %s: \n\t%s\nRelying on goimports to find imports significantly slows down code generation. Consider adding these to the relevant template.\n" , cfg . path , strings . Join ( added , "\n\t" ))
}
return byt , nil
}
2022-11-10 15:36:40 -05:00
type prefixmod struct {
2022-11-15 08:48:31 -05:00
prefix string
replace string
2022-11-10 15:36:40 -05:00
rxp * regexp . Regexp
rxpsuff * regexp . Regexp
}
// PrefixDropper returns an astutil.ApplyFunc that removes the provided prefix
// string when it appears as a leading sequence in type names, var names, and
// comments in a generated Go file.
func PrefixDropper ( prefix string ) astutil . ApplyFunc {
return ( & prefixmod {
2022-11-15 08:48:31 -05:00
prefix : prefix ,
rxpsuff : regexp . MustCompile ( fmt . Sprintf ( `%s([a-zA-Z_]+)` , prefix )),
rxp : regexp . MustCompile ( fmt . Sprintf ( `%s([\s.,;-])` , prefix )),
}). applyfunc
}
// PrefixReplacer returns an astutil.ApplyFunc that removes the provided prefix
// string when it appears as a leading sequence in type names, var names, and
// comments in a generated Go file.
//
// When an exact match for prefix is found, the provided replace string
// is substituted.
func PrefixReplacer ( prefix , replace string ) astutil . ApplyFunc {
return ( & prefixmod {
prefix : prefix ,
replace : replace ,
2022-11-10 15:36:40 -05:00
rxpsuff : regexp . MustCompile ( fmt . Sprintf ( `%s([a-zA-Z_]+)` , prefix )),
rxp : regexp . MustCompile ( fmt . Sprintf ( `%s([\s.,;-])` , prefix )),
}). applyfunc
}
func depoint ( e ast . Expr ) ast . Expr {
if star , is := e .( * ast . StarExpr ); is {
return star . X
}
return e
}
func ( d prefixmod ) applyfunc ( c * astutil . Cursor ) bool {
n := c . Node ()
switch x := n .( type ) {
case * ast . ValueSpec :
d . handleExpr ( x . Type )
for _ , id := range x . Names {
d . do ( id )
}
case * ast . TypeSpec :
// Always do typespecs
d . do ( x . Name )
case * ast . Field :
// Don't rename struct fields. We just want to rename type declarations, and
// field value specifications that reference those types.
d . handleExpr ( x . Type )
case * ast . CommentGroup :
for _ , c := range x . List {
c . Text = d . rxpsuff . ReplaceAllString ( c . Text , "$1" )
2022-11-15 08:48:31 -05:00
if d . replace != "" {
c . Text = d . rxp . ReplaceAllString ( c . Text , d . replace + "$1" )
2022-11-10 15:36:40 -05:00
}
}
}
return true
}
func ( d prefixmod ) handleExpr ( e ast . Expr ) {
// Deref a StarExpr, if there is one
expr := depoint ( e )
switch x := expr .( type ) {
case * ast . Ident :
d . do ( x )
case * ast . ArrayType :
if id , is := depoint ( x . Elt ).( * ast . Ident ); is {
d . do ( id )
}
case * ast . MapType :
if id , is := depoint ( x . Key ).( * ast . Ident ); is {
d . do ( id )
}
if id , is := depoint ( x . Value ).( * ast . Ident ); is {
d . do ( id )
}
}
}
func ( d prefixmod ) do ( n * ast . Ident ) {
2022-11-15 08:48:31 -05:00
if n . Name != d . prefix {
n . Name = strings . TrimPrefix ( n . Name , d . prefix )
} else if d . replace != "" {
n . Name = d . replace
2022-11-10 15:36:40 -05:00
}
}
2022-12-22 07:20:02 -05:00
func isSingleTypeDecl ( gd * dst . GenDecl ) bool {
if gd . Tok == token . TYPE && len ( gd . Specs ) == 1 {
_ , is := gd . Specs [ 0 ].( * dst . TypeSpec )
return is
}
return false
}
func isAdditionalPropertiesStruct ( tspec * dst . TypeSpec ) ( dst . Expr , bool ) {
strct , is := tspec . Type .( * dst . StructType )
if is && len ( strct . Fields . List ) == 1 && strct . Fields . List [ 0 ]. Names [ 0 ]. Name == "AdditionalProperties" {
return strct . Fields . List [ 0 ]. Type , true
}
return nil , false
}
func DecoderCompactor () dstutil . ApplyFunc {
return func ( c * dstutil . Cursor ) bool {
f , is := c . Node ().( * dst . File )
if ! is {
return false
}
compact := make ( map [ string ] bool )
// walk the file decls
for _ , decl := range f . Decls {
if fd , is := decl .( * dst . FuncDecl ); is {
compact [ ddepoint ( fd . Recv . List [ 0 ]. Type ).( * dst . Ident ). Name ] = true
}
}
if len ( compact ) == 0 {
return false
}
replace := make ( map [ string ] dst . Expr )
// Walk again, looking for types we found
for _ , decl := range f . Decls {
if gd , is := decl .( * dst . GenDecl ); is && isSingleTypeDecl ( gd ) {
if tspec := gd . Specs [ 0 ].( * dst . TypeSpec ); compact [ tspec . Name . Name ] {
if expr , is := isAdditionalPropertiesStruct ( tspec ); is {
replace [ tspec . Name . Name ] = expr
}
}
}
}
dstutil . Apply ( f , func ( c * dstutil . Cursor ) bool {
switch x := c . Node ().( type ) {
case * dst . FuncDecl :
c . Delete ()
case * dst . GenDecl :
if isSingleTypeDecl ( x ) && compact [ x . Specs [ 0 ].( * dst . TypeSpec ). Name . Name ] {
c . Delete ()
}
case * dst . Field :
2023-01-03 02:12:48 -05:00
if id , is := ddepoint ( x . Type ).( * dst . Ident ); is {
2022-12-22 07:20:02 -05:00
if expr , has := replace [ id . Name ]; has {
x . Type = expr
}
}
}
return true
}, nil )
return false
}
}
func ddepoint ( e dst . Expr ) dst . Expr {
if star , is := e .( * dst . StarExpr ); is {
return star . X
}
return e
}