depgraph

package
v0.1.0-develop.2026061... Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 10, 2026 License: MIT Imports: 3 Imported by: 0

Documentation

Overview

Package depgraph defines the core data model for the GoCell package-level dependency graph: Graph, Node, Stats, and the layer/cell/slice classifiers.

This package is stdlib-only — it does not depend on golang.org/x/tools or any other third-party library. Graph construction from live Go packages is handled by tools/depgraph, which imports this package and owns the packages.Load integration.

Types

Graph holds the typed dependency graph for a workspace's Go modules (one element in Modules when single-module; all workspace members under a go.work multi-module load). Wire format is stable JSON; see Graph.MarshalJSON and Node.MarshalJSON.

Node represents one Go package. Edges (Imports) use import-path strings (not pointers) so JSON serialization is cycle-safe.

Stats summarizes the graph with package and edge counts.

Layer classification

Classifier is the single classification entry point for a workspace's Go modules. Construct it with NewClassifier(modules []string) — pass every member module's import path (one element for a single-module workspace). It exposes .Layer, .Cell, .Slice, and .OwningModule methods. OwningModule selects the module that owns an import path by LONGEST matching prefix, so a nested module path (e.g. "github.com/ghbvf/gocell/mdm") correctly wins over the core module for packages like ".../mdm/cells/foo" — which then classifies as LayerCells within the mdm module rather than LayerUnknown under the core module. The canonical layer constants (LayerKernel, LayerCells, ...) are the JSON values of Node.Layer and the single source of truth for archtest layer rules.

Graph construction

FromNodes(modules []string, nodes) builds a Graph from a pre-constructed []*Node slice. The caller is responsible for building each Node (ID, Layer, CellID, SliceID, Imports); FromNodes handles sorting, byID indexing, Stats, and MarkTestOnly. Graph.Modules is the set of module import paths the graph spans.

MarkTestOnly marks each Node TestOnly=true when it is imported by at least one test consumer but no production consumer. Both the production and test importer sets are passed as plain string-keyed maps so this package never touches golang.org/x/tools types.

Transitive closure

Graph.TransitiveImports and Graph.TransitiveImportsWithPaths walk module-internal production edges. Used by archtest LAYER-05T/06T/09T rules.

Index

Constants

View Source
const (
	LayerKernel   = "kernel"
	LayerRuntime  = "runtime"
	LayerAdapters = "adapters"
	LayerCells    = "cells"
	LayerPkg      = "pkg"
	LayerCmd      = "cmd"
	// LayerCellModules is the composition-root layer for cellmodules/<cell> modules.
	// It wires platform Cell modules by importing cells/ + adapters/ + runtime/
	// and is the importable counterpart to cmd/ (which is an unimportable CLI
	// binary). Unlike cmd/, cellmodules/ packages are designed to be imported by
	// examples/ and external composition roots (#1085 CellModule public API).
	LayerCellModules = "cellmodules"
	LayerExamples    = "examples"
	LayerTools       = "tools"
	LayerTests       = "tests"
	LayerGenerated   = "generated"
	LayerRoot        = "root"
	LayerStdlib      = "stdlib"
	LayerThirdParty  = "thirdparty"
	// LayerUnknown marks a package whose import path is module-internal
	// (lives under the module root) but whose first segment is not in
	// internalLayerByDir. This is distinct from LayerThirdParty so that
	// governance code can detect a new top-level directory that needs
	// classification, instead of silently treating it as external.
	LayerUnknown = "unknown"
)

Layer constants name the buckets used by archtest layering rules and the `gocell graph` CLI. They are the JSON values for Node.Layer.

Variables

This section is empty.

Functions

func IsStdlib

func IsStdlib(importPath string) bool

IsStdlib reports whether importPath is a standard library package.

Heuristic: the first path segment contains no dot. Stdlib paths are "fmt", "net/http", "encoding/json"; module paths always contain a domain like "github.com" or "golang.org" with a dot.

func IsThirdParty

func IsThirdParty(module, importPath string) bool

IsThirdParty reports whether importPath belongs to neither the given module nor stdlib.

In a multi-module workspace, prefer Classifier.OwningModule(importPath) == "" (longest-prefix) over this function — IsThirdParty takes a single module string and would mis-classify a satellite module's packages as third-party when the satellite path is not passed as the module argument.

func MarkTestOnly

func MarkTestOnly(g *Graph, prodImporters, testImporters map[string]bool)

MarkTestOnly tags each node in g TestOnly=true when it is imported by at least one test consumer (testImporters) and no production consumer (prodImporters). Leaf / orphaned packages (absent from both sets) remain TestOnly=false — they may be entry points or unused production code.

Both maps are keyed by import path (the importee). They are produced by tools/depgraph.collectImporters, which reads golang.org/x/tools/go/packages data — a type this package never touches. Receiving plain map[string]bool keeps kernel/depgraph dependency-free from x/tools.

Types

type Classifier

type Classifier struct {
	// contains filtered or unexported fields
}

Classifier maps Go import paths to layers / cell IDs / slice IDs relative to a SET of workspace module paths. It is the single classification entry point; there is no module-singular free function, because a caller that passed the core module path for a nested module's package would mis-bucket it as LayerUnknown.

OwningModule selects the module that owns an import path by LONGEST matching prefix, so a nested module path (e.g. "github.com/ghbvf/gocell/mdm") wins over the core module ("github.com/ghbvf/gocell") for a package like ".../mdm/cells/foo" — which then classifies as LayerCells within the mdm module rather than LayerUnknown under the core module.

A single-module caller constructs NewClassifier([]string{module}); behavior is byte-identical to the former module-singular classification. The multi-module workspace passes every member module's import path.

func NewClassifier

func NewClassifier(modules []string) Classifier

NewClassifier builds a Classifier over the given module import paths. The input is copied and sorted longest-first (ties broken lexically for determinism); the caller's slice is neither retained nor mutated. Empty entries are dropped.

func (Classifier) Cell

func (c Classifier) Cell(importPath string) string

Cell returns the cell ID for a package under <owningModule>/cells/<id>/..., or "" if the package is not under any member module's cells/. The Go-reserved "internal" segment (e.g. cells/internal/testoutbox — shared cell-test helpers) is not a cell ID; Cell returns "" for paths under cells/internal/.

func (Classifier) Layer

func (c Classifier) Layer(importPath string) string

Layer classifies importPath relative to its owning module. Internal-module packages map to one of LayerKernel..LayerGenerated based on the first path segment under the owning module; the bare owning-module path maps to LayerRoot; an internal first segment not in internalLayerByDir maps to LayerUnknown — distinct from LayerThirdParty so consumers can spot repo-structure evolution that has not been classified.

Packages owned by no member module classify as LayerStdlib (no dot in first segment) or LayerThirdParty.

Satellite example modules (#1556): a package like "github.com/ghbvf/gocell/examples/iotdevice/cells/devicecell" classifies as LayerExamples (NOT LayerCells), because the base-relative first segment (examples/) is what places it in the examples layer of the core repo. This means cell-layering governance rules that test Layer()==LayerCells must NOT rely on that comparison alone to identify all cell packages — example cells return LayerExamples. Use Cell() to obtain the cell ID regardless of layer; downstream rules needing to distinguish example cells from platform cells must check Layer()==LayerExamples (or equivalently Layer()!=LayerCells) in addition to Cell()!="".

func (Classifier) OwningModule

func (c Classifier) OwningModule(importPath string) string

OwningModule returns the module path that owns importPath (the longest member equal to importPath or a "<module>/" prefix of it), or "" when no member owns it (stdlib / third-party / unrelated).

func (Classifier) Slice

func (c Classifier) Slice(importPath string) string

Slice returns the slice ID for a package under <owningModule>/cells/<id>/slices/<sliceId>/..., or "" if not under a slice. Slices may have nested subdirectories; only the immediate slice ID is returned.

type Graph

type Graph struct {
	Modules  []string `json:"modules"`
	Packages []*Node  `json:"packages"`
	Stats    Stats    `json:"stats"`
	// contains filtered or unexported fields
}

Graph is the typed dependency graph for a workspace's Go modules. Wire-format stable.

Modules is the set of module import paths the graph spans (one element for a single-module load; every workspace member under a go.work multi-module load). It is the membership set for Graph.inModule closure walks and the classification basis the builder used. A single-module graph has Modules == []string{module}.

Use NewClassifier(g.Modules) to classify packages by layer/cell/slice.

func FromNodes

func FromNodes(modules []string, nodes []*Node) *Graph

FromNodes builds a Graph from a pre-constructed []*Node slice. modules is the set of module import paths the graph spans (one element for a single-module load, e.g. []string{"github.com/ghbvf/gocell"}; every workspace member for a multi-module load). It becomes Graph.Modules — the membership set for closure walks (Graph.inModule). FromNodes handles:

  • deduplication by Node.ID (first occurrence wins)
  • byID index construction for O(1) ByID lookups and closure walks
  • deterministic sort of Packages by ID
  • Stats computation (package count + edge count)

Callers are responsible for building each Node before passing it in; the Layer, CellID, SliceID, and Imports fields are taken as-is. MarkTestOnly must be called separately when test-importer information is available (see tools/depgraph.FromPackages).

func (*Graph) ByID

func (g *Graph) ByID(id string) *Node

ByID returns the node for an import path, or nil if not in the graph.

func (*Graph) FilterByLayer

func (g *Graph) FilterByLayer(allowedLayers map[string]bool) *Graph

FilterByLayer returns a new Graph containing only the packages whose Layer is in allowedLayers. Stats are recomputed from the retained packages. The original Graph is not modified. Returns an empty Graph if no packages match or if g is nil.

func (*Graph) MarshalJSON

func (g *Graph) MarshalJSON() ([]byte, error)

MarshalJSON ensures deterministic output: Packages sorted by ID. Each Node's MarshalJSON method handles intra-node sorting and Imports non-nil enforcement, so adding a wire field requires editing only Node.

Two graphs with identical content produce byte-identical JSON.

func (*Graph) TransitiveImports

func (g *Graph) TransitiveImports(id string) map[string]bool

TransitiveImports returns the set of all packages reachable from id through production edges, restricted to packages inside the same module. The set excludes:

  • id itself
  • stdlib and other modules (closure stops at module boundary)
  • test-only packages (Node.TestOnly == true)
  • ghost nodes (paths referenced but not loaded into the graph)

These exclusions are tuned for archtest layer rules: cross-cell laundering paths inside the module matter; stdlib / vendor reachability does not.

Returns an empty (non-nil) map when id is not in the graph or has no reachable internal imports. Each call returns a fresh map; the caller owns it and may mutate freely. Implemented on top of TransitiveImportsWithPaths; use that variant directly when violation messages need the laundering chain (src → util → dep).

func (*Graph) TransitiveImportsWithPaths

func (g *Graph) TransitiveImportsWithPaths(id string) map[string][]string

TransitiveImportsWithPaths returns the same set as TransitiveImports keyed as dep → discovery-order path. Each value is the chain of package IDs from id to dep, inclusive on both ends (path[0] == id, path[len-1] == dep). Cycles are broken at first encounter and the recorded path is the DFS discovery path.

Used by archtest's LAYER-05T/06T/09T violation messages to render "src → util → dep" so reviewers can locate the offending intermediary without grepping the codebase.

type Node

type Node struct {
	ID      string `json:"id"`
	Layer   string `json:"layer"`
	CellID  string `json:"cellId,omitempty"`
	SliceID string `json:"sliceId,omitempty"`
	// Imports is always a non-nil slice in JSON output (leaves emit [], never null).
	Imports  []string `json:"imports"`
	TestOnly bool     `json:"testOnly,omitempty"`
}

Node represents one Go package. Edges (Imports) use ID strings (not pointers) so JSON serialization is cycle-safe and round-trippable.

func (*Node) MarshalJSON

func (n *Node) MarshalJSON() ([]byte, error)

MarshalJSON serializes a Node with a deterministic, non-nil Imports slice. The receiver value is left untouched (Imports is copied before sorting) so the live graph remains in its original construction order.

Defined on *Node — not just Graph.MarshalJSON — so any external consumer that marshals a single node (e.g. a Track J HTTP handler returning one package's metadata) still gets the stable wire form. Avoids the maintenance hazard of a parallel serializedNode struct that has to be kept in sync.

type Stats

type Stats struct {
	Packages int `json:"packages"`
	Edges    int `json:"edges"`
}

Stats summarizes the graph. Wire-format stable; new counters with omitempty are non-breaking additions.

Edges counts every entry in every Node.Imports slice — that includes stdlib and third-party targets, NOT just module-internal edges. This is intentionally the "raw structural fan-out" number useful for graph rendering and CLI reporting, and is distinct from the closure walked by Graph.TransitiveImports (which stops at the module boundary).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL