Skip to content

x/tools/go/packages: TypesInfo.Uses adds 3 to the line numbers of types.Objects from cgo files #74037

Open
@beaubrueggemann

Description

@beaubrueggemann

Go version

go version go1.24.4 darwin/amd64

Output of go env in your module/workspace:

AR='ar'
CC='clang'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='clang++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/Users/beaubrueggemann/Library/Caches/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/Users/beaubrueggemann/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -arch x86_64 -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/47/c0lsmsxn3_z86w657l519d6m0000gn/T/go-build1916904101=/tmp/go-build -gno-record-gcc-switches -fno-common'
GOHOSTARCH='amd64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMOD='/Users/beaubrueggemann/go/src/cgobug/go.mod'
GOMODCACHE='/Users/beaubrueggemann/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPATH='/Users/beaubrueggemann/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/local/go'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/beaubrueggemann/Library/Application Support/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/usr/local/go/pkg/tool/darwin_amd64'
GOVCS=''
GOVERSION='go1.24.4'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

Here's the most trivial/simple recipe I could come up with.

Create a cgobug module with three simple packages. Here is the directory structure:

cgobug/
│ 
├── go.mod
│ 
├── cgopkg/
│   └── cgopkg.go
│ 
├── other/
│   └── other.go
│ 
└── pkginfo/
    └── pkginfo.go

The go.mod file was created with a go mod init cgobug command, followed by go get golang.org/x/tools/go/packages, which produces:

module cgobug

go 1.23.0

toolchain go1.24.4

require (
	golang.org/x/mod v0.25.0 // indirect
	golang.org/x/sync v0.15.0 // indirect
	golang.org/x/tools v0.34.0 // indirect
)

Package cgopkg is a simple package that uses cgo (the initial example at the top of the C? Go? Cgo! blog post). It is defined in the file cgopkg.go:

package cgopkg

// #include <stdlib.h>
import "C"

func Random() int {
	return int(C.random())
}

func Seed(i int) {
	C.srandom(C.uint(i))
}

Package other is a simple package that uses package cgopkg. It is defined in the file other.go:

package other

import "cgobug/cgopkg"

func init() {
	cgopkg.Seed(1)
}

func OtherRandom() int {
	return cgopkg.Random()
}

And finally there is command pkginfo, which uses golang.org/x/tools/go/packages to gather identifier information about package other. It then prints the locations of the definitions of any identifiers that are used in other, but are defined in cgopkg. It is implemented in the file pkginfo.go:

package main

import (
	"fmt"
	"golang.org/x/tools/go/packages"
)

// Note: This command must be run from its source directory
// (so that the "../other" package path works below).

func main() {
	// Load type information for "other" package.
	cfg := packages.Config{Mode: packages.LoadSyntax}
	pkgs, err := packages.Load(&cfg, "../other")
	if err != nil {
		fmt.Println(err)
		return
	}

	// Print locations of identifier definitions
	// from package "cgopkg" that are used by package "other".
	for _, pkg := range pkgs {
		fmt.Printf("Package %q uses:\n", pkg.ID)
		if len(pkg.Errors) > 0 {
			fmt.Println(pkg.Errors)
			return
		}
		if len(pkg.TypeErrors) > 0 {
			fmt.Println(pkg.TypeErrors)
			return
		}

		for _, obj := range pkg.TypesInfo.Uses {
			pk := obj.Pkg()
			if pk == nil || pk.Name() != "cgopkg" {
				// Only print uses of identifiers
				// from package cgopkg.
				continue
			}

			name := obj.Name()
			pos := pkg.Fset.Position(obj.Pos())

			fmt.Printf("%s: %s\n", name, pos)
		}
	}
}

What did you see happen?

From the cgobug/pkginfo directory (where this simple command is designed to be run from), I ran

go run .

which produced the following output:

Package "cgobug/other" uses:
Random: /Users/beaubrueggemann/go/src/cgobug/cgopkg/cgopkg.go:9:1
Seed: /Users/beaubrueggemann/go/src/cgobug/cgopkg/cgopkg.go:13:1

What did you expect to see?

I expected the line numbers to match those in cgopkg.go, which would be lines 6 (for Random) and 10 (for Seed). Instead, the line numbers are three higher than they should be.

I've done many experiments, and the results are always the same: For any types.Object found in a types.Info.Uses map, if that Object is from a different package (than the one the types.Info struct is showing uses for), and if that Object is also defined in a cgo file (that imports "C"), then the line numbers of the Object's position will be incremented by three.

The columns are also incorrect, though I'm under the impression that that is to be expected with cgo files (though I can't remember where I read that). But I'm also under the impression that the line numbers are supposed to always be correct, even for cgo files (due to "line directives" in the cgo generated files).

Currently I'm working around this in my tool, by manually checking if the types.Object's file is a cgo file from another package, and then manually adding 3 to the line numbers when I use them. But this doesn't feel good. And is it reliable? Can the offset ever be other than 3? So far it's always been 3 for me (on both linux and macos, with both go1.23 and go1.24), but I'm still worried the 3 might change depending on what's in the cgo file (though all the various cgo files that I've tried so far have always ended up with lines offset by precisely 3).

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugReportIssues describing a possible bug in the Go implementation.ToolsThis label describes issues relating to any tools in the x/tools repository.

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions