SimpleCov::Sorbet
A SimpleCov extension for Sorbet codebases: it skips type-level Sorbet constructs that coverage should not measure, so they stop reading as coverage misses.
Three constructs are skipped:
- Multi-line
T.type_aliasblocks. sorbet-runtime resolves aliases lazily and collection checks are shallow, so the block body of
ResolvedSegment = T.type_alias do
[
CommentParser::Segment,
T.nilable(String),
T.nilable(T::Hash[Symbol, T.untyped]),
]
end
never executes — and SimpleCov (and any patch-coverage gate built on it, like Codecov's) reports those lines as uncovered.
-
sigblocks (bare or with a receiver such asT::Sig::WithoutRuntime.sig). Sigs are type metadata whose correctnesssrb tcowns; coverage should measure behavior. Skipping them sharpens the signal: an untested method reports its body as the miss, without sig-line noise — andT::Sig::WithoutRuntimesigs, which never evaluate at runtime, stop being permanent false positives. Detection matches anysigcall with a multi-line block; a non-Sorbet DSL also namedsigwould be over-matched, an accepted risk in a Sorbet codebase. -
T.absurdsends. Exhaustiveness checks are unreachable by definition when the code is correct — Sorbet statically proves theelsebranch can't be taken — so in healthy code the line is a permanent miss.
The usual workarounds are cramming constructs onto one line or sprinkling # simplecov:disable comments; both encode a tool-compatibility fact into every file. This gem moves that knowledge to the layer that owns it: detection is purely syntactic (a Prism-backed AST pass via ast_transform), and the found ranges feed straight into SimpleCov's skip machinery.
Installation
Add the gem to your Gemfile's test group:
gem "simplecov-sorbet", require: false
Then require it anywhere near your SimpleCov setup — before or after SimpleCov.start, both work (ranges are consumed at report time):
require "simplecov"
require "simplecov/sorbet"
SimpleCov.start
How it works
Requiring simplecov/sorbet prepends an extension onto SimpleCov::Directive.disabled_ranges, the seam SimpleCov 1.0 consults for # simplecov:disable ranges — for both loaded files (SourceFile) and tracked-but-unloaded files (LinesClassifier). The extension parses each covered file, collects the line range of every ignored construct (T.type_alias blocks including the ::T form, multi-line sig blocks, and T.absurd sends), and appends those ranges to all three directive categories (line, branch, method — type-level constructs are not behavior, so anything inside them is skippable). Files the parser rejects contribute no ranges and are otherwise reported untouched.
What is deliberately not skipped
Regular sig behavior is unaffected: sorbet-runtime evaluates a sig block lazily on the method's first call, so before this gem an uncovered sig always accompanied an uncovered method body. Skipping sigs removes the duplicate line, not the signal — the untested body still reports as a miss. One tradeoff to know about: a sig coverage miss on a tested method once exposed a real bug (module_function copies methods before the sig wrapper installs, silently disabling runtime validation). With sigs skipped, that class of bug is RuboCop's to catch (Style/ModuleFunction: EnforcedStyle: extend_self), not coverage's.
Skips apply through SimpleCov's directive machinery, which drives its line classification; if you enable branch coverage, branches inside skipped ranges are covered by the branch directive category the extension also populates.
Requirements
- Ruby >= 3.3
- simplecov ~> 1.0
- A Sorbet codebase (the gem depends on sorbet-runtime for its own inline sigs; it never loads your type system)
Development
This repo is managed with d3mlabs' dev tool (dev up, dev test, dev style, dev typecheck), but plain Bundler works without it: bundle install, then bundle exec rake test. The Ruby version is declared in dependencies.rb and mirrored in .ruby-version.
Releasing a New Version
From a clean checkout of main:
dev release # auto-increments the patch version (0.1.0 → 0.1.1)
dev release 0.2.0 # explicit version
The script (bin/release.rb) bumps lib/simplecov/sorbet/version.rb and Gemfile.lock, commits, tags v<version>, pushes, creates the GitHub release, and then watches the release workflow — which validates that the tag matches version.rb, builds the gem, and publishes it to rubygems.org via Trusted Publishing — until the publish succeeds.
License
MIT — see LICENSE.txt.