simplecov-sorbet 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 769829fc88734ccfb203a5472a779e267ba07dac55d6fd6099955fd9de090740
4
+ data.tar.gz: a948a59b02a9e94812f85ebdce285629d4b665617209df09b20641069498ac94
5
+ SHA512:
6
+ metadata.gz: 577b36bd437cacde42b28a22b17d480b7983cfd6ab88c93ed14a955dc79656e650d8cf233a539c173e87a1277fa9681b4855a0128755c4464b2c58e0bfe36c0c
7
+ data.tar.gz: fea6c1ed8d99d3d66111bcaad5369e77867321025b16730b0b12e4e6c2d374f8c288db3eb8a0264fe019622e352c981f99db3361164b02a14266234cd4e5f62f
@@ -0,0 +1,49 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ lint:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: ruby/setup-ruby@v1
15
+ with:
16
+ ruby-version: '4.0'
17
+ bundler-cache: true
18
+ - run: bundle exec rubocop
19
+
20
+ typecheck:
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+ - uses: ruby/setup-ruby@v1
25
+ with:
26
+ ruby-version: '4.0'
27
+ bundler-cache: true
28
+ - run: bundle exec srb tc
29
+
30
+ test:
31
+ runs-on: ubuntu-latest
32
+ strategy:
33
+ fail-fast: false
34
+ matrix:
35
+ ruby: ['3.3', '3.4', '4.0']
36
+ steps:
37
+ - uses: actions/checkout@v4
38
+ - uses: ruby/setup-ruby@v1
39
+ with:
40
+ ruby-version: ${{ matrix.ruby }}
41
+ bundler-cache: true
42
+ - run: bundle exec rake test
43
+ - name: Upload coverage to Codecov
44
+ if: matrix.ruby == '3.3'
45
+ uses: codecov/codecov-action@v5
46
+ with:
47
+ token: ${{ secrets.CODECOV_TOKEN }}
48
+ files: coverage/coverage.json
49
+ fail_ci_if_error: false
@@ -0,0 +1,30 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ['v*']
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ id-token: write
12
+ contents: read
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: ruby/setup-ruby@v1
16
+ with:
17
+ ruby-version: '3.3'
18
+ - name: Verify version matches tag
19
+ run: |
20
+ TAG_VERSION="${GITHUB_REF#refs/tags/v}"
21
+ GEM_VERSION=$(ruby -e "require './lib/simplecov/sorbet/version'; puts SimpleCov::Sorbet::VERSION")
22
+ if [ "$TAG_VERSION" != "$GEM_VERSION" ]; then
23
+ echo "::error::Tag ($TAG_VERSION) does not match gem version ($GEM_VERSION)"
24
+ exit 1
25
+ fi
26
+ - uses: rubygems/configure-rubygems-credentials@main
27
+ - name: Build and publish gem
28
+ run: |
29
+ gem build simplecov-sorbet.gemspec
30
+ gem push simplecov-sorbet-*.gem
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ *.gem
2
+ /.bundle/
3
+ /coverage/
4
+ /pkg/
5
+ /tmp/
6
+ /.idea
7
+
8
+ # Generated per-machine by `dev up` (d3mlabs dev tool); never committed
9
+ /.shadowenv.d/
10
+
11
+ # Generated by dev knowledge sync; never committed
12
+ /.cursor/rules/org-invariants.mdc
data/.rubocop.yml ADDED
@@ -0,0 +1,15 @@
1
+ inherit_gem:
2
+ rubocop-shopify: rubocop.yml
3
+
4
+ plugins:
5
+ - rubocop-sorbet
6
+
7
+ AllCops:
8
+ SuggestExtensions: false
9
+
10
+ # Tests are written in the RSpock dialect: bare comparisons in Then/Expect
11
+ # blocks and block-name constants (Given/When/Then/Where/Cleanup) are
12
+ # rewritten into assertions by the AST transform, not evaluated for value.
13
+ Lint/Void:
14
+ Exclude:
15
+ - test/**/*_test.rb
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 4.0.6
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.1.0] - 2026-07-29
8
+ ### Initial release
9
+ - `T.type_alias` block skipping: requiring `simplecov/sorbet` prepends an extension onto `SimpleCov::Directive.disabled_ranges` that parses each covered file (Prism via ast_transform's analysis API) and appends every alias block's line range to all three directive categories. Multi-line alias bodies are runtime-unreachable by design and no longer read as coverage misses.
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify the gem's runtime dependencies in simplecov-sorbet.gemspec
6
+ gemspec
7
+
8
+ # Development dependencies
9
+ gem "bundler", ">= 2.1"
10
+ gem "minitest", "~> 5.14"
11
+ gem "minitest-reporters", "~> 1.4"
12
+ gem "rake", "~> 13.0"
13
+ gem "rspock", "~> 3.0"
14
+ gem "rubocop-shopify", "~> 3.0", require: false
15
+ gem "rubocop-sorbet", ">= 0.10", require: false
16
+ gem "sorbet", require: false
17
+ gem "tapioca", require: false
data/Gemfile.lock ADDED
@@ -0,0 +1,198 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ simplecov-sorbet (0.1.0)
5
+ ast_transform (~> 3.1)
6
+ simplecov (~> 1.0)
7
+ sorbet-runtime
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ ansi (1.6.0)
13
+ ast (2.4.3)
14
+ ast_transform (3.1.0)
15
+ parser (>= 3.3)
16
+ prism (>= 1.5)
17
+ unparser (>= 0.8)
18
+ benchmark (0.5.0)
19
+ builder (3.3.0)
20
+ diff-lcs (2.0.0)
21
+ erubi (1.13.1)
22
+ json (2.21.1)
23
+ language_server-protocol (3.17.0.6)
24
+ lint_roller (1.1.0)
25
+ logger (1.7.0)
26
+ minitest (5.27.0)
27
+ minitest-reporters (1.8.0)
28
+ ansi
29
+ builder
30
+ minitest (>= 5.0, < 7)
31
+ ruby-progressbar
32
+ mocha (3.1.0)
33
+ ruby2_keywords (>= 0.0.5)
34
+ netrc (0.11.0)
35
+ parallel (2.1.0)
36
+ parser (3.3.12.0)
37
+ ast (~> 2.4.1)
38
+ racc
39
+ prism (1.9.0)
40
+ racc (1.8.1)
41
+ rainbow (3.1.1)
42
+ rake (13.4.2)
43
+ rbi (0.4.1)
44
+ prism (~> 1.0)
45
+ rbs (>= 4.0.1)
46
+ rbs (4.1.0)
47
+ logger
48
+ prism (>= 1.6.0)
49
+ tsort
50
+ regexp_parser (2.12.0)
51
+ require-hooks (0.4.1)
52
+ rexml (3.4.4)
53
+ rspock (3.0.0)
54
+ ast_transform (~> 3.0)
55
+ minitest (~> 5.0)
56
+ mocha (>= 1.0)
57
+ rubocop (1.88.2)
58
+ json (~> 2.3)
59
+ language_server-protocol (~> 3.17.0.2)
60
+ lint_roller (~> 1.1.0)
61
+ parallel (>= 1.10)
62
+ parser (>= 3.3.0.2)
63
+ rainbow (>= 2.2.2, < 4.0)
64
+ regexp_parser (>= 2.9.3, < 3.0)
65
+ rubocop-ast (>= 1.49.0, < 2.0)
66
+ ruby-progressbar (~> 1.7)
67
+ unicode-display_width (>= 2.4.0, < 4.0)
68
+ rubocop-ast (1.50.0)
69
+ parser (>= 3.3.7.2)
70
+ prism (~> 1.7)
71
+ rubocop-shopify (3.0.1)
72
+ lint_roller
73
+ rubocop (~> 1.72, >= 1.72.1)
74
+ rubocop-sorbet (0.13.2)
75
+ lint_roller
76
+ rubocop (>= 1.75.2)
77
+ ruby-progressbar (1.13.0)
78
+ ruby2_keywords (0.0.5)
79
+ rubydex (0.3.0-aarch64-linux)
80
+ rubydex (0.3.0-arm64-darwin)
81
+ rubydex (0.3.0-x86_64-darwin)
82
+ rubydex (0.3.0-x86_64-linux)
83
+ simplecov (1.0.3)
84
+ sorbet (0.6.13367)
85
+ sorbet-static (= 0.6.13367)
86
+ sorbet-runtime (0.6.13367)
87
+ sorbet-static (0.6.13367-aarch64-linux)
88
+ sorbet-static (0.6.13367-universal-darwin)
89
+ sorbet-static (0.6.13367-x86_64-linux)
90
+ sorbet-static-and-runtime (0.6.13367)
91
+ sorbet (= 0.6.13367)
92
+ sorbet-runtime (= 0.6.13367)
93
+ spoom (1.8.5)
94
+ erubi (>= 1.10.0)
95
+ prism (>= 0.28.0)
96
+ rbi (>= 0.4.1)
97
+ rbs (>= 4.0.0.dev.5)
98
+ rexml (>= 3.2.6)
99
+ sorbet-static-and-runtime (>= 0.5.10187)
100
+ thor (>= 0.19.2)
101
+ tapioca (0.19.2)
102
+ benchmark
103
+ bundler (>= 2.2.25)
104
+ netrc (>= 0.11.0)
105
+ parallel (>= 1.21.0)
106
+ rbi (>= 0.3.7)
107
+ require-hooks (>= 0.2.2)
108
+ rubydex (>= 0.1.0.beta10)
109
+ sorbet-static-and-runtime (>= 0.6.12698)
110
+ spoom (>= 1.7.16)
111
+ thor (>= 1.2.0)
112
+ tsort
113
+ thor (1.5.0)
114
+ tsort (0.2.0)
115
+ unicode-display_width (3.2.0)
116
+ unicode-emoji (~> 4.1)
117
+ unicode-emoji (4.2.0)
118
+ unparser (0.9.0)
119
+ diff-lcs (>= 1.6, < 3)
120
+ parser (>= 3.3.0)
121
+ prism (>= 1.5.1)
122
+
123
+ PLATFORMS
124
+ aarch64-linux
125
+ arm64-darwin
126
+ universal-darwin
127
+ x86_64-darwin
128
+ x86_64-linux
129
+
130
+ DEPENDENCIES
131
+ bundler (>= 2.1)
132
+ minitest (~> 5.14)
133
+ minitest-reporters (~> 1.4)
134
+ rake (~> 13.0)
135
+ rspock (~> 3.0)
136
+ rubocop-shopify (~> 3.0)
137
+ rubocop-sorbet (>= 0.10)
138
+ simplecov-sorbet!
139
+ sorbet
140
+ tapioca
141
+
142
+ CHECKSUMS
143
+ ansi (1.6.0) sha256=ac9ea0c0ea8d32fb4e271348e609963ac78882f34b73836c2a02b3622e666658
144
+ ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
145
+ ast_transform (3.1.0) sha256=e1b80d7600e20439407d446b813457343cd5adc9d01287d68cc7e4d8dc60643c
146
+ benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
147
+ builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
148
+ bundler (4.0.17) sha256=214e21431b5665dd2f99df8a5511c6b151d7a72e8015c8b38f8b775b61cbb6c1
149
+ diff-lcs (2.0.0) sha256=708a5d52ec2945b50f8f53a181174aa1ef2c496edf81c05957fe956dabb363d5
150
+ erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
151
+ json (2.21.1) sha256=13a43df75d95641443f5702dff350f237164a9d811ff0f2c2800d4d980220583
152
+ language_server-protocol (3.17.0.6) sha256=5ef2c0c138f8267e1bc631d3328347d354f96724b0af22f2c79516120443b7f0
153
+ lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
154
+ logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
155
+ minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5
156
+ minitest-reporters (1.8.0) sha256=8ce5280fb73ad3178ae525454df169b6f28c1b38b1d088ea91815d3a370ba384
157
+ mocha (3.1.0) sha256=75f42d69ebfb1f10b32489dff8f8431d37a418120ecdfc07afe3bc183d4e1d56
158
+ netrc (0.11.0) sha256=de1ce33da8c99ab1d97871726cba75151113f117146becbe45aa85cb3dabee3f
159
+ parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356
160
+ parser (3.3.12.0) sha256=21a6d7f755d5a24dfbdc6e6b772e4e879a52e7631a88bc5a3a134606052c9828
161
+ prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
162
+ racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
163
+ rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
164
+ rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
165
+ rbi (0.4.1) sha256=66611ca331b0b47d98607a7afda12ab44e0a98297d5393d4b93b846b9786d44d
166
+ rbs (4.1.0) sha256=8baba59008b0643b4ba2090e9b1d0149655b0bbd42eb2ffe42b1d0eb6923fd72
167
+ regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
168
+ require-hooks (0.4.1) sha256=3a4cd3ed0131b9ec5c2e68f64cb0c5ba56daec6fc363d56e8eef74816c374d7d
169
+ rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
170
+ rspock (3.0.0) sha256=279095618eb3f154a6770829a1b8537d65bb22a41555f5d89a727aeb19d0bb1e
171
+ rubocop (1.88.2) sha256=8def251c90cd955feb4daa3edc0ab56893250c4ce90ef81e6c80c03f9a939bbf
172
+ rubocop-ast (1.50.0) sha256=b9ca88300da0803ee222ad20cdb30494c0a784eed06fdc35d254b06d662788db
173
+ rubocop-shopify (3.0.1) sha256=4adffa6313294bd9da2b0896ae44c5eb8e419336b2413de20c38b7691a7e6774
174
+ rubocop-sorbet (0.13.2) sha256=7901e57b6b8e9e9b970d941dbdbb89f2c9de0183bfd2551460468f8aa9548655
175
+ ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
176
+ ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef
177
+ rubydex (0.3.0-aarch64-linux) sha256=a7221b71b21c5a1cdda1fbac0946ee0411a2efd94c8115cf37e4e69461d88e73
178
+ rubydex (0.3.0-arm64-darwin) sha256=19f685610216e4e7f488722ac90dfbc33113a384dd16db309cf939606f4c32dd
179
+ rubydex (0.3.0-x86_64-darwin) sha256=59d20c98b0bf2226f5db5bee14cf57075aa93aa0c3fa0176f496a217691ff7b6
180
+ rubydex (0.3.0-x86_64-linux) sha256=dfe4026591f226b4a1b53f82f86dc14fad2e7c221ee8a5be99a0a46bf2d58b04
181
+ simplecov (1.0.3) sha256=38ef0514f16ae7562f0d0f4df02610071115103d301b6de7dacbcc000082e39b
182
+ simplecov-sorbet (0.1.0)
183
+ sorbet (0.6.13367) sha256=099d9f5f420e09606deb84c9110f352e1caa970eabc01978c55dc6eb089858ce
184
+ sorbet-runtime (0.6.13367) sha256=4891cafce050f32f0e03f593a63cfa2ed535e541633b75a035ed36fb1b067d42
185
+ sorbet-static (0.6.13367-aarch64-linux) sha256=46416a8ed47e5c12d129a6f6418a98d2bb56d8ebeaea6d6a6a47306b4307acc5
186
+ sorbet-static (0.6.13367-universal-darwin) sha256=4b917d62aa2429273ea9af3cdfb3da9586e2524e9acdf33b9a3745cbc2a03aa5
187
+ sorbet-static (0.6.13367-x86_64-linux) sha256=232f7ab7f97bdb5becb0e0d23b154f9e995fa20b3a3b6e21161c80d614f9f394
188
+ sorbet-static-and-runtime (0.6.13367) sha256=be34c7aa16c9e73e63a9aa466619d78443649da210caf766a04b2042c9624c2a
189
+ spoom (1.8.5) sha256=c69e8d00ae4aaca98fc55087c4e402c276c45d9661536f0f7ec7b1cdf82a4cc7
190
+ tapioca (0.19.2) sha256=938731b07811aee8d23871b1aee8861d464fbaf2cfffbf79a62b0c869a5120ec
191
+ thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
192
+ tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
193
+ unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
194
+ unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
195
+ unparser (0.9.0) sha256=4331f174a73a23b69250b13d47da3794ed1449711ee0f9ed8947dc020ba76067
196
+
197
+ BUNDLED WITH
198
+ 4.0.17
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Jean-Philippe Duchesne
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # SimpleCov::Sorbet
2
+
3
+ A [SimpleCov](https://github.com/simplecov-ruby/simplecov) extension for [Sorbet](https://sorbet.org) codebases: it skips constructs Sorbet makes runtime-unreachable by design, so they stop reading as coverage misses.
4
+
5
+ Today that means multi-line `T.type_alias` blocks. sorbet-runtime resolves aliases lazily and collection checks are shallow, so the block body of
6
+
7
+ ```ruby
8
+ ResolvedSegment = T.type_alias do
9
+ [
10
+ CommentParser::Segment,
11
+ T.nilable(String),
12
+ T.nilable(T::Hash[Symbol, T.untyped]),
13
+ ]
14
+ end
15
+ ```
16
+
17
+ never executes — and SimpleCov (and any patch-coverage gate built on it, like Codecov's) reports those lines as uncovered. The usual workarounds are cramming the alias onto one line or sprinkling `# simplecov:disable` comments; both encode a tool-compatibility fact into every file that has an alias. This gem moves that knowledge to the layer that owns it: detection is purely syntactic (a [Prism](https://github.com/ruby/prism)-backed AST pass via [ast_transform](https://github.com/rspockframework/ast-transform)), and the found ranges feed straight into SimpleCov's skip machinery.
18
+
19
+ ## Installation
20
+
21
+ Add the gem to your Gemfile's test group:
22
+
23
+ ```ruby
24
+ gem "simplecov-sorbet", require: false
25
+ ```
26
+
27
+ Then require it anywhere near your SimpleCov setup — before or after `SimpleCov.start`, both work (ranges are consumed at report time):
28
+
29
+ ```ruby
30
+ require "simplecov"
31
+ require "simplecov/sorbet"
32
+
33
+ SimpleCov.start
34
+ ```
35
+
36
+ ## How it works
37
+
38
+ 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 `T.type_alias` block (including the `::T` form), and appends those ranges to all three directive categories (`line`, `branch`, `method` — the block body is runtime-unreachable, so anything inside it is skippable). Files the parser rejects contribute no ranges and are otherwise reported untouched.
39
+
40
+ ## Requirements
41
+
42
+ - Ruby >= 3.3
43
+ - simplecov ~> 1.0
44
+ - A Sorbet codebase (the gem depends on sorbet-runtime for its own inline sigs; it never loads your type system)
45
+
46
+ ## Development
47
+
48
+ 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`.
49
+
50
+ ## Releasing a New Version
51
+
52
+ 1. Update `VERSION` in `lib/simplecov/sorbet/version.rb`, run `bundle install` to regenerate `Gemfile.lock`, commit, open a PR, and merge to main
53
+ 2. Tag and push:
54
+ ```
55
+ git checkout main && git pull
56
+ git tag v0.1.0
57
+ git push origin v0.1.0
58
+ ```
59
+
60
+ The [release workflow](.github/workflows/release.yml) validates that the tag matches `version.rb`, builds the gem, and publishes it to [rubygems.org](https://rubygems.org) via [Trusted Publishing](https://guides.rubygems.org/trusted-publishing/).
61
+
62
+ ## License
63
+
64
+ MIT — see [LICENSE.txt](LICENSE.txt).
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ # test_loader runs first (-r) so rspock and ASTTransform.install are set up
7
+ # before any test file loads (the rspock dialect is rewritten at load time).
8
+ Rake::TestTask.new(:test) do |t|
9
+ t.libs << "test"
10
+ t.libs << File.expand_path("lib", __dir__)
11
+ t.ruby_opts << "-r #{File.expand_path('test/test_loader.rb', __dir__)}"
12
+ t.test_files = FileList["test/**/*_test.rb"]
13
+ t.warning = false
14
+ end
15
+
16
+ task default: :test
data/bin/tapioca ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'tapioca' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ require "rubygems"
14
+ require "bundler/setup"
15
+
16
+ load Gem.bin_path("tapioca", "tapioca")
data/dependencies.rb ADDED
@@ -0,0 +1,12 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ # Toolchain-only manifest for d3mlabs' dev tool: it provisions this exact
5
+ # Ruby (rbenv + shadowenv) for `dev` commands. Gems stay bundler-managed
6
+ # through the hand-written gemspec/Gemfile; contributors without dev can
7
+ # ignore this file and use .ruby-version.
8
+ require "dev/deps"
9
+
10
+ Dev::Deps.define do
11
+ ruby "4.0.6"
12
+ end
data/dev.yml ADDED
@@ -0,0 +1,14 @@
1
+ name: simplecov-sorbet
2
+ commands:
3
+ up:
4
+ desc: Install gems
5
+ run: bundle install
6
+ test:
7
+ desc: Run this repo's tests
8
+ run: bundle exec rake test
9
+ style:
10
+ desc: Run RuboCop
11
+ run: bundle exec rubocop
12
+ typecheck:
13
+ desc: Run Sorbet
14
+ run: bundle exec srb tc
@@ -0,0 +1,51 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module SimpleCov
5
+ module Sorbet
6
+ # Prepended onto SimpleCov::Directive's singleton class. Directive.disabled_ranges is the one choke point both
7
+ # SourceFile (loaded files) and LinesClassifier (tracked-but-unloaded files) consult for skip ranges, so
8
+ # extending its result covers every path SimpleCov classifies lines through. Alias ranges join all three
9
+ # categories: the block body is runtime-unreachable, so any line, branch, or method inside it is skippable.
10
+ module DirectiveExtension
11
+ extend T::Sig
12
+
13
+ sig { params(src_lines: T::Array[String]).returns(T::Hash[Symbol, T::Array[T::Range[Integer]]]) }
14
+ def disabled_ranges(src_lines)
15
+ ranges = T.let(super, T::Hash[Symbol, T::Array[T::Range[Integer]]])
16
+ alias_ranges = type_alias_ranges(src_lines.join)
17
+ return ranges if alias_ranges.empty?
18
+
19
+ ranges.each_value { |category_ranges| category_ranges.concat(alias_ranges) }
20
+ ranges
21
+ end
22
+
23
+ private
24
+
25
+ # Scans +source+ for +T.type_alias+ blocks. Unparseable source yields no ranges: coverage annotation must
26
+ # never take down a suite's reporting, and a file Ruby executed but this parser rejects has no aliases we
27
+ # could trust anyway. The rescue is deliberately wide — Prism parses most broken source tolerantly, and what
28
+ # escapes is not Parser::SyntaxError but arbitrary errors from the whitequark builder choking on
29
+ # error-recovered trees (e.g. NoMethodError in join_exprs).
30
+ #
31
+ # @param source [String] The Ruby source to scan.
32
+ #
33
+ # @return [Array<Range<Integer>>] One line range per alias block, in source order.
34
+ sig { params(source: String).returns(T::Array[T::Range[Integer]]) }
35
+ def type_alias_ranges(source)
36
+ TypeAliasRanges.new.run(source_parser.parse(source)).ranges
37
+ rescue StandardError
38
+ []
39
+ end
40
+
41
+ # The parser is fixed for the extension's lifetime. Prepended onto SimpleCov::Directive's singleton class,
42
+ # this module has no constructor to own it, so the singleton's ivar plays that role.
43
+ #
44
+ # @return [ASTTransform::SourceParser] The memoized parser.
45
+ sig { returns(ASTTransform::SourceParser) }
46
+ def source_parser
47
+ @source_parser ||= T.let(ASTTransform::SourceParser.new, T.nilable(ASTTransform::SourceParser))
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,49 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module SimpleCov
5
+ module Sorbet
6
+ # Read-only analysis pass collecting the line ranges of +T.type_alias+ blocks. Their bodies are
7
+ # runtime-unreachable by design: sorbet-runtime resolves aliases lazily and collection checks are shallow, so a
8
+ # multi-line alias block never executes and reads as a permanent coverage miss. Detection is purely syntactic —
9
+ # this gem never loads Sorbet's type system.
10
+ class TypeAliasRanges < ASTTransform::AbstractAnalysis
11
+ extend T::Sig
12
+
13
+ # Structural patterns for the alias send (node equality ignores source locations): +T.type_alias+ and its
14
+ # explicit top-level form +::T.type_alias+.
15
+ TYPE_ALIAS_SENDS = T.let(
16
+ [
17
+ s(:send, s(:const, nil, :T), :type_alias),
18
+ s(:send, s(:const, s(:cbase), :T), :type_alias),
19
+ ].freeze,
20
+ T::Array[Parser::AST::Node],
21
+ )
22
+
23
+ # One line range per alias block, in source order.
24
+ sig { returns(T::Array[T::Range[Integer]]) }
25
+ attr_reader :ranges
26
+
27
+ sig { void }
28
+ def initialize
29
+ @ranges = T.let([], T::Array[T::Range[Integer]])
30
+ super
31
+ end
32
+
33
+ # Collects the block's full expression range when the block's send is a +T.type_alias+.
34
+ #
35
+ # @param node [Parser::AST::Node] The block node being visited.
36
+ #
37
+ # @return [Parser::AST::Node] The rebuilt node (discarded by AbstractAnalysis#run).
38
+ sig { params(node: Parser::AST::Node).returns(Parser::AST::Node) }
39
+ def on_block(node)
40
+ if TYPE_ALIAS_SENDS.include?(node.children.first)
41
+ expression = node.loc.expression
42
+ @ranges << (expression.first_line..expression.last_line)
43
+ end
44
+
45
+ super
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,8 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module SimpleCov
5
+ module Sorbet
6
+ VERSION = "0.1.0"
7
+ end
8
+ end
@@ -0,0 +1,21 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "simplecov"
5
+ require "sorbet-runtime"
6
+ require "ast_transform"
7
+ require "ast_transform/abstract_analysis"
8
+
9
+ require "simplecov/sorbet/version"
10
+ require "simplecov/sorbet/type_alias_ranges"
11
+ require "simplecov/sorbet/directive_extension"
12
+
13
+ module SimpleCov
14
+ # SimpleCov extension for Sorbet codebases: skips constructs Sorbet makes runtime-unreachable by design, starting
15
+ # with multi-line +T.type_alias+ blocks. Requiring this file installs the extension; Module#prepend is idempotent,
16
+ # so requiring it more than once is harmless.
17
+ module Sorbet
18
+ end
19
+ end
20
+
21
+ SimpleCov::Directive.singleton_class.prepend(SimpleCov::Sorbet::DirectiveExtension)
@@ -0,0 +1,6 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # Entry point matching the gem name, so both `require "simplecov-sorbet"` and the conventional dashed-name path
5
+ # `require "simplecov/sorbet"` work.
6
+ require "simplecov/sorbet"
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("../lib", __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "simplecov/sorbet/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "simplecov-sorbet"
9
+ spec.version = SimpleCov::Sorbet::VERSION
10
+ spec.authors = ["Jean-Philippe Duchesne"]
11
+ spec.email = ["jpduchesne89@gmail.com"]
12
+
13
+ spec.summary = "SimpleCov extension that skips runtime-unreachable Sorbet constructs (T.type_alias blocks)."
14
+ spec.description = "Multi-line T.type_alias blocks never execute (sorbet-runtime resolves aliases lazily), " \
15
+ "so SimpleCov reports them as uncovered. This extension detects them syntactically and feeds their line " \
16
+ "ranges into SimpleCov's skip machinery."
17
+ spec.homepage = "https://github.com/d3mlabs/simplecov-sorbet"
18
+ spec.license = "MIT"
19
+ spec.files = %x(git ls-files -z).split("\x0").reject do |f|
20
+ f.match(%r{^(test|spec|features|sorbet)/})
21
+ end
22
+ spec.require_paths = ["lib"]
23
+ spec.required_ruby_version = ">= 3.3"
24
+
25
+ # Development dependencies live in the Gemfile (Gemspec/DevelopmentDependencies).
26
+
27
+ # Runtime dependencies
28
+ # simplecov ~> 1.0: Directive.disabled_ranges is the seam this gem extends.
29
+ spec.add_runtime_dependency "simplecov", "~> 1.0"
30
+ # ast_transform ~> 3.1: AbstractAnalysis + ASTTransform.parse (Prism-backed).
31
+ spec.add_runtime_dependency "ast_transform", "~> 3.1"
32
+ # sorbet-runtime backs the inline sigs; the audience is Sorbet codebases, so it is already in their bundle.
33
+ spec.add_runtime_dependency "sorbet-runtime"
34
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simplecov-sorbet
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jean-Philippe Duchesne
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-07-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: simplecov
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: ast_transform
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sorbet-runtime
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Multi-line T.type_alias blocks never execute (sorbet-runtime resolves
56
+ aliases lazily), so SimpleCov reports them as uncovered. This extension detects
57
+ them syntactically and feeds their line ranges into SimpleCov's skip machinery.
58
+ email:
59
+ - jpduchesne89@gmail.com
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - ".github/workflows/ci.yml"
65
+ - ".github/workflows/release.yml"
66
+ - ".gitignore"
67
+ - ".rubocop.yml"
68
+ - ".ruby-version"
69
+ - CHANGELOG.md
70
+ - Gemfile
71
+ - Gemfile.lock
72
+ - LICENSE.txt
73
+ - README.md
74
+ - Rakefile
75
+ - bin/tapioca
76
+ - dependencies.rb
77
+ - dev.yml
78
+ - lib/simplecov-sorbet.rb
79
+ - lib/simplecov/sorbet.rb
80
+ - lib/simplecov/sorbet/directive_extension.rb
81
+ - lib/simplecov/sorbet/type_alias_ranges.rb
82
+ - lib/simplecov/sorbet/version.rb
83
+ - simplecov-sorbet.gemspec
84
+ homepage: https://github.com/d3mlabs/simplecov-sorbet
85
+ licenses:
86
+ - MIT
87
+ metadata: {}
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '3.3'
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubygems_version: 3.5.22
104
+ signing_key:
105
+ specification_version: 4
106
+ summary: SimpleCov extension that skips runtime-unreachable Sorbet constructs (T.type_alias
107
+ blocks).
108
+ test_files: []