Как заставить типы работать правильно с сигнатурой, которая использует импортированные типы? - PullRequest
0 голосов
/ 06 мая 2018

types.Implements(impl1, iface) возвращает false, когда сигнатура определения интерфейса использует какой-либо тип, и возможная реализация находится в другом пакете и нуждается в импорте этого типа.

Структура проекта

awesome
├── main.go
├── pkg1
│   └── file.go
└── pkg2
    └── file.go

То есть, в папке awesome есть пакет main.

awesome/pkg1/file.go

package pkg1

import (
    "context"
)

// Interface generic object
type Interface interface {
    Method(context.Context) string
}

// Implementation1 implementation of Interface
type Implementation1 struct{}

// Method ...
func (Implementation1) Method(context.Context) string {
    return ""
}

awesome/pkg2/file.go выглядит как

package pkg2

import (
    "context"
)

// Implementation2 implementation for pkg1.Interface
type Implementation2 struct{}

// Method ...
func (Implementation2) Method(context.Context) string {
    return ""
}

А сейчас awesome/main.go

package main

import (
    "go/ast"
    "go/importer"
    "go/parser"
    "go/token"
    "go/types"
    "os"
    "os/user"
    "path/filepath"
    "fmt"
)

var fset = token.NewFileSet()

func getPath() string {
    gopath := os.Getenv("GOPATH")
    if len(gopath) == 0 {
        usr, err := user.Current()
        if err != nil {
            panic(err)
        }
        gopath = filepath.Join(usr.HomeDir, "go")
    }
    return filepath.Join(gopath, "src")
}

func getTypes(path string) *types.Package {
    fullpath := filepath.Join(getPath(), path)
    pkgs, err := parser.ParseDir(fset, fullpath, nil, parser.ParseComments)
    if err != nil {
        panic(err)
    }
    for _, pkg := range pkgs {
        config := &types.Config{
            Importer: importer.Default(),
        }
        info := types.Info{
            Types: map[ast.Expr]types.TypeAndValue{},
        }
        var files []*ast.File
        for _, file := range pkg.Files {
            files = append(files, file)
        }
        typeInfo, err := config.Check(path, fset, files, &info)
        if err != nil {
            panic(err)
        }
        return typeInfo
    }
    return nil
}

func main() {
    p1 := getTypes("awesome/pkg1")
    p2 := getTypes("awesome/pkg2")

    iface := p1.Scope().Lookup("Interface").(*types.TypeName).Type().(*types.Named).Underlying().(*types.Interface)
    impl1 := p1.Scope().Lookup("Implementation1").Type()
    impl2 := p2.Scope().Lookup("Implementation2").Type()
    fmt.Println("Implementation1 implements Interface", types.Implements(impl1, iface))
    fmt.Println("Implementation2 implements Interface", types.Implements(impl2, iface))
}

Вывод программы:

$ go install awesome
$ awesome
Implementation1 implements Interface true
Implementation2 implements Interface false

Это происходит из-за импортированного типа context.Context. Он работает нормально и возвращает true во втором случае, когда я изменяю тип аргумента Method на string, int, byte, что угодно или просто удаляю его. Что я делаю не так?

@ mkopriva ответ на самом деле направляет меня в правильном направлении: проверка разных типов приводит к разным типам, поэтому Implements не справляется с неосновными типами. Нам просто нужно повторно использовать объекты для проверки типов: это main.go на самом деле работает.

package main

import (
    "fmt"
    "go/ast"
    "go/importer"
    "go/parser"
    "go/token"
    "go/types"
    "os"
    "os/user"
    "path/filepath"
)

var fset = token.NewFileSet()
var config = &types.Config{
    Importer: importer.Default(),
}
var typeInfo = &types.Info{    
    Types:      map[ast.Expr]types.TypeAndValue{},
    Defs:       nil,
    Uses:       nil,
    Implicits:  nil,
    Selections: nil,
    Scopes:     nil,
    InitOrder:  nil,
}

func getPath() string {
    gopath := os.Getenv("GOPATH")
    if len(gopath) == 0 {
        usr, err := user.Current()
        if err != nil {
            panic(err)
        }
        gopath = filepath.Join(usr.HomeDir, "go")
    }
    return filepath.Join(gopath, "src")
}

func getTree(path string) *ast.Package {
    fullpath := filepath.Join(getPath(), path)
    pkgs, err := parser.ParseDir(fset, fullpath, nil, parser.ParseComments)
    if err != nil {
        panic(err)
    }
    for _, pkg := range pkgs {
        return pkg
    }
    return nil
}

func getTypes(pkg *ast.Package, path string) *types.Package {

    var files []*ast.File
    for _, file := range pkg.Files {
        files = append(files, file)
    }
    typeInfo, err := config.Check(path, fset, files, typeInfo)
    if err != nil {
        panic(err)
    }
    return typeInfo
}

func main() {
    const pkg1Path = "awesome/pkg1"
    t1 := getTree(pkg1Path)
    p1 := getTypes(t1, pkg1Path)
    const pkg2Path = "awesome/pkg2"
    t2 := getTree(pkg2Path)
    p2 := getTypes(t2, pkg2Path)

    iface := p1.Scope().Lookup("Interface").(*types.TypeName).Type().(*types.Named).Underlying().(*types.Interface)
    impl1 := p1.Scope().Lookup("Implementation1").Type()
    fmt.Printf("%s\n", impl1.(*types.Named).Method(0).Name())
    impl2 := p2.Scope().Lookup("Implementation2").Type()
    fmt.Println("Implementation1 implements Interface", types.Implements(impl1, iface))
    fmt.Println("Implementation2 implements Interface", types.Implements(impl2, iface))
}

Нам просто нужно поделиться *types.Config и *types.Info, чтобы процедура проверки работала с импортированным типом (который представлен как объект), а не регистрировала его снова как новый объект.

1 Ответ

0 голосов
/ 06 мая 2018

Я не уверен, является ли это предполагаемым поведением или это ошибка, но если вы покопаетесь в источнике, вы обнаружите, что types.Implements "терпит неудачу" здесь: https://github.com/golang/go/blob/master/src/go/types/predicates.go#L282-L287

Как видно из комментария, сравнение вернет true, только если

имена типов начинаются в том же объявлении типа

но если вы добавите туда операторы print для проверки значений x, y, вы увидите, что он сравнивает два различных указателя со значением types.Named того же типа context.Context. Тот факт, что информация о типах выделяется дважды, эквивалентен именованным типам , а не , происходящим из того же объявления. Причина, по которой у вас есть два экземпляра одного и того же именованного типа, заключается в том, что вы анализируете и проверяете два пакета по отдельности.

Таким образом, решение состоит в том, чтобы проанализировать и проверить оба пакета вместе. Я не уверен, является ли это жизнеспособным решением для вас или нет, но одну вещь, которую вы можете сделать, это объявить третий пакет, который импортирует два пакета, проанализировать и проверить этот третий пакет.

Например:

awesome
├── main.go
├── pkg1
│   └── file.go
├── pkg2
│   └── file.go
└── pkg3
    └── file.go

Тогда содержимое pkg3 будет выглядеть так:

awesome/pkg3/file.go

package pkg3

import (
    _ "awesome/pkg1"
    _ "awesome/pkg2"
)

А твоя главная вот так:

awesome/main.go (я добавил только те изменения, которые необходимо внести в оригинал)

func getTypes(path string) *types.Package {

    // ...

    for _, pkg := range pkgs {
        config := &types.Config{
            Importer: importer.For("source", nil),
        }

        // ...
    }
    return nil
}

// ...

func main() {
    p3 := getTypes("awesome/pkg3")

    p1 := p3.Imports()[0]
    p2 := p3.Imports()[1]

    // ...
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...