Skip to content

refactor(convert): preload dependency dirs via go list#650

Merged
MeteorsLiu merged 7 commits into
xgo-dev:mainfrom
luoliwoshang:fix/dep-loader-go-list-cache
Feb 14, 2026
Merged

refactor(convert): preload dependency dirs via go list#650
MeteorsLiu merged 7 commits into
xgo-dev:mainfrom
luoliwoshang:fix/dep-loader-go-list-cache

Conversation

@luoliwoshang
Copy link
Copy Markdown
Contributor

@luoliwoshang luoliwoshang commented Feb 13, 2026

Summary

  • preload dependency package directory mapping in NewPkgDepLoader via:
    • go list -deps -e -f={{.ImportPath}}={{.Dir}} all
  • cache the mapping as pkgPath -> pkgDir and use it directly in Import
  • keep -mod configurable through convert.BuildMod when building the cache
  • make NewPkgDepLoader return (*PkgDepLoader, error) and propagate errors at call sites

Behavior change

  • Import now relies on the preloaded go-list cache for package directory lookup.
  • if a package path has no directory entry in cache, it returns config error directly.

Motivation

  • reduce per-import lookup overhead and centralize path resolution logic to one go-list scan.
  • ensure dependency import-path to local-dir mapping is explicit and deterministic.

Validation

  • go test ./cl/internal/convert -run TestImport -count=1
  • go test ./cl/internal/convert -run TestNewPackageLinkWithoutLibCommand -count=1

Related

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @luoliwoshang, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request refactors the package dependency loading mechanism to improve performance and determinism. Instead of performing individual lookups for each package import, it now preloads all dependency package directories into a cache using a single go list command. This change centralizes path resolution, reduces overhead during import operations, and provides clearer error handling when a package directory cannot be found.

Highlights

  • Dependency Directory Preloading: Preloaded dependency package directory mappings in NewPkgDepLoader using go list -deps -e -f={{.ImportPath}}={{.Dir}} all to centralize path resolution.
  • Cached Package Directory Lookup: The pkgPath -> pkgDir mapping is now cached and directly used by the Import method, reducing per-import lookup overhead.
  • Configurable Go Module Behavior: The -mod flag for go list is now configurable via a new BuildMod variable, allowing control over module behavior during cache building.
  • Error Propagation: NewPkgDepLoader now returns an error, which is propagated to call sites, improving error handling for dependency loading.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • cl/internal/convert/deps.go
    • Added bytes and os/exec imports.
    • Introduced pkgs map to PkgDepLoader for caching package path to directory mappings.
    • Added BuildMod global variable to control go list -mod behavior.
    • Modified NewPkgDepLoader to return (*PkgDepLoader, error) and call loadPkgDirs.
    • Updated Import method to use the preloaded pkgs cache for directory lookups.
    • Implemented loadPkgDirs method to execute go list and populate the package directory cache.
    • Added runGoCommand helper function for executing Go commands and handling output.
  • cl/internal/convert/package.go
    • Updated the call to NewPkgDepLoader to handle the new error return value.
  • cl/internal/convert/package_test.go
    • Modified the test case TestImport to correctly handle the error returned by NewPkgDepLoader.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the dependency loading mechanism to improve performance by preloading package directories with a single go list command. While the changes are well-structured, a significant concern is the introduction of a global configuration variable BuildMod. This variable is not thread-safe and can lead to race conditions in concurrent environments, requiring it to be moved to instance-specific configuration for isolation and reliability. Additionally, there are suggestions to improve the robustness of the new go list output parsing and to address some minor redundancy.

Comment thread cl/internal/convert/deps.go Outdated
return &PkgDepLoader{
// BuildMod is passed to `go list -mod=...` when preloading package directories.
// Keep it empty to use the go command default behavior.
var BuildMod string
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

security-medium medium

The BuildMod variable is defined as a global string, which is not thread-safe. In a concurrent environment (e.g., if this library is used in a web service or a multi-threaded application), multiple goroutines calling NewPkgDepLoader will share and potentially overwrite this global state. This can lead to race conditions where one caller's configuration is used by another, potentially causing incorrect package loading or security issues if the -mod flag is used to enforce security policies (like readonly). Consider moving this configuration into the PkgDepLoader struct or passing it as a parameter to the constructor.

Comment on lines +146 to +151
for _, line := range strings.Split(string(data), "\n") {
pos := strings.Index(line, "=")
if pos != -1 {
pm.pkgs[line[:pos]] = line[pos+1:]
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The current parsing logic using strings.Index can be fragile. For example, if a line from go list is "=foo", it will result in an empty key in the map, which is likely unintended.

Using strings.SplitN is a more robust and idiomatic way to parse key-value pairs. This ensures you correctly handle various edge cases.

Suggested change
for _, line := range strings.Split(string(data), "\n") {
pos := strings.Index(line, "=")
if pos != -1 {
pm.pkgs[line[:pos]] = line[pos+1:]
}
}
for _, line := range strings.Split(string(data), "\n") {
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 && parts[0] != "" {
pm.pkgs[parts[0]] = parts[1]
}
}

Comment thread cl/internal/convert/deps.go Outdated
cmd.Dir = root
data, err := cmd.CombinedOutput()
if err != nil {
msg := strings.TrimSpace(string(bytes.TrimSpace(data)))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

There is a redundant call to strings.TrimSpace here. The data byte slice is already trimmed with bytes.TrimSpace. Calling strings.TrimSpace on the resulting string is unnecessary. You can remove the outer strings.TrimSpace for slightly better performance and cleaner code.

Suggested change
msg := strings.TrimSpace(string(bytes.TrimSpace(data)))
msg := string(bytes.TrimSpace(data))

Comment thread cl/internal/convert/deps.go Outdated
cmd.Dir = root
data, err := cmd.CombinedOutput()
if err != nil {
msg := strings.TrimSpace(string(bytes.TrimSpace(data)))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

bytes.TrimSpace(data) followed by strings.TrimSpace() is redundant - both trim the same whitespace characters. Simplify to:

Suggested change
msg := strings.TrimSpace(string(bytes.TrimSpace(data)))
msg := string(bytes.TrimSpace(data))

Comment thread cl/internal/convert/deps.go Outdated
func runGoCommand(root string, args ...string) ([]byte, error) {
cmd := exec.Command("go", args...)
cmd.Dir = root
data, err := cmd.CombinedOutput()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

CombinedOutput() merges stdout and stderr. If go list -e emits any warnings/diagnostics to stderr (which it can), they will be mixed with the formatted output and could corrupt parsing (lines without = are silently skipped, but lines with = in a warning message could create bogus entries).

Consider using separate capture for stdout only:

var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
// ... use stdout.Bytes() for parsing

Comment thread cl/internal/convert/deps.go Outdated
Comment on lines +136 to +140
args := []string{"list", "-deps", "-e", "-f={{.ImportPath}}={{.Dir}}"}
if BuildMod != "" {
args = append(args, "-mod", BuildMod)
}
args = append(args, "all")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Running go list -deps ... all loads the entire module dependency graph (potentially thousands of packages for large modules), but Import() is only called for a small, bounded set of dependencies from llcppg.cfg.

Consider passing only the known dependency paths as arguments instead:

args := []string{"list", "-e", "-f={{.ImportPath}}={{.Dir}}", "pkg1", "pkg2", "pkg3"}

This would produce the same mapping but with significantly less overhead.

Comment thread cl/internal/convert/deps.go Outdated
Comment on lines +28 to +31

func NewPkgDepLoader(mod *xgomod.Module, pkg *gogen.Package) *PkgDepLoader {
return &PkgDepLoader{
// BuildMod is passed to `go list -mod=...` when preloading package directories.
// Keep it empty to use the go command default behavior.
var BuildMod string
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This exported package-level mutable variable has a couple of concerns:

  1. No validation: If set to an unexpected value, it passes directly to exec.Command. Consider validating against an allowlist ("", "mod", "readonly", "vendor").

  2. Not concurrency-safe: Any concurrent callers of NewPkgDepLoader would race on this shared state. Consider passing this as a parameter to NewPkgDepLoader instead.

@fennoai
Copy link
Copy Markdown
Contributor

fennoai Bot commented Feb 13, 2026

Overall: Good refactor that centralizes dependency resolution into a single go list invocation. The approach is sound and error handling is properly propagated.

Key suggestions:

  • Consider narrowing the go list scope to only required dependencies instead of all for performance
  • Use separate stdout/stderr capture to avoid potential parsing issues
  • The BuildMod global could be passed as a parameter for better encapsulation

Test coverage looks good with the updated TestImport handling the new error return.

@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 13, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.16%. Comparing base (beba8c5) to head (3419f47).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #650      +/-   ##
==========================================
- Coverage   85.30%   85.16%   -0.14%     
==========================================
  Files          28       28              
  Lines        2415     2447      +32     
==========================================
+ Hits         2060     2084      +24     
- Misses        311      315       +4     
- Partials       44       48       +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@luoliwoshang luoliwoshang changed the title refactor(convert): preload dependency dirs via go list cache refactor(convert): preload dependency dirs via go list Feb 14, 2026
Comment thread cl/internal/convert/deps.go Outdated
@MeteorsLiu MeteorsLiu merged commit df16664 into xgo-dev:main Feb 14, 2026
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

proposal(convert): remove xgomod dependency and resolve deps via go list

2 participants