Helcaraxan/gomod

View on GitHub
internal/depgraph/deps_pkg.go

Summary

Maintainability
A
1 hr
Test Coverage
package depgraph

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "strings"

    "go.uber.org/zap"

    "github.com/Helcaraxan/gomod/internal/graph"
    "github.com/Helcaraxan/gomod/internal/logger"
    "github.com/Helcaraxan/gomod/internal/modules"
    "github.com/Helcaraxan/gomod/internal/util"
)

func (g *DepGraph) buildImportGraph(dl *logger.Builder) error {
    log := dl.Domain(logger.PackageInfoDomain)
    log.Debug("Building initial dependency graph based on the import graph.")

    err := g.retrieveTransitiveImports(log, []string{fmt.Sprintf("%s/...", g.Main.Info.Path)})
    if err != nil {
        return err
    }

    pkgs := g.Graph.GetLevel(int(LevelPackages))
    for _, node := range pkgs.List() {
        pkg := node.(*Package)

        imports := pkg.Info.Imports
        if pkg.parent.Name() == g.Main.Name() {
            imports = append(imports, pkg.Info.TestImports...)
            imports = append(imports, pkg.Info.XTestImports...)
        }

        for _, imp := range imports {
            if isStandardLib(imp) {
                continue
            }

            targetNode, _ := pkgs.Get(packageHash(imp))
            if targetNode == nil {
                log.Error("Detected import of unknown package.", zap.String("package", imp))
                continue
            }

            log.Debug(
                "Adding package dependency.",
                zap.String("source", pkg.Name()),
                zap.String("source-module", pkg.Parent().Name()),
                zap.String("target", targetNode.Name()),
                zap.String("target-module", targetNode.Parent().Name()),
            )
            targetPkg := targetNode.(*Package)
            if err = g.Graph.AddEdge(pkg, targetPkg); err != nil {
                return err
            }
        }
    }

    if err = g.markNonTestDependencies(log); err != nil {
        return err
    }

    return nil
}

func (g *DepGraph) markNonTestDependencies(log *logger.Logger) error {
    log.Debug("Marking non-test dependencies.")

    var todo []graph.Node
    seen := map[string]bool{}

    for _, mainPkg := range g.Main.packages.List() {
        if strings.HasSuffix(mainPkg.(*Package).Info.Name, "_test") {
            log.Debug("Skipping main module package as it is a test-only package.", zap.String("package", mainPkg.Name()))
            continue
        }

        todo = append(todo, mainPkg)
        seen[mainPkg.Name()] = true
    }

    for len(todo) > 0 {
        next := todo[0]
        todo = todo[1:]

        log.Debug("Marking package as non-test dependency.", zap.String("package", next.Name()))
        next.(*Package).isNonTestDependency = true
        next.Parent().(*Module).isNonTestDependency = true

        for _, imp := range next.(*Package).Info.Imports {
            if isStandardLib(imp) {
                continue
            }

            dep, err := g.Graph.GetNode(packageHash(imp))
            if err != nil {
                return err
            }

            if !seen[dep.Name()] {
                todo = append(todo, dep)
                seen[dep.Name()] = true
            }
        }
    }
    return nil
}

func (g *DepGraph) retrieveTransitiveImports(log *logger.Logger, pkgs []string) error {
    const maxQueryLength = 950 // This is chosen conservatively to ensure we don't exceed maximum command lengths for 'go list' invocations.

    queued := map[string]bool{}
    for len(pkgs) > 0 {
        queryLength := 0

        cursor := 0
        for {
            if cursor == len(pkgs) || queryLength+len(pkgs[cursor]) > maxQueryLength {
                break
            }
            queryLength += len(pkgs[cursor]) + 1
            cursor++
        }
        query := pkgs[:cursor]
        pkgs = pkgs[cursor:]

        imports, err := g.retrievePackageInfo(log, query)
        if err != nil {
            return err
        }

        for _, pkg := range imports {
            if !queued[pkg] {
                queued[pkg] = true
                pkgs = append(pkgs, pkg)
            }
        }
    }
    return nil
}

func (g *DepGraph) retrievePackageInfo(log *logger.Logger, pkgs []string) (imports []string, err error) {
    stdout, _, err := util.RunCommand(log, g.Main.Info.Dir, "go", append([]string{"list", "-json", "-mod=mod"}, pkgs...)...)
    if err != nil {
        log.Error("Failed to list imports for packages.", zap.Strings("packages", pkgs), zap.Error(err))
        return nil, err
    }
    dec := json.NewDecoder(bytes.NewReader(stdout))

    for {
        pkgInfo := &modules.PackageInfo{}
        if err = dec.Decode(pkgInfo); err != nil {
            if err == io.EOF {
                break
            } else {
                log.Error("Failed to parse go list output.", zap.Error(err))
                return nil, err
            }
        }
        parentModule, ok := g.getModule(pkgInfo.Module.Path)
        if !ok {
            log.Error("Encountered package in unknown module.", zap.String("package", pkgInfo.ImportPath), zap.String("module", pkgInfo.Module.Path))
            continue
        }

        pkg := NewPackage(pkgInfo, parentModule)
        _ = g.Graph.AddNode(pkg)
        log.Debug("Added import information for package", zap.String("package", pkg.Name()), zap.String("module", parentModule.Name()))

        importCandidates := make([]string, len(pkgInfo.Imports))
        copy(importCandidates, pkgInfo.Imports)
        if parentModule.Name() == g.Main.Name() {
            importCandidates = append(importCandidates, pkgInfo.TestImports...)
            importCandidates = append(importCandidates, pkgInfo.XTestImports...)
        }

        for _, candidate := range importCandidates {
            if !isStandardLib(candidate) {
                imports = append(imports, candidate)
            }
        }
    }
    return imports, nil
}

func isStandardLib(pkg string) bool {
    return !strings.Contains(strings.Split(pkg, "/")[0], ".")
}