--hotspots to
also rank the files that are both complex and frequently changed.
--limit 2:
actual/threshold so you can see how far past the line
it sits. The worst offenders come first.
Options
Every threshold can also be set persistently in a
config file, which is usually what you want in CI rather
than repeating six flags on each invocation.
The checks
Generated files are excluded from every check, as are files matched by the
config’s
ignore globs. A circular dependency that touches
a generated type is dropped whole rather than reported with that type left
out — like --exclude-tests, since a path with a hole in it would name edges
that aren’t there.
?? is not a branch
The null-coalescing operator doesn’t count towards cyclomatic complexity.
It’s a defaulting idiom, not control flow the reader has to trace — nobody
writes a second test case for the null side of name ?? "anonymous". Counting
it made hand-rolled with-style methods score wildly:
?? still shows up: the right-hand side is
walked like any other expression, so Load() ?? throw new InvalidOperationException()
or a ?? whose fallback contains a ternary is counted on its own merits.
??= never counted, since the grammar treats it as an assignment.
&& and || are unchanged and still cost one point each — that’s McCabe’s
definition, and unlike ?? they genuinely add a path a test has to cover.
Cognitive complexity continues to collapse a run of them to a single point.What “scanned” counts
The footer’sN project(s), N file(s), N symbol(s) scanned counts what was
eligible to be reported, not what was parsed. Anything ruled out up front —
test projects under --exclude-tests, files matched by the config’s
ignore globs (top-level or health.ignore) — is
subtracted, and named on a line of its own:
excluded: line only appears when something was actually excluded, so a
plain run is unchanged. Up to three project names are listed; past that the
line says +N more.
This matters most when the exclusion comes from a
config file, where there is no command line to eyeball:
health.excludeTests: true is otherwise invisible, and the footer is the one
place a reader can confirm both that the setting was picked up and that
their test project was recognized as one.
Generated files are not reported as an exclusion. They are never eligible
for any check, under any setting, so there is no setting for a reader to
confirm — unlike
--exclude-tests and ignore, which are choices that can
silently fail to apply.Sorting and severity
Severity is simply how many times over its threshold a finding sits —metric / threshold — which makes a cyclomatic complexity of 46 against a
limit of 10 directly comparable to a 242-line body against a limit of 40.
A declaration’s severity is the worst of its checks, and a file’s is the
worst of its declarations.
--sort severity (the default) puts the worst thing in the codebase first.
--sort path groups by file path instead, which is stable regardless of the
metrics and is the better choice when diffing two runs. Both are
deterministic: severity ties break on location.
--limit caps the human report only. When it bites, roe says how many
findings it held back:
--sort and --limit are presentation only and don’t apply to
--format json, which always emits every finding. Tooling does its own
ordering, and a silently truncated array would be actively misleading.Large types report a breakdown
Thirty auto-properties is a data holder; thirty methods is a god class. roe doesn’t guess which one you have — it prints the composition and lets you judge:const fields don’t count either, for the same reason: a const has no
behaviour and no state at runtime — it’s a name for a literal, inlined at
every call site — so it can’t be part of the cohesion problem this check
looks for. A class holding fifty tuning constants is a lookup table, not a god
class. static readonly fields do still count: roe has no type analysis, so
it can’t tell a static readonly float tuning value from a static readonly HttpClient that the type genuinely depends on.
Only required parameters count
The point of a parameter limit is call-site burden: how much a caller has to supply, and how much they have to keep straight while doing it. A parameter the caller can leave out costs them nothing, so it isn’t counted. A parameter is required when the caller has to pass something and think about what. That means:
Findings print the whole picture, so nothing is hidden — the metric is the
required count, and the declared signature follows in parentheses:
6 declared: 6 required next to 6/5 params would be noise.
A method with a dozen
out parameters is still worth a second look — it’s
usually asking to return a record instead. roe just doesn’t report it
under a check whose stated meaning is “too much to pass in”. The declared
total is always printed, so a long signature is still visible in the report
and in --format json.Overloads
Overloads share a name, so two flagged rows could otherwise be indistinguishable. Where that collision actually happens, roe appends a Roslyn-style arity suffix — and only there, since an arity on a name with no overloads is noise:Circular dependencies
A cycle is reported as a chain of real references, each type pointing at the next and the last pointing back at the first. Larger tangles usually contain more types than any single loop through them touches; those are listed separately rather than being spliced into the path, which would imply references that don’t exist:Hotspots
--hotspots reads git history and ranks files by complexity multiplied by
recent churn — the files that are both hard to understand and constantly
being changed, which is where refactoring pays off most. Commits are weighted
by recency on a 90-day half-life, so last month’s churn counts for more than
last year’s.
100 and everything
else is measured against it, so they compare files within one run rather than
across runs or repositories.
Hotspots are informational and never affect the exit code — every
codebase has a riskiest file, and failing a build over the existence of a
ranking would make the check useless as a CI gate.
This is the one part of roe that reads git history, so the analysis root
must be inside a git repository. If it isn’t,
--hotspots fails with exit
2 rather than silently reporting nothing.Baselines
Turningroe health on over a codebase that didn’t have it from day one
means starting at a few hundred findings, which fails every build until
someone fixes all of them. A baseline records what’s already there so CI can
gate on new debt from the first day, and the existing debt gets paid down
on its own schedule.
--write-baseline writes the file, reports what it recorded on stderr, and
exits 0 without printing a report:
--baseline roe-baseline.json reports and
exits on new findings only, and the footer says how many it hid:
The file
roe-baseline.json
kind and then name, so the file diffs
cleanly and a regenerated baseline shows only what actually changed. Unknown
fields and unknown versions are rejected rather than ignored, the same way
config files are.
What matches
A finding is hidden when its kind and name match a baseline entry. The line number is deliberately not part of the match: a baselined method stays baselined when something above it in the file grows by three lines, which is the whole point of recording it once.file is written for readability and
diffing and isn’t matched on either.
Cycles match on their set of member names, regardless of the order the path
happens to be printed in.
Metric regressions still report
A matched finding whose metric is higher than the baselined value is reported anyway. A method going from cyclomatic 12 to 30 is new debt in an old place, and a baseline that hid it would let a rewrite land unexamined under the cover of an entry that was about something much smaller. Equal or lower stays hidden.Stale entries
A baseline entry that no longer matches anything — the method was fixed, renamed, or deleted — is reported as a warning, never a failure:In CI
health.baseline in a config file instead if you’d rather
a bare roe and roe check picked it up too.
Suppressing findings
Individual findings take inline suppression comments using the rule nameshigh-complexity,
high-cognitive-complexity, long-method, too-many-parameters,
large-file, and large-type:
health.ignore scopes the suppression to health alone, so the
file keeps its dead-code and dupes coverage.