codemetrics

package module
v0.7.1 Latest Latest
Warning

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

Go to latest
Published: Jul 3, 2026 License: MIT Imports: 6 Imported by: 0

README

go-codemetrics

Go Reference

Per-function cyclomatic and cognitive complexity for source code — a CLI and GitHub Action that gate pull requests on complexity, plus a small Go library.

Most Go tools give you one metric or the other: fzipp/gocyclo does cyclomatic, uudashr/gocognit does cognitive. go-codemetrics computes both in one pass behind a single API, and is structured so more languages can be added behind Parse without breaking callers.

The Go analyzer is built on the standard library's go/astzero external dependencies.

The two metrics

  • Cyclomatic complexity (McCabe) counts decision points flatly: 1 + one per branch (if / for / range / case / comm-clause / && / ||). It measures the number of independent paths through a function.
  • Cognitive complexity (SonarSource) weights nested control flow more heavily, so deeply-nested logic scores higher than a flat sequence with the same number of branches. It tracks how hard code is to understand rather than how many paths it has. The implementation follows the SonarSource specification; results match uudashr/gocognit.

Install

CLI — Homebrew (macOS):

brew install --cask richardwooding/tap/codemetrics

The cask installs a pre-built binary. Since it isn't notarized, the cask strips the macOS quarantine attribute on install so it runs without a Gatekeeper prompt.

CLI — go install (any platform), or download a binary from the releases page:

go install github.com/richardwooding/go-codemetrics/cmd/codemetrics@latest

Library:

go get github.com/richardwooding/go-codemetrics

GitHub Action (PR complexity gate)

This repo ships a composite Action that runs the --diff gate on pull requests: it installs the released codemetrics binary and fails the check when a function the PR touches exceeds your threshold. No Docker image — it just downloads the binary for the runner.

name: complexity
on: pull_request
jobs:
  gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0 # required: full history for the diff's merge-base
      - uses: richardwooding/go-codemetrics@v0.7.1
        with:
          max-cognitive: "15"

Optionally upload SARIF so findings show up in the PR's Files-changed view:

    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: richardwooding/go-codemetrics@v0.7.1
        with:
          max-cognitive: "15"
          sarif-file: codemetrics.sarif
      - if: always() # upload even when the gate fails
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: codemetrics.sarif
    permissions:
      contents: read
      security-events: write # needed for the SARIF upload

Inputs: paths (default .), max-cognitive (default 15), max-cyclomatic (default 0), base-ref (default the PR base branch), baseline, sarif-file, fail-on-findings (default true), codemetrics-version (default latest). Runs on Linux and macOS runners.

CLI usage

The CLI is built on Kong and is polyglot: files are routed to a language backend by extension via projectdetect — Go through go/ast, the other 16 languages through the tree-sitter backend.

# Top 10 functions by cognitive complexity across a tree
codemetrics --top 10 ./...            # (pass directories or files)
codemetrics --top 10 internal/

# Sort by cyclomatic instead, only show the gnarly ones
codemetrics --sort cyclomatic --min 15 .

# JSON for tooling / CI
codemetrics --format json ./mypkg | jq '.[] | select(.cognitive > 20)'

# Read from stdin (Go by default; --lang for anything else)
cat foo.go | codemetrics
cat foo.py | codemetrics --lang python
COGNITIVE  CYCLOMATIC  LINES  FUNCTION            LOCATION
83         35          148    BuildCodeGraph      codegraph.go:172
74         39          175    UnusedExports       unused_exports.go:179
...

Directories are walked recursively, skipping dot-directories, testdata, and each detected project's build-artefact dirs (vendor, node_modules, target, __pycache__, …, resolved by projectdetect). Files whose extension maps to no supported language are ignored.

Flags: --sort cognitive|cyclomatic, --top N, --min N, --format table|json|sarif, --lang <id>, plus the quality-gate flags below.

Flag style changed. The CLI now uses GNU-style double-dash flags (--sort, --top, --min) and --json is replaced by --format json.

Quality gate: SARIF + baseline

Set a threshold and any function above it becomes a finding. Findings render as [SARIF 2.1.0][sarif] (via go-sarif) for GitHub Code Scanning, and the process exits non-zero when a finding remains — so it gates CI.

# Fail the build if any function is too complex; emit SARIF for Code Scanning
codemetrics --format sarif --max-cognitive 15 --max-cyclomatic 20 . > results.sarif

To adopt the gate on an existing codebase without fixing all debt first, record a baseline. Baselined findings (matched by rule + file + function, ignoring line numbers) are suppressed; the gate then fails only on new findings — the same pattern as detekt / semgrep.

# 1. Record today's findings as the accepted baseline
codemetrics --max-cognitive 15 --write-baseline .codemetrics-baseline.json .

# 2. In CI: pass unless a NEW violation is introduced
codemetrics --max-cognitive 15 --baseline .codemetrics-baseline.json .

A GitHub Actions step that uploads the SARIF:

- run: codemetrics --format sarif --max-cognitive 15 . > results.sarif
  continue-on-error: true          # let the upload run even when the gate fails
- uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: results.sarif

Quality-gate flags: --max-cognitive N, --max-cyclomatic N (0 = disabled), --baseline FILE, --write-baseline FILE.

PR mode: gate only what changed

--diff <ref> restricts the whole run to functions touched by a git diff, so a pull request is judged only on the code it actually changes — not the pre-existing tree. It parses git diff <ref> and keeps a function only when its line span overlaps a changed (new-side) line, then applies your thresholds to that subset.

# Fail the PR only if a function it touches is too complex
codemetrics --diff origin/main...HEAD --max-cognitive 15 .

<ref> is passed straight to git diff, so use whatever expresses "this PR": origin/main...HEAD (changes since the merge base — the usual PR diff), origin/main, or HEAD~1. Added files count in full; modified files count only their changed regions. Combine with --format sarif to upload PR-scoped findings, or with --baseline to also suppress known offenders among them.

On GitHub, the Action above wires this up for you; to run the CLI directly in a workflow instead:

- run: git fetch origin ${{ github.base_ref }}
- run: codemetrics --diff origin/${{ github.base_ref }}...HEAD --max-cognitive 15 .

The CLI embeds the tree-sitter grammars (~22 MB binary). The library packages stay dependency-light — this weight lives only in the codemetrics command.

Library usage

package main

import (
	"fmt"

	codemetrics "github.com/richardwooding/go-codemetrics"
)

func main() {
	src := []byte(`package p
func classify(n int) string {
	if n < 0 {
		return "neg"
	} else if n == 0 {
		return "zero"
	}
	return "pos"
}`)

	fns, err := codemetrics.ParseGo(src)
	if err != nil {
		panic(err)
	}
	for _, f := range fns {
		fmt.Printf("%-12s cyclomatic=%d cognitive=%d lines=%d\n",
			f.QualifiedName(), f.Cyclomatic, *f.Cognitive, f.Lines())
	}
	// classify     cyclomatic=3 cognitive=2 lines=8
}

Parse(language, src) dispatches by language identifier ("go" / "golang" today) and returns a wrapped ErrUnsupportedLanguage for anything else. SupportedLanguages() lists what's available.

type FunctionMetrics struct {
	Name       string // bare name, e.g. "Write"
	Receiver   string // receiver type for methods, e.g. "*Buffer"; "" otherwise
	Cyclomatic int
	Cognitive  *int   // nil if unavailable for the language; always set for Go
	StartLine  int    // 1-based, inclusive
	EndLine    int
}

Cognitive is a pointer so a language without cognitive support is distinguishable (nil) from a genuine zero. For Go it is always populated.

Already have an AST?

If you've already parsed Go source with go/parser, compute either metric for a single declaration without re-parsing:

func Cyclomatic(body *ast.BlockStmt) int // 1 + branch points
func Cognitive(fn *ast.FuncDecl) int     // SonarSource cognitive complexity

Both are nil-safe (return 0). This mirrors the AST-level entry points of gocyclo and gocognit.

Parsing is best-effort: input that still yields a partial syntax tree is tolerated and metrics are computed for every recovered function; only a total parse failure returns an error.

Other languages (tree-sitter)

The subpackage github.com/richardwooding/go-codemetrics/treesitter computes the same metrics for 16 more languages using the pure-Go tree-sitter runtime gotreesitter:

Python · JavaScript · TypeScript · Java · Rust · C · C++ · C# · Kotlin · PHP · Ruby · Scala · R · MATLAB · Perl · Swift

import "github.com/richardwooding/go-codemetrics/treesitter"

fns, err := treesitter.Parse("python", src) // -> []codemetrics.FunctionMetrics

It returns the same FunctionMetrics type, so the two backends are interchangeable. Cognitive complexity is computed for every language except Swift (whose grammar lacks a stable cognitive spec) — there Cognitive is nil while Cyclomatic is still reported.

Already parsed the tree? If you've parsed the source with gotreesitter yourself (e.g. while extracting symbols), compute metrics over that existing tree — no second parse:

func MetricsFromTree(language string, tree *ts.Tree, lang *ts.Language, spans []Span) []codemetrics.FunctionMetrics

This is how treesitter-symbols returns symbols and complexity from a single parse.

Dependencies stay opt-in. The module root is go/ast-only and pulls in nothing; gotreesitter and its embedded grammars are compiled in only when you import the treesitter subpackage. A plain build of that subpackage embeds every bundled grammar (~22 MB); to embed only the languages you use, build with the gotreesitter subset tags:

go build -tags 'grammar_subset grammar_subset_python grammar_subset_rust' ./...

License

MIT — see LICENSE.

Documentation

Overview

Package codemetrics computes per-function complexity metrics from source code: McCabe cyclomatic complexity and SonarSource cognitive complexity.

Cyclomatic complexity counts decision points flatly (1 + one per branch: if / for / range / case / comm-clause / && / ||). Cognitive complexity weights *nested* control flow more heavily, so deeply-nested logic scores higher than a flat sequence of the same number of branches — it tracks how hard code is to understand rather than how many paths it has.

The Go analyzer is built on the standard library's go/ast and has no external dependencies. The cognitive-complexity algorithm follows the SonarSource specification (reference implementation: github.com/uudashr/gocognit).

The public API is shaped so additional languages can be added behind Parse without breaking callers; today only Go is supported and FunctionMetrics.Cognitive is always populated. For languages that do not (yet) support cognitive complexity, Cognitive is nil — distinct from a genuine zero.

Index

Constants

This section is empty.

Variables

View Source
var ErrUnsupportedLanguage = errors.New("codemetrics: unsupported language")

ErrUnsupportedLanguage is returned by Parse for a language with no analyzer. Test for it with errors.Is.

Functions

func Cognitive added in v0.2.0

func Cognitive(fn *ast.FuncDecl) int

Cognitive returns the SonarSource cognitive complexity of a Go function declaration. Use this when you already hold a parsed AST; ParseGo derives the same value from source. A nil declaration (or one without a name) returns 0.

See the package documentation for the increment rules.

func Cyclomatic added in v0.2.0

func Cyclomatic(body *ast.BlockStmt) int

Cyclomatic returns the McCabe cyclomatic complexity of a function body — 1 + one per branch point (if / for / range / case / comm-clause / && / ||).

Use this when you already hold a parsed AST (e.g. from your own go/parser pass); ParseGo derives the same value from source. A nil body returns 0.

func SupportedLanguages

func SupportedLanguages() []string

SupportedLanguages returns the language identifiers accepted by Parse.

Types

type FunctionMetrics

type FunctionMetrics struct {
	// Name is the bare function or method name (e.g. "Write"), without the
	// receiver. For methods, Receiver carries the receiver type.
	Name string
	// Receiver is the receiver type of a method (e.g. "*Buffer" or "Buffer"),
	// or "" for a plain function.
	Receiver string
	// Cyclomatic is the McCabe cyclomatic complexity (1 + branch points).
	Cyclomatic int
	// Cognitive is the SonarSource cognitive complexity, or nil when the
	// language's analyzer does not compute it. For Go it is always set.
	Cognitive *int
	// StartLine and EndLine are the 1-based inclusive line span of the
	// function declaration.
	StartLine int
	EndLine   int
}

FunctionMetrics holds the complexity metrics for a single function or method, together with its 1-based inclusive line span.

func Parse

func Parse(language string, src []byte) ([]FunctionMetrics, error)

Parse computes complexity metrics for every function in src, dispatching on language. Recognised identifiers: "go" (alias "golang"). Unknown languages return a wrapped ErrUnsupportedLanguage.

Parsing is best-effort: input that still yields a partial syntax tree is tolerated and metrics are computed for every recovered function. A hard parse failure returns a non-nil error and no metrics.

func ParseGo

func ParseGo(src []byte) ([]FunctionMetrics, error)

ParseGo computes cyclomatic and cognitive complexity for every function and method declaration with a body in a Go source file. Functions without a body (e.g. external or interface declarations) are skipped.

Parsing is best-effort: if the parser recovers a partial syntax tree from malformed input, metrics are still computed for every function it found and the returned error is nil. A total parse failure (no tree) returns the parse error and no metrics.

func (FunctionMetrics) Lines

func (m FunctionMetrics) Lines() int

Lines returns the number of source lines the function spans (inclusive).

func (FunctionMetrics) QualifiedName

func (m FunctionMetrics) QualifiedName() string

QualifiedName returns the receiver-qualified name for a method (e.g. "(*Buffer).Write") or the bare name for a plain function.

Directories

Path Synopsis
cmd
codemetrics command
Command codemetrics reports per-function cyclomatic and cognitive complexity for source files, with table/JSON/SARIF output and a baseline quality gate.
Command codemetrics reports per-function cyclomatic and cognitive complexity for source files, with table/JSON/SARIF output and a baseline quality gate.
Package treesitter computes cyclomatic and cognitive complexity for the non-Go languages that go-codemetrics supports, using the pure-Go tree-sitter runtime github.com/odvcencio/gotreesitter and its bundled grammars.
Package treesitter computes cyclomatic and cognitive complexity for the non-Go languages that go-codemetrics supports, using the pure-Go tree-sitter runtime github.com/odvcencio/gotreesitter and its bundled grammars.

Jump to

Keyboard shortcuts

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