loov / goda

Go Dependency Analysis toolkit
MIT License
1.4k stars 45 forks source link

Allow to use the package as library #70

Open dcu opened 1 year ago

dcu commented 1 year ago

there are some cases where using as library would be preferable than using as cli

is there a reason to not allow it? thanks

egonelbre commented 1 year ago

Mostly, I don't want to put the effort into a stable API. I could provide an unstable one.

Is there a concrete problem you are finding difficult to solve? I'm mostly interested in which parts of the system you are interested in being exposed.

Sometimes using golang.org/x/tools/go/packages directly might be more easier.

dcu commented 1 year ago

I would use pkgset.Calc to find all dependencies of a package

egonelbre commented 1 year ago

If you just want all the packages, then this would be sufficient:

package main

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

func Dependencies(pkgs ...string) (map[string]*packages.Package, error) {
    roots, err := packages.Load(&packages.Config{
        Mode: packages.NeedName | packages.NeedImports,
        // Tests: true, // if you need tests
    }, pkgs...)
    if err != nil {
        return nil, fmt.Errorf("failed to load packages %v: %w", pkgs, err)
    }

    r := map[string]*packages.Package{}
    include(r, roots)
    return r, nil
}

func include(r map[string]*packages.Package, pkgs []*packages.Package) {
    for _, pkg := range pkgs {
        if _, added := r[pkg.ID]; added {
            continue
        }
        r[pkg.ID] = pkg
        include(r, pkg.Imports)
    }
}
HenrikPoulsen commented 3 weeks ago

Is there a concrete problem you are finding difficult to solve?

For me I wanted to experiment with using it in a CI setup, to figure out which packages need to be re-built and tested based on changes in a PR (this is for a monorepo) and stuff like that. I would want to avoid reverse engineering the whole thing, or forking it just to export it :sweat_smile: I could most likely accomplish this with parsing the goda output, but in general I think this sort of thing is nicer to do programmatically.

Mostly, I don't want to put the effort into a stable API. I could provide an unstable one.

I at least think that's reasonable. I would be fine under the assumption that when a new release is out, it likely means I need to change my code

egonelbre commented 3 weeks ago

If you can cache the build output, then Go won't test anything that hasn't changed.

Otherwise, the logic for such a things is approximately this:

// !!!
// WARNING: CODE UNTESTED
// !!!

package main

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

// TODO: you'll need extra conditions for changing testdata, environment flags, build flags, go.mod, go.work etc.

func NeedsRetest(pkgs, modifiedPackages []string) ([]string, error) {
    roots, err := packages.Load(&packages.Config{
        Mode: packages.NeedName | packages.NeedImports,
        Tests: true,
    }, pkgs...)
    if err != nil {
        return nil, fmt.Errorf("failed to load packages %v: %w", pkgs, err)
    }

    allPackages := map[string]*packages.Package{}
    include(allPackages, roots)

    retest := map[string]bool{}
    for _, modifiedPackage := range modifiedPackages {
        retest[modifiedPackage] = true
    }

    // check whether parent has any child that needs modification,
    // and keep around a cache to avoid double checking packages.
    check := func(parent *packages.Package) bool {
        if needs, ok := retest[parent.ID]; ok {
            return needs
        }

        for _, pkg := range parent.Imports {
            if check(pkg) {
                retest[parent.ID] = true
                return true
            }
        }

        return false
    }

    for _, pkg := range allPackages {
        check(pkg)
    }

    result := []string{}
    for pkg, ok := range retest {
        if ok {
            result = append(result, pkg)
        }
    }

    sort.Strings(result)
    return result, nil
}

func include(r map[string]*packages.Package, pkgs []*packages.Package) {
    for _, pkg := range pkgs {
        if _, added := r[pkg.ID]; added {
            continue
        }
        r[pkg.ID] = pkg
        include(r, pkg.Imports)
    }
}