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]),
]
endnever 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.
Add the gem to your Gemfile's test group:
gem "simplecov-sorbet", require: falseThen 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.startRequiring 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.
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.
- 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)
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.
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.
MIT — see LICENSE.txt.