Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ cover.html

# macOS system files
.DS_Store

.idea
65 changes: 61 additions & 4 deletions internal/convgen/parse/parser.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package parse

import (
"errors"
"fmt"
"go/ast"
"go/build/constraint"
Expand Down Expand Up @@ -98,7 +99,10 @@ func (p *Parser) ParseFunc(expr ast.Expr, hasErr bool) (typeinfo.Func, error) {
return nil, codefmt.Errorf(p, expr, "cannot use %c as function", expr)
}

obj := p.Pkg().TypesInfo.ObjectOf(id)
obj, err := p.getFuncObj(id, expr)
if err != nil {
return nil, codefmt.Errorf(p, expr, "%s", err.Error())
}
fn_, err := typeinfo.FuncOf[typeinfo.BothXY](obj)
if err != nil {
return nil, codefmt.Errorf(p, expr, "%s", err.Error())
Expand Down Expand Up @@ -133,9 +137,12 @@ func (p *Parser) ParseErrWrap(expr ast.Expr) (typeinfo.Func, error) {
return nil, codefmt.Errorf(p, expr, "cannot use %c as error wrapper", expr)
}

obj := p.Pkg().TypesInfo.ObjectOf(id)
if _, ok := obj.(*types.Nil); ok {
return nil, codefmt.Errorf(p, expr, "cannot use nil as error wrapper")
obj, err := p.getFuncObj(id, expr)
if err != nil {
if errors.Is(err, errNilFuncObj) {
return nil, codefmt.Errorf(p, expr, "cannot use nil as error wrapper")
}
return nil, codefmt.Errorf(p, expr, "%s", err.Error())
}

fn_, err := typeinfo.FuncOf[typeinfo.OnlyX](obj)
Expand Down Expand Up @@ -204,6 +211,48 @@ func (p *Parser) ConvgenGoFiles() []*ast.File {
return files
}

var errNilFuncObj = errors.New("nil function object")

// getFuncObj returns the object of the function in the expression. If the expression is a generic function instance, it creates an object with a generic name,
// but a function literal signature
func (p *Parser) getFuncObj(id *ast.Ident, expr ast.Expr) (types.Object, error) {
obj := p.Pkg().TypesInfo.ObjectOf(id)
if _, ok := obj.(*types.Nil); ok {
return nil, errNilFuncObj
}
objType := obj.Type()
declarationSignature, ok := objType.(*types.Signature) // This holds the generic function declaration (func[T any](T) string)
if !ok {
return nil, fmt.Errorf("cannot get object signature of type %s", objType.String())
}
instanceType := p.Pkg().TypesInfo.TypeOf(expr)
instanceSignature, ok := instanceType.(*types.Signature) // This holds the instance (func (int) string)
if !ok {
return nil, fmt.Errorf("cannot get object signature of type %s", instanceType.String())
}
if declarationSignature.TypeParams().Len() > 0 {
var name strings.Builder
name.WriteString(obj.Name())
switch e := expr.(type) {
case *ast.IndexExpr:
name.WriteString("[")
name.WriteString(codefmt.New(p.pkg).Expr(e.Index))
name.WriteString("]")
case *ast.IndexListExpr:
name.WriteString("[")
for i, idx := range e.Indices {
if i > 0 {
name.WriteString(", ")
}
name.WriteString(codefmt.New(p.pkg).Expr(idx))
}
name.WriteString("]")
}
return types.NewFunc(obj.Pos(), obj.Pkg(), name.String(), instanceSignature), nil
}
return obj, nil
}

// hasGoBuildConvgen checks if the file has a "//go:build convgen" constraint.
func hasGoBuildConvgen(file *ast.File) bool {
ok := false
Expand Down Expand Up @@ -242,6 +291,14 @@ func tailIdent(expr ast.Expr) (*ast.Ident, bool) {
// foo.bar.baz
// ^^^
return tailIdent(expr.Sel)
case *ast.IndexExpr:
// foo[T]
// ^^^
return tailIdent(expr.X)
case *ast.IndexListExpr:
// foo[T, U]
// ^^^
return tailIdent(expr.X)
}
return nil, false
}
7 changes: 7 additions & 0 deletions testdata/analysis/DuplicateImportFunc/testdata.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ func int2string(int) string { return "" }

func string2int(string) (int, error) { return 0, nil }

func Identity[T any](x T) T { return x }

type (
TheInt = int
MyInt int
Expand All @@ -23,6 +25,11 @@ var _ = convgen.Module(
convgen.ImportFunc(int2string), // want `duplicate int to string converter`
convgen.ImportFunc(func(int) string { return "" }), // want `duplicate int to string converter`

// Generic function instantiations
convgen.ImportFunc(Identity[string]), // ok
convgen.ImportFunc(Identity[string]), // want `duplicate string to string converter`
convgen.ImportFunc(Identity[int]), // ok

// Type aliases and defined types
convgen.ImportFunc(func(TheInt) string { return "" }), // want `duplicate TheInt to string converter`
convgen.ImportFunc(func(MyInt) string { return "" }), // ok, because MyInt is different from int
Expand Down
60 changes: 60 additions & 0 deletions testdata/program/ImportFuncGeneric/main/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//go:build convgen

package main

import (
"fmt"
"github.com/sublee/convgen"
)

type FooStr struct {
Value string
}

type BarStr struct {
Value Generic[string]
}

type FooInt struct {
Value int
}

type BarInt struct {
Value Generic[int]
}

type Generic[T any] struct {
Value T
}

func wrapGeneric[T any](x T) Generic[T] {
return Generic[T]{Value: x}
}

func wrapGenericErr[T any](x T) (Generic[T], error) {
return Generic[T]{Value: x}, nil
}

func identity[T any](x T) T {
return x
}

var mod = convgen.Module(
convgen.ImportFunc(wrapGeneric[string]),
convgen.ImportFuncErr(wrapGenericErr[int]),
convgen.ImportFunc(identity[string]),
)
var IdentityTest = convgen.Struct[FooStr, FooStr](mod)
var WrapTest = convgen.Struct[FooStr, BarStr](mod)
var WrapErrTest = convgen.StructErr[FooInt, BarInt](mod)

func main() {
foo := IdentityTest(FooStr{"42"})
fmt.Println(foo.Value)

bar := WrapTest(FooStr{"42"})
fmt.Println(bar.Value.Value)

baz, _ := WrapErrTest(FooInt{42})
fmt.Println(baz.Value.Value)
}
3 changes: 3 additions & 0 deletions testdata/program/ImportFuncGeneric/want/program_output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
42
42
42