Halstead analyzer reference¶
The Halstead analyzer computes Halstead complexity metrics (1977) from the operators and operands of every function in the UAST.
For the conceptual model — what counts as an operator or operand, how the codebase aggregate is defined and what the thresholds mean — see Understanding Halstead metrics. To run it, see the Quick start.
codefang run -a static/halstead --format json ./src
codefang run -a static/halstead --format json --per-file ./src
codefang run -a static/halstead --format yaml ./src
codefang run -a static/halstead --format plot --output ./out ./src
Configuration options¶
The Halstead analyzer uses the UAST directly and has no analyzer-specific configuration options.
| Option | Type | Default | Description |
|---|---|---|---|
| (none) | -- | -- | Uses UAST; no analyzer-specific config |
The run-level flags apply: --per-file (json only — yaml and bin always report the codebase aggregate), --include-vendored / --include-generated, and --languages.
Definitions¶
| Symbol | Meaning | Formula |
|---|---|---|
n1 | distinct operators | — |
n2 | distinct operands | — |
N1 | total operators | — |
N2 | total operands | — |
Vocabulary n | n1 + n2 | |
Length N | N1 + N2 | |
| Estimated length | n1·log2(n1) + n2·log2(n2) | |
Volume V | N · log2(n) | |
Difficulty D | (n1 / 2) · (N2 / n2) | |
Effort E | D · V | |
Time T (seconds) | E / 18 | |
Bugs B | V / 3000 |
Classification table¶
| UAST node (inside a function body) | Counted as | Name |
|---|---|---|
leaf Identifier, leaf Literal | operand | its text (token, else the source span, else props.name / props.value) |
leaf Identifier with the Type role | operator (type specifier) | its text |
any node with props["operator"] | operator | that property, and nothing else from the node |
Call / Index / Slice | operator | fixed () / [] / [:] |
| every other node | operator(s) | the node's residue: its source span with its children blanked (:=, +, return and its trailing space, ., ++), split into symbol runs and whitelisted keywords |
Parameter (type or role), Comment, DocString | skipped | — |
| the function's signature (every child before its body) | skipped | — |
A quoted literal ("x") is one operand including its quotes; a composite literal or qualified name is classified by its parts, so fmt.Println yields the operands fmt, Println and the operator .. Closures and lambdas belong to the function that contains them; nested named functions and methods are their own records. A node type name is never a token and a statement's text is never an operand. The keyword whitelist and the canonical fallbacks are in crates/cf-halstead/src/classify.rs.
Aggregate¶
The codebase aggregate — and each --per-file entry — is the union of the operator and operand multisets of its functions: n1 / n2 are distinct across every function, N1 / N2 are the summed occurrences, and V / D / E / T / B derive from those four numbers. Per-function statistics (mean and max volume, difficulty and effort; the high-effort count and share) sit alongside.
Thresholds¶
All bands are lower-inclusive (a value at a bound is in the higher band).
| Per function | Low / Simple | Medium / Moderate | High / Complex | Very High |
|---|---|---|---|---|
Volume V (distribution, complexity_level) | V < 100 | 100 ≤ V < 1000 | 1000 ≤ V < 5000 | V ≥ 5000 |
Difficulty D (difficulty_assessment) | D < 5 | 5 ≤ D < 15 | D ≥ 15 | — |
Effort E (effort_assessment) | E < 1000 | 1000 ≤ E < 10000 | E ≥ 10000 | — |
- High-effort function:
E ≥ 10000(listed underhigh_effort_functions;risk_levelisHIGHfromE ≥ 50000, elseMEDIUM). - Issue severity:
poorwhenE ≥ 50000orB ≥ 1.0;fairwhenE ≥ 10000orB ≥ 0.3; elsegood. - Status message and health score come from the share
hof high-effort functions:
h | status / message | health_score |
|---|---|---|
| no functions | No functions found | 100 |
h < 5% | Low Halstead complexity - well-structured code | 100 → 90 |
5% ≤ h < 15% | Moderate Halstead complexity - acceptable | 90 → 70 |
15% ≤ h < 30% | High Halstead complexity - consider refactoring | 70 → 40 |
h ≥ 30% | Very high Halstead complexity - significant refactoring recommended | 40 → 0 |
The health score interpolates linearly between the anchors (0, 100), (5%, 90), (15%, 70), (30%, 40), (100%, 0). The section score is health_score / 100 and score_label is round(score · 10)/10.
Function lists (issues, function_halstead, high_effort_functions) are ordered by effort descending, then volume descending, then name, then file.
Output formats¶
The examples below are the real output for a two-file fixture: hal.go (c := a + b; return c * 2) and calls.go (three fmt.Println(...) calls).
JSON (--format json)¶
{
"overall_score_label": "10/10",
"sections": [
{
"title": "HALSTEAD",
"score_label": "10/10",
"status": "Low Halstead complexity - well-structured code",
"metrics": [
{ "label": "Total Functions", "value": "2" },
{ "label": "Distinct Operators (n1)", "value": "6" },
{ "label": "Distinct Operands (n2)", "value": "8" },
{ "label": "Total Operators (N1)", "value": "10" },
{ "label": "Total Operands (N2)", "value": "14" },
{ "label": "Vocabulary", "value": "14" },
{ "label": "Length", "value": "24" },
{ "label": "Volume", "value": "91.4" },
{ "label": "Difficulty", "value": "5.2" },
{ "label": "Effort", "value": "479.7" },
{ "label": "Est. Bugs", "value": "0.0" },
{ "label": "Mean Effort", "value": "77.4" },
{ "label": "Max Effort", "value": "87.2" },
{ "label": "High-Effort Functions", "value": "0" }
],
"distribution": [
{ "label": "Low (<100)", "percent": 1, "count": 2 },
{ "label": "Medium (100-1000)", "percent": 0, "count": 0 },
{ "label": "High (1000-5000)", "percent": 0, "count": 0 },
{ "label": "Very High (>=5000)", "percent": 0, "count": 0 }
],
"issues": [
{ "name": "calls", "location": "calls.go", "value": "effort=87.2 | vol=38.8 | bugs=0.0", "severity": "good" },
{ "name": "hal", "location": "hal.go", "value": "effort=67.5 | vol=27.0 | bugs=0.0", "severity": "good" }
],
"score": 1
}
],
"overall_score": 1
}
With --per-file the section gains a files array with one entry per analyzed file (function-free files included), each carrying the same metrics list computed from that file's own functions, its own distribution (absent when the file has no functions), its issues, and a score from its own high-effort share:
"files": [
{
"file_path": "calls.go",
"score_label": "10/10",
"status": "Low Halstead complexity - well-structured code",
"metrics": [
{ "label": "Total Functions", "value": "1" },
{ "label": "Distinct Operators (n1)", "value": "2" },
{ "label": "Distinct Operands (n2)", "value": "4" },
{ "label": "Total Operators (N1)", "value": "6" },
{ "label": "Total Operands (N2)", "value": "9" },
...
],
"distribution": [ ... ],
"issues": [ ... ],
"score": 1
},
...
]
A folder with no functions (or no analyzable files) produces the same shape with every count at 0, "status": "No functions found", no distribution, and empty issues / files; the exit code is 0.
YAML / bin (--format yaml, --format bin)¶
Both formats serialize the same value (bin wraps it in the CFB1 envelope):
function_halstead:
- name: calls
source_file: calls.go
language: go
directory: .
volume: 38.77443751081734
difficulty: 2.25
effort: 87.24248439933902
time_to_program: 4.846804688852168
delivered_bugs: 0.012924812503605781
complexity_level: Low
- name: hal
source_file: hal.go
language: go
directory: .
volume: 27
difficulty: 2.5
effort: 67.5
time_to_program: 3.75
delivered_bugs: 0.009
complexity_level: Low
distribution:
low: 2
medium: 0
high: 0
very_high: 0
high_effort_functions: []
aggregate:
total_functions: 2
volume: 91.3765181293825
difficulty: 5.25
effort: 479.72672017925817
time_to_program: 26.65148445440323
delivered_bugs: 0.030458839376460836
distinct_operators: 6
distinct_operands: 8
total_operators: 10
total_operands: 14
vocabulary: 14
length: 24
estimated_length: 39.50977500432694
mean_volume: 32.88721875540867
max_volume: 38.77443751081734
mean_difficulty: 2.375
max_difficulty: 2.5
mean_effort: 77.37124219966951
max_effort: 87.24248439933902
high_effort_functions: 0
high_effort_share: 0
health_score: 100
message: Low Halstead complexity - well-structured code
high_effort_functions entries carry name, source_file, volume, effort, time_to_program, delivered_bugs and risk_level.
Terminal (--format text, --format compact)¶
The section shows the status line and the same key-metrics list as JSON (union counts, derived measures, mean / max effort, high-effort count).
Plot (--format plot --output <dir>)¶
<dir>/report.json carries the raw report under halstead: every function in walk order with its counts, derived measures, assessments and the full operators / operands maps, plus the union aggregate, per-function statistics and health_score:
{
"name": "hal",
"_source_file": "hal.go", "_language": "go", "_directory": ".",
"distinct_operators": 4, "distinct_operands": 4,
"total_operators": 4, "total_operands": 5,
"vocabulary": 8, "length": 9, "estimated_length": 16,
"volume": 27, "difficulty": 2.5, "effort": 67.5,
"time_to_program": 3.75, "delivered_bugs": 0.009,
"volume_assessment": "🟢 Low",
"difficulty_assessment": "🟢 Simple",
"effort_assessment": "🟢 Low",
"operators": { "*": 1, "+": 1, ":=": 1, "return": 1 },
"operands": { "2": 1, "a": 1, "b": 1, "c": 2 }
}
The HTML page includes Top Functions by Effort, the Volume vs Difficulty risk map (bubble size: estimated bugs) and the Volume Distribution by bucket.
See also¶
- Understanding Halstead metrics — the mental model, counting policy, aggregation and limitations.
- Quick start — run static analysis.