From ed9e8cee547d1b8e5541835b28c71fb30af8ff29 Mon Sep 17 00:00:00 2001 From: jupblb Date: Wed, 6 May 2026 17:08:23 +0200 Subject: [PATCH 1/5] Enable indexing of repositories with vendored deps and stdlib Changes required to successfully index large Go source trees that mix in-tree dependencies (vendored), build-constrained packages, and generated files outside the module root. The motivating case is github.com/golang/go itself, but every change is generic. - Bump golang.org/x/tools v0.43.0 -> v0.44.0. v0.43.0 calls log.Fatalf inside packages.Load when the loader is asked to import a vendored stdlib package that has no type information, which made indexing of GOROOT/src impossible. v0.44.0 contains the upstream fix that turns this into a recoverable error. - internal/index/scip.go: skip packages with no syntax files ("unsafe" pseudo-package, build-constrained packages such as internal/runtime/wasitest with zero matching .go files for the host platform). Also tolerate packages whose syntax files were all skipped (see visitors change below) via a new firstSyntaxWithDocument helper. - internal/loader/loader.go: promote in-tree dependencies to project packages. `go list ./...` deliberately excludes vendor/ directories, so vendored packages would otherwise be loaded only as dependencies and never indexed even though their source lives in the tree. Detect in-tree-ness via Syntax (with CompiledGoFiles / GoFiles fallbacks) because some loaders, including the stdlib's vendored packages loaded via export data, leave GoFiles empty. - internal/visitors/visitors.go: drop syntax files whose path is outside the module root. The Go test driver writes _testmain.go shims into $GOCACHE for every .test package; those paths resolve to ../../../Library/Caches/... and shouldn't be in the index. - internal/visitors/visitor_file.go: when resolving an ImportSpec to its loaded package, try the source-level import path first and only fall back to pkgName.Imported().Path(). pkg.Imports is keyed by the source-level string; a vendor-rewritten resolved path (e.g. vendor/golang.org/x/crypto/chacha20) misses every entry and produced "Could not find node" warnings that dropped all import references to vendored packages. --- go.mod | 6 +-- go.sum | 6 +++ internal/index/scip.go | 44 +++++++++++++++++++- internal/loader/loader.go | 69 +++++++++++++++++++++++++++++++ internal/visitors/visitor_file.go | 29 ++++++++++++- internal/visitors/visitors.go | 10 ++++- 6 files changed, 157 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 1680c04..8bbf6d1 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,8 @@ go 1.25.0 require ( github.com/alecthomas/kong v1.14.0 github.com/scip-code/scip/bindings/go/scip v0.7.1 - golang.org/x/mod v0.34.0 - golang.org/x/tools v0.43.0 + golang.org/x/mod v0.35.0 + golang.org/x/tools v0.44.0 golang.org/x/tools/go/vcs v0.1.0-deprecated google.golang.org/protobuf v1.36.11 ) @@ -21,6 +21,6 @@ require ( github.com/sourcegraph/beaut v0.0.0-20240611013027-627e4c25335a // indirect github.com/stretchr/testify v1.11.1 // indirect golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.42.0 // indirect + golang.org/x/sys v0.43.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 27969da..36195f0 100644 --- a/go.sum +++ b/go.sum @@ -34,13 +34,19 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/tools/go/vcs v0.1.0-deprecated h1:cOIJqWBl99H1dH5LWizPa+0ImeeJq3t3cJjaeOWUAL4= golang.org/x/tools/go/vcs v0.1.0-deprecated/go.mod h1:zUrvATBAvEI9535oC0yWYsLsHIV4Z7g63sNPVMtuBy8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/internal/index/scip.go b/internal/index/scip.go index 5c7b27c..6940e90 100644 --- a/internal/index/scip.go +++ b/internal/index/scip.go @@ -22,6 +22,7 @@ import ( "github.com/scip-code/scip-go/internal/symbols" "github.com/scip-code/scip-go/internal/visitors" "github.com/scip-code/scip/bindings/go/scip" + "golang.org/x/tools/go/packages" "google.golang.org/protobuf/proto" ) @@ -204,6 +205,18 @@ func indexVisitPackages( for _, pkgID := range lookupIDs { pkg := projectPackages[pkgID] slog.Debug("Visiting package", "path", pkg.PkgPath) + + // Some packages have no syntax files for the current build: + // - "unsafe" is a compiler-builtin pseudo-package with no source. + // - Build-constrained packages (e.g. internal/runtime/wasitest) + // may have zero matching .go files for the host platform. + // There is nothing to visit in either case. + if len(pkg.Syntax) == 0 { + slog.Debug("Skipping package with no syntax files", "path", pkg.PkgPath) + atomic.AddUint64(&count, 1) + continue + } + visitors.VisitPackageSyntax(opts.ModuleRoot, pkg, pathToDocuments, globalSymbols) pkgSymbol, _ := globalSymbols.GetPkgSymbol(pkg) @@ -218,12 +231,23 @@ func indexVisitPackages( Text: "package " + pkg.Name, }, } - firstFile := pkg.Syntax[0] - firstDoc := pathToDocuments[pkg.Fset.File(firstFile.Package).Name()] + // Find the first file that produced a document. Some files + // may have been skipped by VisitPackageSyntax (e.g. generated + // files outside the module root), so pkg.Syntax[0] is not + // guaranteed to be in pathToDocuments. + firstFile, firstDoc := firstSyntaxWithDocument(pkg, pathToDocuments) + if firstDoc == nil { + slog.Debug("Skipping package with no in-tree syntax files", "path", pkg.PkgPath) + atomic.AddUint64(&count, 1) + continue + } firstDoc.SetSymbolInformation(firstFile.Name.NamePos, symInfo) for _, f := range pkg.Syntax { doc := pathToDocuments[pkg.Fset.File(f.Package).Name()] + if doc == nil { + continue + } position := pkg.Fset.Position(f.Name.NamePos) doc.PackageOccurrence = &scip.Occurrence{ @@ -241,3 +265,19 @@ func indexVisitPackages( return pathToDocuments, globalSymbols } + +// firstSyntaxWithDocument returns the first parsed file in pkg.Syntax that +// has an associated *document.Document in pathToDocuments. Files outside +// the module root (e.g. generated _testmain.go in $GOCACHE) are skipped +// during visiting and won't appear in pathToDocuments. +func firstSyntaxWithDocument( + pkg *packages.Package, + pathToDocuments map[string]*document.Document, +) (*ast.File, *document.Document) { + for _, f := range pkg.Syntax { + if doc := pathToDocuments[pkg.Fset.File(f.Package).Name()]; doc != nil { + return f, doc + } + } + return nil, nil +} diff --git a/internal/loader/loader.go b/internal/loader/loader.go index 087f2e1..de37938 100644 --- a/internal/loader/loader.go +++ b/internal/loader/loader.go @@ -122,11 +122,80 @@ func LoadPackages( projectPackages[newtypes.GetID(pkg)] = allPackages[newtypes.GetID(pkg)] } + // Promote any in-tree dependency to a project package. + // + // `go list ./...` and friends deliberately exclude vendor/ + // directories, so vendored dependencies arrive only via the + // transitive import graph. Their source lives in the repository + // being indexed, however, so they should be indexed (with + // definitions, hover docs, etc.) just like first-party code. + // The most prominent case is the standard library, which vendors + // golang.org/x/{crypto,net,...} under src/vendor/, but the same + // applies to any module that vendors its dependencies. + for id, pkg := range allPackages { + if _, ok := projectPackages[id]; ok { + continue + } + if isInTree(pkg, moduleRoot) { + projectPackages[id] = pkg + } + } + return nil }) return projectPackages, allPackages, err } +// isInTree reports whether pkg's source files live under root. Packages +// loaded transitively from the module cache, GOROOT, or anywhere outside +// the indexed tree return false. +// +// Use Syntax (or CompiledGoFiles, falling back to GoFiles) because some +// loaders—most notably the stdlib's vendored packages loaded via export +// data—report empty GoFiles even though their source is on disk and +// available via the parsed Syntax file positions. +func isInTree(pkg *packages.Package, root string) bool { + if pkg == nil { + return false + } + + files := packageSourceFiles(pkg) + if len(files) == 0 { + return false + } + + for _, f := range files { + rel, err := filepath.Rel(root, f) + if err != nil || !filepath.IsLocal(rel) { + return false + } + } + return true +} + +// packageSourceFiles returns the paths of all parseable source files +// associated with pkg, preferring Syntax (parsed AST) > CompiledGoFiles +// > GoFiles. +func packageSourceFiles(pkg *packages.Package) []string { + if pkg == nil { + return nil + } + + if len(pkg.Syntax) > 0 && pkg.Fset != nil { + files := make([]string, 0, len(pkg.Syntax)) + for _, f := range pkg.Syntax { + if tf := pkg.Fset.File(f.Package); tf != nil { + files = append(files, tf.Name()) + } + } + return files + } + if len(pkg.CompiledGoFiles) > 0 { + return pkg.CompiledGoFiles + } + return pkg.GoFiles +} + func isStandardLib(pkg *packages.Package) bool { return pkg.Module == nil || pkg.Module.Path == "std" } diff --git a/internal/visitors/visitor_file.go b/internal/visitors/visitor_file.go index 08c440a..557883b 100644 --- a/internal/visitors/visitor_file.go +++ b/internal/visitors/visitor_file.go @@ -6,6 +6,7 @@ import ( "go/token" "go/types" "log/slog" + "strconv" "github.com/scip-code/scip-go/internal/document" "github.com/scip-code/scip-go/internal/lookup" @@ -109,7 +110,7 @@ func (v *fileVisitor) Visit(n ast.Node) ast.Visitor { return nil } - importedPackage := v.pkg.Imports[pkgName.Imported().Path()] + importedPackage := v.lookupImportedPackage(node, pkgName) if importedPackage == nil { slog.Warn("Could not find node", "node.Path", node.Path) return nil @@ -270,6 +271,32 @@ func (v *fileVisitor) Visit(n ast.Node) ast.Visitor { return v } +// lookupImportedPackage resolves an *ast.ImportSpec to its loaded +// *packages.Package. +// +// pkg.Imports is keyed by the import path string as it appears in the +// source code, while pkgName.Imported().Path() returns the resolved path. +// These differ when an import is satisfied through a vendor directory +// (most commonly the standard library's own vendored copies of +// golang.org/x/...), so we try both keys. +func (v *fileVisitor) lookupImportedPackage( + node *ast.ImportSpec, + pkgName *types.PkgName, +) *packages.Package { + if sourcePath, err := strconv.Unquote(node.Path.Value); err == nil { + if imp := v.pkg.Imports[sourcePath]; imp != nil { + return imp + } + } + + resolvedPath := pkgName.Imported().Path() + if imp := v.pkg.Imports[resolvedPath]; imp != nil { + return imp + } + + return nil +} + func (v *fileVisitor) emitImportReference( globalSymbols *lookup.Global, position token.Position, diff --git a/internal/visitors/visitors.go b/internal/visitors/visitors.go index c30fe3f..0065bff 100644 --- a/internal/visitors/visitors.go +++ b/internal/visitors/visitors.go @@ -26,7 +26,15 @@ func VisitPackageSyntax( for _, f := range pkg.Syntax { abs := pkg.Fset.File(f.Package).Name() - relative, _ := filepath.Rel(moduleRoot, abs) + relative, err := filepath.Rel(moduleRoot, abs) + if err != nil || !filepath.IsLocal(relative) { + // The file is outside the module root. This commonly happens + // for generated files placed by the toolchain in $GOCACHE + // (e.g. _testmain.go for ".test" packages). Such files + // are machine-specific, recreated on every build, and not + // part of the source tree being indexed, so skip them. + continue + } doc := visitSyntax(pkg, pkgSymbols, f, relative) From 11c24ea53c4fd01f1bdb8383da4299a2aad8c96f Mon Sep 17 00:00:00 2001 From: jupblb Date: Thu, 7 May 2026 12:21:36 +0200 Subject: [PATCH 2/5] ci: index golang/go in the matrix Adds the Go source tree as a matrix target. Two pieces of plumbing allow it to share the existing job: - A new optional matrix.module_root field threads through setup-go's cache-dependency-path, scip-go's --module-root, and scip stats's --project-root, defaulting to '.' when unset so every other entry behaves exactly as before. - A 3-line GOROOT-export step gated on matrix.name == 'golang'. Without it, scip-go silently indexes the runner's system stdlib while reporting paths under target/src/, producing an index that is missing target's vendor/golang.org/x/... files and includes spurious files from the system Go. --- .github/workflows/test.yaml | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 72d0102..2bb71f1 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -67,6 +67,11 @@ jobs: ref: v0.50.0 expect_file: "cmd/gazelle/main.go" gopackagesdriver: true + - name: golang + repo: golang/go + ref: go1.26.2 + expect_file: "fmt/print.go" + module_root: src runs-on: ubuntu-latest name: ${{ matrix.name }} @@ -99,7 +104,7 @@ jobs: - uses: actions/setup-go@v6 with: go-version: stable - cache-dependency-path: target/go.sum + cache-dependency-path: target/${{ matrix.module_root || '.' }}/go.sum - name: Set up GOPACKAGESDRIVER if: matrix.gopackagesdriver @@ -114,11 +119,16 @@ jobs: chmod +x "$driver" echo "GOPACKAGESDRIVER=$driver" >> "$GITHUB_ENV" + - name: Point GOROOT at target stdlib + if: matrix.name == 'golang' + run: echo "GOROOT=$GITHUB_WORKSPACE/target" >> "$GITHUB_ENV" + - name: Run scip-go working-directory: target run: | /usr/bin/time -v -o "$GITHUB_WORKSPACE/metrics.txt" \ - scip-go -o "$GITHUB_WORKSPACE/index.scip" -VV + scip-go --module-root="${{ matrix.module_root || '.' }}" \ + -o "$GITHUB_WORKSPACE/index.scip" -VV - name: Validate index working-directory: target @@ -127,7 +137,8 @@ jobs: echo "Index size: $size bytes" [ "$size" -ge 1024 ] || { echo "FAIL: index too small"; exit 1; } - scip stats --from "$GITHUB_WORKSPACE/index.scip" --project-root . \ + scip stats --from "$GITHUB_WORKSPACE/index.scip" \ + --project-root "${{ matrix.module_root || '.' }}" \ | jq -e '.documents > 0 and .occurrences > 0 and .definitions > 0' scip print --json "$GITHUB_WORKSPACE/index.scip" \ From 6be2dfe8a2e12a31fb27e65bca139644585792c4 Mon Sep 17 00:00:00 2001 From: jupblb Date: Thu, 7 May 2026 12:42:38 +0200 Subject: [PATCH 3/5] Remove unnecessary comments --- internal/index/scip.go | 16 +++------------- internal/loader/loader.go | 13 +++++-------- internal/visitors/visitors.go | 5 ----- 3 files changed, 8 insertions(+), 26 deletions(-) diff --git a/internal/index/scip.go b/internal/index/scip.go index 6940e90..fd33ffb 100644 --- a/internal/index/scip.go +++ b/internal/index/scip.go @@ -206,11 +206,6 @@ func indexVisitPackages( pkg := projectPackages[pkgID] slog.Debug("Visiting package", "path", pkg.PkgPath) - // Some packages have no syntax files for the current build: - // - "unsafe" is a compiler-builtin pseudo-package with no source. - // - Build-constrained packages (e.g. internal/runtime/wasitest) - // may have zero matching .go files for the host platform. - // There is nothing to visit in either case. if len(pkg.Syntax) == 0 { slog.Debug("Skipping package with no syntax files", "path", pkg.PkgPath) atomic.AddUint64(&count, 1) @@ -231,10 +226,6 @@ func indexVisitPackages( Text: "package " + pkg.Name, }, } - // Find the first file that produced a document. Some files - // may have been skipped by VisitPackageSyntax (e.g. generated - // files outside the module root), so pkg.Syntax[0] is not - // guaranteed to be in pathToDocuments. firstFile, firstDoc := firstSyntaxWithDocument(pkg, pathToDocuments) if firstDoc == nil { slog.Debug("Skipping package with no in-tree syntax files", "path", pkg.PkgPath) @@ -266,10 +257,9 @@ func indexVisitPackages( return pathToDocuments, globalSymbols } -// firstSyntaxWithDocument returns the first parsed file in pkg.Syntax that -// has an associated *document.Document in pathToDocuments. Files outside -// the module root (e.g. generated _testmain.go in $GOCACHE) are skipped -// during visiting and won't appear in pathToDocuments. +// firstSyntaxWithDocument returns the first parsed file in pkg.Syntax that has +// an associated *document.Document in pathToDocuments. Files outside the module +// root are skipped during visiting and won't appear in pathToDocuments. func firstSyntaxWithDocument( pkg *packages.Package, pathToDocuments map[string]*document.Document, diff --git a/internal/loader/loader.go b/internal/loader/loader.go index de37938..f7a0cc7 100644 --- a/internal/loader/loader.go +++ b/internal/loader/loader.go @@ -124,14 +124,11 @@ func LoadPackages( // Promote any in-tree dependency to a project package. // - // `go list ./...` and friends deliberately exclude vendor/ - // directories, so vendored dependencies arrive only via the - // transitive import graph. Their source lives in the repository - // being indexed, however, so they should be indexed (with - // definitions, hover docs, etc.) just like first-party code. - // The most prominent case is the standard library, which vendors - // golang.org/x/{crypto,net,...} under src/vendor/, but the same - // applies to any module that vendors its dependencies. + // `go list ./...` deliberately exclude vendor/ directories, so vendored + // dependencies arrive only via the transitive import graph. Their source + // lives in the repository being indexed, however, so they should be indexed + // just like first-party code. The most prominent case is the standard + // library, which vendors golang.org/x/{crypto,net,...} under src/vendor/. for id, pkg := range allPackages { if _, ok := projectPackages[id]; ok { continue diff --git a/internal/visitors/visitors.go b/internal/visitors/visitors.go index 0065bff..8082ab0 100644 --- a/internal/visitors/visitors.go +++ b/internal/visitors/visitors.go @@ -28,11 +28,6 @@ func VisitPackageSyntax( abs := pkg.Fset.File(f.Package).Name() relative, err := filepath.Rel(moduleRoot, abs) if err != nil || !filepath.IsLocal(relative) { - // The file is outside the module root. This commonly happens - // for generated files placed by the toolchain in $GOCACHE - // (e.g. _testmain.go for ".test" packages). Such files - // are machine-specific, recreated on every build, and not - // part of the source tree being indexed, so skip them. continue } From c0f58456da799a7f4f5721d3be081c8acda296b7 Mon Sep 17 00:00:00 2001 From: jupblb Date: Thu, 7 May 2026 13:04:51 +0200 Subject: [PATCH 4/5] Simplify vendored package and import path resolution - isInTree: check the first parsed syntax file only; all files in a Go package share a directory, so a single check is sufficient. - visitor_file: look up imports directly by the source-level import path (pkg.Imports is keyed that way for vendored imports). --- internal/loader/loader.go | 53 ++++++------------------------- internal/visitors/visitor_file.go | 36 ++++++--------------- 2 files changed, 19 insertions(+), 70 deletions(-) diff --git a/internal/loader/loader.go b/internal/loader/loader.go index f7a0cc7..d3fdb10 100644 --- a/internal/loader/loader.go +++ b/internal/loader/loader.go @@ -143,54 +143,21 @@ func LoadPackages( return projectPackages, allPackages, err } -// isInTree reports whether pkg's source files live under root. Packages -// loaded transitively from the module cache, GOROOT, or anywhere outside -// the indexed tree return false. -// -// Use Syntax (or CompiledGoFiles, falling back to GoFiles) because some -// loaders—most notably the stdlib's vendored packages loaded via export -// data—report empty GoFiles even though their source is on disk and -// available via the parsed Syntax file positions. +// isInTree reports whether pkg's source files live under root. All files +// of a Go package share a directory, so checking the first parsed file +// is sufficient. We deliberately use Syntax (rather than GoFiles) because +// some loaders—most notably the stdlib's vendored packages loaded via +// export data—report empty GoFiles even when source is available on disk. func isInTree(pkg *packages.Package, root string) bool { - if pkg == nil { + if pkg == nil || len(pkg.Syntax) == 0 || pkg.Fset == nil { return false } - - files := packageSourceFiles(pkg) - if len(files) == 0 { + f := pkg.Fset.File(pkg.Syntax[0].Package) + if f == nil { return false } - - for _, f := range files { - rel, err := filepath.Rel(root, f) - if err != nil || !filepath.IsLocal(rel) { - return false - } - } - return true -} - -// packageSourceFiles returns the paths of all parseable source files -// associated with pkg, preferring Syntax (parsed AST) > CompiledGoFiles -// > GoFiles. -func packageSourceFiles(pkg *packages.Package) []string { - if pkg == nil { - return nil - } - - if len(pkg.Syntax) > 0 && pkg.Fset != nil { - files := make([]string, 0, len(pkg.Syntax)) - for _, f := range pkg.Syntax { - if tf := pkg.Fset.File(f.Package); tf != nil { - files = append(files, tf.Name()) - } - } - return files - } - if len(pkg.CompiledGoFiles) > 0 { - return pkg.CompiledGoFiles - } - return pkg.GoFiles + rel, err := filepath.Rel(root, f.Name()) + return err == nil && filepath.IsLocal(rel) } func isStandardLib(pkg *packages.Package) bool { diff --git a/internal/visitors/visitor_file.go b/internal/visitors/visitor_file.go index 557883b..1cc206e 100644 --- a/internal/visitors/visitor_file.go +++ b/internal/visitors/visitor_file.go @@ -110,7 +110,15 @@ func (v *fileVisitor) Visit(n ast.Node) ast.Visitor { return nil } - importedPackage := v.lookupImportedPackage(node, pkgName) + // pkg.Imports is keyed by the import path string as it appears + // in source, not by the resolved path returned by the type checker + // (the two differ for vendored imports). + sourcePath, err := strconv.Unquote(node.Path.Value) + if err != nil { + slog.Warn("Could not find node", "node.Path", node.Path) + return nil + } + importedPackage := v.pkg.Imports[sourcePath] if importedPackage == nil { slog.Warn("Could not find node", "node.Path", node.Path) return nil @@ -271,32 +279,6 @@ func (v *fileVisitor) Visit(n ast.Node) ast.Visitor { return v } -// lookupImportedPackage resolves an *ast.ImportSpec to its loaded -// *packages.Package. -// -// pkg.Imports is keyed by the import path string as it appears in the -// source code, while pkgName.Imported().Path() returns the resolved path. -// These differ when an import is satisfied through a vendor directory -// (most commonly the standard library's own vendored copies of -// golang.org/x/...), so we try both keys. -func (v *fileVisitor) lookupImportedPackage( - node *ast.ImportSpec, - pkgName *types.PkgName, -) *packages.Package { - if sourcePath, err := strconv.Unquote(node.Path.Value); err == nil { - if imp := v.pkg.Imports[sourcePath]; imp != nil { - return imp - } - } - - resolvedPath := pkgName.Imported().Path() - if imp := v.pkg.Imports[resolvedPath]; imp != nil { - return imp - } - - return nil -} - func (v *fileVisitor) emitImportReference( globalSymbols *lookup.Global, position token.Position, From 67f4b8a30ac8b490556bc260a160a4e750ddd7f5 Mon Sep 17 00:00:00 2001 From: jupblb Date: Mon, 11 May 2026 11:01:36 +0200 Subject: [PATCH 5/5] --- internal/loader/loader.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/loader/loader.go b/internal/loader/loader.go index d3fdb10..912cf0e 100644 --- a/internal/loader/loader.go +++ b/internal/loader/loader.go @@ -146,8 +146,7 @@ func LoadPackages( // isInTree reports whether pkg's source files live under root. All files // of a Go package share a directory, so checking the first parsed file // is sufficient. We deliberately use Syntax (rather than GoFiles) because -// some loaders—most notably the stdlib's vendored packages loaded via -// export data—report empty GoFiles even when source is available on disk. +// some loaders report empty GoFiles even when source is available on disk. func isInTree(pkg *packages.Package, root string) bool { if pkg == nil || len(pkg.Syntax) == 0 || pkg.Fset == nil { return false