coupling

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jul 7, 2026 License: MIT Imports: 8 Imported by: 0

README

go-coupling

Go Reference

Website: richardwooding.github.io/go-coupling

Robert C. Martin's package-coupling metrics — afferent coupling (Ca), efferent coupling (Ce), instability (I = Ce/(Ca+Ce)) — plus circular dependency detection, across 9 language ecosystems.

No other open-source Go library computes these metrics with manifest-aware, multi-language first-party detection.

Ecosystem Detected by Node =
Go go.mod package import path
Rust Cargo.toml crate
Swift Package.swift SwiftPM target/module
JVM (Java/Kotlin/Scala) pom.xml / Gradle / sbt / Mill declared package
C# .sln / .csproj / SDK markers namespace
PHP composer.json namespace (\-separated)
Perl cpanfile / Makefile.PL / dist.ini / META package (::-separated)
Python pyproject.toml / setup.py / … dotted package dir
JS / TS package.json / tsconfig.json directory module
C / C++ CMake / autotools / meson directory module (#include graph)

How it works

go-coupling does no parsing. You give it, per file, the data you've already extracted — path, language, imports, declared package — and a project root. It detects the ecosystem from the root's build manifest, builds the first-party import graph, and computes the metrics.

Pair it with treesitter-symbols (or any extractor) to get the imports and package per file:

import (
	coupling "github.com/richardwooding/go-coupling"
	symbols  "github.com/richardwooding/treesitter-symbols"
)

var files []coupling.File
for _, path := range sourcePaths {
	src, _ := os.ReadFile(path)
	s, _ := symbols.Extract(lang, src)
	files = append(files, coupling.File{
		Path: path, Language: lang,
		Imports: s.Imports, RelativeImports: s.RelativeImports, Package: s.Package,
	})
}

g := coupling.Build(root, files)
for _, c := range g.Coupling() {
	fmt.Printf("%-30s Ca=%d Ce=%d I=%.2f\n", c.Package, c.Afferent, c.Efferent, c.Instability)
}
for _, cyc := range g.Cycles() {
	fmt.Printf("cycle: %v\n", cyc.Nodes)
}

Coupling() is ranked most-depended-upon first (high Ca), then most unstable (high I) — the fragile hubs where a refactor is riskiest. Cycles() returns strongly-connected components of size > 1, largest first.

FileCoupling() maps each file's Path (as given to Build) to its node's Ca/Ce/I, so you can attribute node-level coupling back to individual files across any supported ecosystem without reproducing its node rule:

for path, c := range g.FileCoupling() {
	fmt.Printf("%-40s Ca=%d Ce=%d\n", path, c.Afferent, c.Efferent)
}

Convenience wrappers: Analyze(root, files) and FindCycles(root, files).

Install

go get github.com/richardwooding/go-coupling

The only dependency is github.com/pelletier/go-toml/v2 (for Cargo.toml).

Notes

  • Build reads the filesystem at root to detect the ecosystem and resolve first-party boundaries (Rust crate manifests, C/C++ include roots, the Python src/ and Ruby lib/ layouts). File.Path may be absolute or relative to the current working directory.
  • Resolution is name/structure-based and static: dynamic require, autoloading (Ruby Zeitwerk), and tsconfig path aliases aren't followed — documented per-adapter limitations shared with the source this was extracted from.
  • A root with no recognised manifest yields a non-analysable graph (empty results), not an error.

License

MIT — see LICENSE.

Documentation

Overview

Package coupling computes Robert C. Martin's package-coupling metrics — afferent coupling (Ca), efferent coupling (Ce), and instability (I = Ce/(Ca+Ce)) — and detects circular dependencies, across many language ecosystems.

It works on a first-party import graph it builds from per-file data the caller supplies (path, language, imports, declared package). The package itself does no parsing; pair it with a symbol extractor such as github.com/richardwooding/treesitter-symbols to obtain the imports and package for each file.

The ecosystem is detected from the build manifest at the project root — go.mod (Go packages), Cargo.toml (Rust crates), Package.swift (SwiftPM target modules), Maven/Gradle/sbt (JVM packages), .sln/.csproj (C# namespaces), a Python manifest (packages), package.json/tsconfig.json (JS/TS directory modules), a Perl dist manifest (:: packages), Gemfile/*.gemspec (Ruby directory modules), and CMake/autotools/meson (C/C++ #include directory modules). The first-party boundary and the file→node / import→node mappings are per-ecosystem; the graph math is language-agnostic.

g := coupling.Build(root, files)
for _, c := range g.Coupling() { ... } // Ca / Ce / I per package
for _, c := range g.Cycles()  { ... } // circular dependencies

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Coupling

type Coupling struct {
	Package     string  `json:"package"`     // node id (import path / crate / namespace / dir)
	Afferent    int     `json:"afferent"`    // Ca: distinct first-party nodes that import this one
	Efferent    int     `json:"efferent"`    // Ce: distinct first-party nodes this one imports
	Instability float64 `json:"instability"` // I = Ce / (Ca + Ce); 0 when isolated
}

Coupling is the afferent/efferent coupling profile of one first-party node (package / crate / namespace / directory module).

func Analyze

func Analyze(root string, files []File) []Coupling

Analyze is a convenience wrapper: Build(root, files).Coupling().

type Cycle

type Cycle struct {
	Nodes  []string `json:"nodes"`
	Length int      `json:"length"`
}

Cycle is one circular dependency: a strongly-connected component of size > 1 in the first-party import graph. Nodes is the sorted member set (not an edge-ordered path).

func FindCycles

func FindCycles(root string, files []File) []Cycle

FindCycles is a convenience wrapper: Build(root, files).Cycles().

type File

type File struct {
	// Path is the file's location on disk (absolute or cwd-relative). Used by
	// path-based ecosystems (Go/Rust/Python/JS-TS/Ruby/C-C++) to map a file to
	// its node.
	Path string
	// Language is the canonical language id ("go", "rust", "java", …).
	Language string
	// Imports are the file's import paths (absolute form).
	Imports []string
	// RelativeImports are dotted-relative imports with leading dots preserved
	// (Python); empty for other languages.
	RelativeImports []string
	// Package is the file's declared package / namespace, for the
	// declaration-based ecosystems (Java/Kotlin/Scala/C#/PHP/Perl). Ignored by
	// the path-based ones.
	Package string
}

File is the per-file input to Build: the data a caller has already extracted for one source file. Path may be absolute or relative to the current working directory.

type Graph

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

Graph is a first-party import graph built by Build. Query it with Graph.Coupling and Graph.Cycles; both are pure reads, so a Graph may be queried repeatedly and concurrently.

func Build

func Build(root string, files []File) *Graph

Build constructs the first-party import graph for the project rooted at root from files. root selects the ecosystem (via its build manifest) and anchors first-party resolution; each File carries the imports / package the caller already extracted.

When root carries no recognised manifest, the returned graph is non-analysable (Graph.Analysable is false) and its queries return empty — mirroring the "unknown ecosystem" case rather than erroring.

func (*Graph) Analysable

func (g *Graph) Analysable() bool

Analysable reports whether root carried a recognised build manifest. When false, Coupling and Cycles return empty.

func (*Graph) Coupling

func (g *Graph) Coupling() []Coupling

Coupling returns the per-node Ca/Ce/I profile, ranked most-depended-upon first (high Ca), then most unstable (high I), then by name — the "fragile hub" seams where a refactor is riskiest.

func (*Graph) Cycles

func (g *Graph) Cycles() []Cycle

Cycles returns the circular dependencies — strongly-connected components of size > 1 — ranked largest-first, then alphabetically by first member.

func (*Graph) FileCoupling added in v0.3.0

func (g *Graph) FileCoupling() map[string]Coupling

FileCoupling returns the coupling profile of each first-party file's node, keyed by the File.Path that was passed to Build. Files that mapped to no first-party node — an unmatched language, an empty/external node, or a file under a non-analysable root — are absent from the map.

It lets a caller attribute node-level Ca/Ce/instability back to individual files (many files may share one node's profile) without reproducing the ecosystem's node rule. Use the same Path form (absolute or relative) you gave Build; the returned keys echo it verbatim.

func (*Graph) Module

func (g *Graph) Module() string

Module returns the project identity: the go.mod module path, the Rust workspace root crate, or the root directory's base name for the declaration/directory ecosystems. "" when the ecosystem is unknown.

Jump to

Keyboard shortcuts

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