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

fix(injector): source import shadowed results in build error "X is not a type" #463

Merged
merged 1 commit into from
Dec 13, 2024
Merged
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
9 changes: 7 additions & 2 deletions internal/injector/aspect/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,13 @@ func (c *context) ParseSource(bytes []byte) (*dst.File, error) {
return c.sourceParser.Parse(bytes)
}

func (c *context) AddImport(path string, alias string) bool {
return c.refMap.AddImport(c.file, path, alias)
func (c *context) AddImport(path string, name string) bool {
nodeChain := []dst.Node{c.node}
for p := c.NodeChain.parent; p != nil; p = p.parent {
nodeChain = append(nodeChain, p.node)
}

return c.refMap.AddImport(c.file, nodeChain, path, name)
Comment on lines +228 to +233
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just pass the c.NodeChain? That would be equivalent, but incur fewer allocations? (Here you're basically copying a linked list into an array list...)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because of circular imports :/

}

func (c *context) AddLink(path string) bool {
Expand Down
11 changes: 7 additions & 4 deletions internal/injector/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@

// typeCheck runs the Go type checker on the provided files, and returns the
// Uses type information map that is built in the process.
func (i *Injector) typeCheck(fset *token.FileSet, files []*ast.File) (map[*ast.Ident]types.Object, error) {
func (i *Injector) typeCheck(fset *token.FileSet, files []*ast.File) (types.Info, error) {
pkg := types.NewPackage(i.ImportPath, i.Name)
typeInfo := types.Info{Uses: make(map[*ast.Ident]types.Object)}
typeInfo := types.Info{
Uses: make(map[*ast.Ident]types.Object),
Scopes: make(map[ast.Node]*types.Scope),
}

checkerCfg := types.Config{
GoVersion: i.GoVersion,
Expand All @@ -27,8 +30,8 @@
checker := types.NewChecker(&checkerCfg, fset, pkg, &typeInfo)

if err := checker.Files(files); err != nil {
return nil, fmt.Errorf("type-checking files: %w", err)
return types.Info{}, fmt.Errorf("type-checking files: %w", err)

Check warning on line 33 in internal/injector/check.go

View check run for this annotation

Codecov / codecov/patch

internal/injector/check.go#L33

Added line #L33 was not covered by tests
}

return typeInfo.Uses, nil
return typeInfo, nil
}
16 changes: 9 additions & 7 deletions internal/injector/injector.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"go/ast"
"go/importer"
"go/token"
"go/types"
"sync"

"github.com/DataDog/orchestrion/internal/injector/aspect"
Expand Down Expand Up @@ -84,7 +85,7 @@ func (i *Injector) InjectFiles(files []string) (map[string]InjectedFile, context
return nil, context.GoLangVersion{}, err
}

uses, err := i.typeCheck(fset, astFiles)
typeInfo, err := i.typeCheck(fset, astFiles)
if err != nil {
return nil, context.GoLangVersion{}, err
}
Expand All @@ -103,7 +104,7 @@ func (i *Injector) InjectFiles(files []string) (map[string]InjectedFile, context
go func(idx int, astFile *ast.File) {
defer wg.Done()

decorator := decorator.NewDecoratorWithImports(fset, i.ImportPath, gotypes.New(uses))
decorator := decorator.NewDecoratorWithImports(fset, i.ImportPath, gotypes.New(typeInfo.Uses))
dstFile, err := decorator.DecorateFile(astFile)
if err != nil {
errsMu.Lock()
Expand All @@ -112,7 +113,7 @@ func (i *Injector) InjectFiles(files []string) (map[string]InjectedFile, context
return
}

res, err := i.injectFile(decorator, dstFile)
res, err := i.injectFile(decorator, dstFile, typeInfo)
if err != nil {
errsMu.Lock()
defer errsMu.Unlock()
Expand Down Expand Up @@ -152,11 +153,11 @@ func (i *Injector) validate() error {

// injectFile injects code in the specified file. This method can be called concurrently by multiple goroutines,
// as is guarded by a sync.Mutex.
func (i *Injector) injectFile(decorator *decorator.Decorator, file *dst.File) (result, error) {
func (i *Injector) injectFile(decorator *decorator.Decorator, file *dst.File, typeInfo types.Info) (result, error) {
result := result{InjectedFile: InjectedFile{Filename: decorator.Filenames[file]}}

var err error
result.Modified, result.References, result.GoLang, err = i.applyAspects(decorator, file, i.RootConfig)
result.Modified, result.References, result.GoLang, err = i.applyAspects(decorator, file, i.RootConfig, typeInfo)
if err != nil {
return result, err
}
Expand All @@ -171,18 +172,19 @@ func (i *Injector) injectFile(decorator *decorator.Decorator, file *dst.File) (r
return result, nil
}

func (i *Injector) applyAspects(decorator *decorator.Decorator, file *dst.File, rootConfig map[string]string) (bool, typed.ReferenceMap, context.GoLangVersion, error) {
func (i *Injector) applyAspects(decorator *decorator.Decorator, file *dst.File, rootConfig map[string]string, typeInfo types.Info) (bool, typed.ReferenceMap, context.GoLangVersion, error) {
var (
chain *context.NodeChain
modified bool
references typed.ReferenceMap
references = typed.NewReferenceMap(decorator.Ast.Nodes, typeInfo.Scopes)
err error
)

pre := func(csor *dstutil.Cursor) bool {
if err != nil || csor.Node() == nil || isIgnored(csor.Node()) {
return false
}

root := chain == nil
chain = chain.Child(csor)
if root {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ aspects:
}()

syntheticReferences:
github.com/go-chi/chi/v5: true
Copy link
Contributor

Choose a reason for hiding this comment

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

That reference exists in the original file, so it should be be reported as a synthetic one? It's not a big deal here (it'd be filtered later one due to it already being present in the importcfg file, but still...)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is part of the idea to mitigate shadowing when dot imports are used

gopkg.in/DataDog/dd-trace-go.v1/contrib/go-chi/chi.v5: true

code: |-
Expand Down
47 changes: 47 additions & 0 deletions internal/injector/testdata/injector/import-shadowing/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
%YAML 1.1
---
aspects:
- id: Register
join-point:
function-call: database/sql.Register
advice:
- wrap-expression:
imports:
sqltrace: gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql
sql: database/sql
driver: database/sql/driver
template: |-
func(driverName string, driver driver.Driver) {
sql.Register(driverName, driver)
sqltrace.Register(driverName, driver)
}({{ index .AST.Args 0 }}, {{ index .AST.Args 1 }})

syntheticReferences:
database/sql/driver: true # shadowed import
gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql: true

code: |-
package test

import (
"database/sql"
"database/sql/driver"
)

var conn driver.Connector

func main() {
var driver string // shadowing import
sql.Register("foo", nil)

db1, err := sql.Open("foo", "bar")
if err != nil {
panic(err)
}
defer db1.Close()

println(driver)

db2 := sql.OpenDB(conn)
defer db2.Close()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
--- input.go
+++ output.go
@@ -1,15 +1,25 @@
+//line input.go:1:1
package test

import (
"database/sql"
- "database/sql/driver"
+//line <generated>:1
+ __orchestrion_driver "database/sql/driver"
+ __orchestrion_sqltrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql"
)

-var conn driver.Connector
+//line input.go:8
+var conn __orchestrion_driver.Connector

func main() {
var driver string // shadowing import
- sql.Register("foo", nil)
+//line <generated>:1
+ func(driverName string, driver __orchestrion_driver.Driver) {
+ sql.Register(driverName, driver)
+ __orchestrion_sqltrace.Register(driverName, driver)
+ }(
+//line input.go:12
+ "foo", nil)

db1, err := sql.Open("foo", "bar")
if err != nil {
82 changes: 71 additions & 11 deletions internal/injector/typed/refmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

import (
"fmt"
"go/ast"
"go/token"
"go/types"
"slices"
"strings"

Expand All @@ -23,6 +25,8 @@
ReferenceMap struct {
refs map[string]ReferenceKind
aliases map[string]string
nodeMap map[dst.Node]ast.Node
scopes map[ast.Node]*types.Scope
}
)

Expand All @@ -33,44 +37,100 @@
RelocationTarget ReferenceKind = false
)

// AddImport determines whether a new import declaration needs to be added to make the provided path
// available within the specified file. Returns true if that is the case. False if the import path
// is already available within the file.
func (r *ReferenceMap) AddImport(file *dst.File, path string, alias string) bool {
if hasImport(file, path) {
func NewReferenceMap(nodeMap map[dst.Node]ast.Node, scopes map[ast.Node]*types.Scope) ReferenceMap {
return ReferenceMap{nodeMap: nodeMap, scopes: scopes}
}

// AddImport takes a package import path and the name in file and the result of a recursive parent lookup.
// It first determines if the import is already present
// and if it has not been shadowed by a local declaration. If both conditions are met, the import is added to the
// reference map and the function returns true. Otherwise, it returns false.
func (r *ReferenceMap) AddImport(file *dst.File, nodes []dst.Node, path string, localName string) bool {
if len(nodes) == 0 {
panic("nodeChain must not be empty")

Check warning on line 50 in internal/injector/typed/refmap.go

View check run for this annotation

Codecov / codecov/patch

internal/injector/typed/refmap.go#L50

Added line #L50 was not covered by tests
}

// If the import is already present, has a meaningful alias or no alias,
// and is accessible from the current scope, we don't need to do anything.
prevLocalName, ok := hasImport(file, path)
if ok && prevLocalName != "." && prevLocalName != "_" && r.isImportInScope(nodes, path, localName) {
return false
}

// Register in this ReferenceMap
r.add(path, ImportStatement)
if alias != "_" {
if localName != "_" {
// We don't register blank aliases, as this is the default behavior anyway...
if r.aliases == nil {
r.aliases = make(map[string]string)
}
r.aliases[path] = fmt.Sprintf("__orchestrion_%s", alias)
r.aliases[path] = fmt.Sprintf("__orchestrion_%s", localName)
}

return true
}

func hasImport(file *dst.File, path string) bool {
// isImportInScope checks if the provided name is an import in the scope of the provided node
func (r *ReferenceMap) isImportInScope(nodes []dst.Node, path string, name string) bool {
if len(nodes) == 0 {
panic("nodes must not be empty")

Check warning on line 76 in internal/injector/typed/refmap.go

View check run for this annotation

Codecov / codecov/patch

internal/injector/typed/refmap.go#L76

Added line #L76 was not covered by tests
}

var (
scope *types.Scope
pos = r.nodeMap[nodes[0]].Pos()
)
for i := 0; i < len(nodes) && scope == nil; i++ {
node := nodes[i]
if funcDecl, ok := node.(*dst.FuncDecl); ok {
// Somehow scopes are not attached to FuncDecl nodes, so we need to look at the type ¯\_(シ)_/¯
node = funcDecl.Type
}

astNode, ok := r.nodeMap[node]
if !ok {
continue

Check warning on line 92 in internal/injector/typed/refmap.go

View check run for this annotation

Codecov / codecov/patch

internal/injector/typed/refmap.go#L92

Added line #L92 was not covered by tests
}

scope = r.scopes[astNode]
}

if scope == nil {
panic(fmt.Errorf("unable to find scope for node %T in parent chain", nodes[0]))

Check warning on line 99 in internal/injector/typed/refmap.go

View check run for this annotation

Codecov / codecov/patch

internal/injector/typed/refmap.go#L99

Added line #L99 was not covered by tests
}

_, obj := scope.LookupParent(name, pos)
if obj != nil {
if pkg, isImport := obj.(*types.PkgName); isImport {
return pkg.Imported().Path() == path
}
}

return false
}

// hasImport checks if the provided file already imports the provided path and its local name.
func hasImport(file *dst.File, path string) (string, bool) {
for _, spec := range file.Imports {
specPath, err := basiclit.String(spec.Path)
if err != nil {
continue
}
if specPath == path {
return true
name := ""
if spec.Name != nil {
name = spec.Name.Name
}
return name, true
}
}
return false
return "", false
}

// AddLink registers the provided path as a relocation target resolution source. If this path is
// already registered as an import, this method does nothing and returns false.
func (r *ReferenceMap) AddLink(file *dst.File, path string) bool {
if hasImport(file, path) {
if _, ok := hasImport(file, path); ok {
return false
}

Expand Down
Loading