Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update AST helper generation #7558

Merged
merged 42 commits into from
Mar 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
71482a7
it's a start
systay Feb 12, 2021
4cbdb20
apply pre- and post-switch
systay Feb 12, 2021
08e38d9
switch
systay Feb 12, 2021
0a1992e
extracted rewriter code to own file
systay Feb 12, 2021
05b0af3
visit struct fields that implement the interface
systay Feb 12, 2021
d72873d
support field with struct type
systay Feb 12, 2021
9290991
extracted rewriter generation into its own file
systay Feb 13, 2021
9c3dc09
added replacer methods
systay Feb 13, 2021
e2d23e2
support interface implementations that are not pointers
systay Feb 15, 2021
98e4e5b
add replacement methods for array fields
systay Feb 16, 2021
5c93919
added case statements for slice fields
systay Feb 16, 2021
2d66e2e
Addition of panic of value type implementation
frouioui Feb 19, 2021
999a36f
asthelpergen: improve integration tests
vmg Feb 17, 2021
21e902c
better tests
systay Feb 22, 2021
7af230c
benchmark for slice replacer
systay Feb 22, 2021
7b5f171
nicer replacement methods for slices
systay Feb 22, 2021
0116a12
Added replacer and cases for InterfaceSlice
GuptaManan100 Feb 23, 2021
c3c68be
moved code away from the generated file
systay Feb 23, 2021
0fb0313
support slices of non-AST elements
systay Feb 23, 2021
3f43634
use the new rewriter
systay Feb 23, 2021
34c6502
Merge remote-tracking branch 'upstream/master' into visitorgen-rewrite3
systay Feb 23, 2021
f2d4e68
refactored and addressed review comments
systay Feb 24, 2021
99b6ac0
Merge remote-tracking branch 'upstream/master' into visitorgen-rewrite3
systay Feb 24, 2021
ffe3527
Add deep clone capability to ASThelpergen
systay Feb 26, 2021
725fcd5
added support for basic types that implement interfaces
systay Feb 26, 2021
e2dc629
handle interface{} fields
systay Feb 26, 2021
044b550
final touch ups. produce the clone.go file for the AST
systay Feb 26, 2021
b42085d
code clean up; minor change for interface clone methods
systay Feb 26, 2021
b79366a
make the clone methods use less memory
systay Feb 27, 2021
858859e
added comment to clone methods
systay Feb 27, 2021
46c42d4
Added the option to excempt types from clone generation
systay Feb 27, 2021
dc14a65
remove the hand written clone and start using the generated one
systay Feb 27, 2021
09f3f0b
inline replacer methods for the rewriter
systay Feb 27, 2021
fce3959
Merge remote-tracking branch 'upstream/master' into deep-clone
systay Feb 27, 2021
aa88caf
made nextval as reference type in ast
harshit-gangal Mar 1, 2021
3e95345
dry
systay Mar 2, 2021
f4036ca
simplify the function used to visit slice elements
systay Mar 2, 2021
cd7c4b3
simplify the rewriter some more
systay Mar 2, 2021
f03d4b1
rewriter benchmark
systay Mar 2, 2021
9af91e0
Revert "simplify the rewriter some more"
systay Mar 2, 2021
5488cc6
added error to the return params of Rewrite
systay Mar 2, 2021
db053f7
return errors instead of panicking
systay Mar 2, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ parser:
make -C go/vt/sqlparser

visitor:
go generate go/vt/sqlparser/rewriter.go
go run ./go/tools/asthelpergen -in ./go/vt/sqlparser -iface vitess.io/vitess/go/vt/sqlparser.SQLNode -except "*ColName"

sizegen:
go run go/tools/sizegen/sizegen.go \
Expand Down
291 changes: 291 additions & 0 deletions go/tools/asthelpergen/asthelpergen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
/*
Copyright 2021 The Vitess Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"bytes"
"flag"
"fmt"
"go/types"
"io/ioutil"
"log"
"path"
"strings"

"github.com/dave/jennifer/jen"
"golang.org/x/tools/go/packages"
)

const licenseFileHeader = `Copyright 2021 The Vitess Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.`

type generator interface {
visitStruct(t types.Type, stroct *types.Struct) error
visitInterface(t types.Type, iface *types.Interface) error
visitSlice(t types.Type, slice *types.Slice) error
createFile(pkgName string) (string, *jen.File)
}

// astHelperGen finds implementations of the given interface,
// and uses the supplied `generator`s to produce the output code
type astHelperGen struct {
DebugTypes bool
mod *packages.Module
sizes types.Sizes
namedIface *types.Named
iface *types.Interface
gens []generator
}

func newGenerator(mod *packages.Module, sizes types.Sizes, named *types.Named, generators ...generator) *astHelperGen {
return &astHelperGen{
DebugTypes: true,
mod: mod,
sizes: sizes,
namedIface: named,
iface: named.Underlying().(*types.Interface),
gens: generators,
}
}

func findImplementations(scope *types.Scope, iff *types.Interface, impl func(types.Type) error) error {
for _, name := range scope.Names() {
obj := scope.Lookup(name)
if _, ok := obj.(*types.TypeName); !ok {
continue
}
baseType := obj.Type()
if types.Implements(baseType, iff) {
err := impl(baseType)
if err != nil {
return err
}
continue
}
pointerT := types.NewPointer(baseType)
if types.Implements(pointerT, iff) {
err := impl(pointerT)
if err != nil {
return err
}
continue
}
}
return nil
}

func (gen *astHelperGen) visitStruct(t types.Type, stroct *types.Struct) error {
for _, g := range gen.gens {
err := g.visitStruct(t, stroct)
if err != nil {
return err
}
}
return nil
}

func (gen *astHelperGen) visitSlice(t types.Type, slice *types.Slice) error {
for _, g := range gen.gens {
err := g.visitSlice(t, slice)
if err != nil {
return err
}
}
return nil
}

func (gen *astHelperGen) visitInterface(t types.Type, iface *types.Interface) error {
for _, g := range gen.gens {
err := g.visitInterface(t, iface)
if err != nil {
return err
}
}
return nil
}

// GenerateCode is the main loop where we build up the code per file.
func (gen *astHelperGen) GenerateCode() (map[string]*jen.File, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a comment for an exported function?

pkg := gen.namedIface.Obj().Pkg()
iface, ok := gen.iface.Underlying().(*types.Interface)
if !ok {
return nil, fmt.Errorf("expected interface, but got %T", gen.iface)
}

err := findImplementations(pkg.Scope(), iface, func(t types.Type) error {
switch n := t.Underlying().(type) {
case *types.Struct:
return gen.visitStruct(t, n)
case *types.Slice:
return gen.visitSlice(t, n)
case *types.Pointer:
strct, isStrct := n.Elem().Underlying().(*types.Struct)
if isStrct {
return gen.visitStruct(t, strct)
}
case *types.Interface:
return gen.visitInterface(t, n)
default:
// do nothing
}
return nil
})

if err != nil {
return nil, err
}

result := map[string]*jen.File{}
for _, g := range gen.gens {
file, code := g.createFile(pkg.Name())
fullPath := path.Join(gen.mod.Dir, strings.TrimPrefix(pkg.Path(), gen.mod.Path), file)
result[fullPath] = code
}

return result, nil
}

type typePaths []string

func (t *typePaths) String() string {
return fmt.Sprintf("%v", *t)
}

func (t *typePaths) Set(path string) error {
*t = append(*t, path)
return nil
}

func main() {
var patterns typePaths
var generate, except string
var verify bool

flag.Var(&patterns, "in", "Go packages to load the generator")
flag.StringVar(&generate, "iface", "", "Root interface generate rewriter for")
flag.BoolVar(&verify, "verify", false, "ensure that the generated files are correct")
flag.StringVar(&except, "except", "", "don't deep clone these types")
flag.Parse()

result, err := GenerateASTHelpers(patterns, generate, except)
if err != nil {
log.Fatal(err)
}

if verify {
for _, err := range VerifyFilesOnDisk(result) {
log.Fatal(err)
}
log.Printf("%d files OK", len(result))
} else {
for fullPath, file := range result {
if err := file.Save(fullPath); err != nil {
log.Fatalf("failed to save file to '%s': %v", fullPath, err)
}
log.Printf("saved '%s'", fullPath)
}
}
}

// VerifyFilesOnDisk compares the generated results from the codegen against the files that
// currently exist on disk and returns any mismatches
func VerifyFilesOnDisk(result map[string]*jen.File) (errors []error) {
for fullPath, file := range result {
existing, err := ioutil.ReadFile(fullPath)
if err != nil {
errors = append(errors, fmt.Errorf("missing file on disk: %s (%w)", fullPath, err))
continue
}

var buf bytes.Buffer
if err := file.Render(&buf); err != nil {
errors = append(errors, fmt.Errorf("render error for '%s': %w", fullPath, err))
continue
}

if !bytes.Equal(existing, buf.Bytes()) {
errors = append(errors, fmt.Errorf("'%s' has changed", fullPath))
continue
}
}
return errors
}

// GenerateASTHelpers loads the input code, constructs the necessary generators,
// and generates the rewriter and clone methods for the AST
func GenerateASTHelpers(packagePatterns []string, rootIface, exceptCloneType string) (map[string]*jen.File, error) {
loaded, err := packages.Load(&packages.Config{
Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesSizes | packages.NeedTypesInfo | packages.NeedDeps | packages.NeedImports | packages.NeedModule,
Logf: log.Printf,
}, packagePatterns...)

if err != nil {
return nil, err
}

scopes := make(map[string]*types.Scope)
for _, pkg := range loaded {
scopes[pkg.PkgPath] = pkg.Types.Scope()
}

pos := strings.LastIndexByte(rootIface, '.')
if pos < 0 {
return nil, fmt.Errorf("unexpected input type: %s", rootIface)
}

pkgname := rootIface[:pos]
typename := rootIface[pos+1:]

scope := scopes[pkgname]
if scope == nil {
return nil, fmt.Errorf("no scope found for type '%s'", rootIface)
}

tt := scope.Lookup(typename)
if tt == nil {
return nil, fmt.Errorf("no type called '%s' found in '%s'", typename, pkgname)
}

nt := tt.Type().(*types.Named)

iface := nt.Underlying().(*types.Interface)

interestingType := func(t types.Type) bool {
return types.Implements(t, iface)
}
rewriter := newRewriterGen(interestingType, nt.Obj().Name())
clone := newCloneGen(iface, scope, exceptCloneType)

generator := newGenerator(loaded[0].Module, loaded[0].TypesSizes, nt, rewriter, clone)
it, err := generator.GenerateCode()
if err != nil {
return nil, err
}

return it, nil
}
43 changes: 43 additions & 0 deletions go/tools/asthelpergen/asthelpergen_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
Copyright 2021 The Vitess Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"fmt"
"strings"
"testing"

"github.com/stretchr/testify/require"
)

func TestFullGeneration(t *testing.T) {
result, err := GenerateASTHelpers([]string{"./integration/..."}, "vitess.io/vitess/go/tools/asthelpergen/integration.AST", "*NoCloneType")
require.NoError(t, err)

verifyErrors := VerifyFilesOnDisk(result)
require.Empty(t, verifyErrors)

for _, file := range result {
contents := fmt.Sprintf("%#v", file)
require.Contains(t, contents, "http://www.apache.org/licenses/LICENSE-2.0")
applyIdx := strings.Index(contents, "func (a *application) apply(parent, node AST, replacer replacerFunc)")
cloneIdx := strings.Index(contents, "CloneAST(in AST) AST")
if applyIdx == 0 && cloneIdx == 0 {
t.Fatalf("file doesn't contain expected contents")
}
}
}
Loading