simplecov-sorbet 0.1.0 → 0.2.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 +4 -4
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +8 -0
- data/Gemfile.lock +2 -2
- data/README.md +28 -12
- data/bin/release.rb +225 -0
- data/dev.yml +3 -0
- data/lib/simplecov/sorbet/directive_extension.rb +13 -13
- data/lib/simplecov/sorbet/ignored_ranges.rb +86 -0
- data/lib/simplecov/sorbet/version.rb +1 -1
- data/lib/simplecov/sorbet.rb +4 -4
- data/simplecov-sorbet.gemspec +4 -4
- metadata +9 -7
- data/lib/simplecov/sorbet/type_alias_ranges.rb +0 -49
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 73744e2677661e69504801208d5e5e99224532de82e6ba71c62f8cd10df75aed
|
|
4
|
+
data.tar.gz: ca8f0945384722715fb35b473f64d80083b4a8d00e4240c4e5796f7bedc33384
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b6695fce11be79e42e782781912385748a65a374427e5484ba8c7250ad97589d10a3a18a89c494bd9858cb15cc9b5f569702c4e02af0266001a338d9420f6d06
|
|
7
|
+
data.tar.gz: 1e2a853a3fb0da0a5a693a5f8ae88d76705e12d498504fbbd2a83465fa2c2f69df535fcdf03135d549be25d7603011e5bccdcb83d28b5b80c2507764f0faded4
|
data/.rubocop.yml
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
inherit_gem:
|
|
2
2
|
rubocop-shopify: rubocop.yml
|
|
3
3
|
|
|
4
|
+
inherit_mode:
|
|
5
|
+
merge:
|
|
6
|
+
- Exclude
|
|
7
|
+
|
|
4
8
|
plugins:
|
|
5
9
|
- rubocop-sorbet
|
|
6
10
|
|
|
7
11
|
AllCops:
|
|
8
12
|
SuggestExtensions: false
|
|
13
|
+
Exclude:
|
|
14
|
+
# Polyglot sh/ruby bootstrap scripts (#!/bin/sh preamble re-execing via
|
|
15
|
+
# `ruby -x`); not parseable as plain Ruby.
|
|
16
|
+
- bin/**/*
|
|
9
17
|
|
|
10
18
|
# Tests are written in the RSpock dialect: bare comparisons in Then/Expect
|
|
11
19
|
# blocks and block-name constants (Given/When/Then/Where/Cleanup) are
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.2.0] - 2026-07-29
|
|
8
|
+
### Added
|
|
9
|
+
- `sig` block skipping: all multi-line `sig` blocks (bare or with a receiver, e.g. `T::Sig::WithoutRuntime.sig`) are excluded from coverage. Sigs are type metadata whose correctness `srb tc` owns; an untested method still reports its body as the miss.
|
|
10
|
+
- `T.absurd` send skipping: exhaustiveness checks are unreachable by definition when the code is correct, so their lines no longer read as permanent misses.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- `TypeAliasRanges` is now `IgnoredRanges`, one analysis pass collecting all three construct kinds.
|
|
14
|
+
|
|
7
15
|
## [0.1.0] - 2026-07-29
|
|
8
16
|
### Initial release
|
|
9
17
|
- `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.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
simplecov-sorbet (0.
|
|
4
|
+
simplecov-sorbet (0.2.0)
|
|
5
5
|
ast_transform (~> 3.1)
|
|
6
6
|
simplecov (~> 1.0)
|
|
7
7
|
sorbet-runtime
|
|
@@ -179,7 +179,7 @@ CHECKSUMS
|
|
|
179
179
|
rubydex (0.3.0-x86_64-darwin) sha256=59d20c98b0bf2226f5db5bee14cf57075aa93aa0c3fa0176f496a217691ff7b6
|
|
180
180
|
rubydex (0.3.0-x86_64-linux) sha256=dfe4026591f226b4a1b53f82f86dc14fad2e7c221ee8a5be99a0a46bf2d58b04
|
|
181
181
|
simplecov (1.0.3) sha256=38ef0514f16ae7562f0d0f4df02610071115103d301b6de7dacbcc000082e39b
|
|
182
|
-
simplecov-sorbet (0.
|
|
182
|
+
simplecov-sorbet (0.2.0)
|
|
183
183
|
sorbet (0.6.13367) sha256=099d9f5f420e09606deb84c9110f352e1caa970eabc01978c55dc6eb089858ce
|
|
184
184
|
sorbet-runtime (0.6.13367) sha256=4891cafce050f32f0e03f593a63cfa2ed535e541633b75a035ed36fb1b067d42
|
|
185
185
|
sorbet-static (0.6.13367-aarch64-linux) sha256=46416a8ed47e5c12d129a6f6418a98d2bb56d8ebeaea6d6a6a47306b4307acc5
|
data/README.md
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
[](https://github.com/d3mlabs/simplecov-sorbet/actions/workflows/ci.yml)
|
|
2
|
+
[](https://codecov.io/gh/d3mlabs/simplecov-sorbet)
|
|
3
|
+
|
|
1
4
|
# SimpleCov::Sorbet
|
|
2
5
|
|
|
3
|
-
A [SimpleCov](https://github.com/simplecov-ruby/simplecov) extension for [Sorbet](https://sorbet.org) codebases: it skips
|
|
6
|
+
A [SimpleCov](https://github.com/simplecov-ruby/simplecov) extension for [Sorbet](https://sorbet.org) codebases: it skips type-level Sorbet constructs that coverage should not measure, so they stop reading as coverage misses.
|
|
7
|
+
|
|
8
|
+
Three constructs are skipped:
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
- **Multi-line `T.type_alias` blocks.** sorbet-runtime resolves aliases lazily and collection checks are shallow, so the block body of
|
|
6
11
|
|
|
7
12
|
```ruby
|
|
8
13
|
ResolvedSegment = T.type_alias do
|
|
@@ -14,7 +19,13 @@ ResolvedSegment = T.type_alias do
|
|
|
14
19
|
end
|
|
15
20
|
```
|
|
16
21
|
|
|
17
|
-
never executes — and SimpleCov (and any patch-coverage gate built on it, like Codecov's) reports those lines as uncovered.
|
|
22
|
+
never executes — and SimpleCov (and any patch-coverage gate built on it, like Codecov's) reports those lines as uncovered.
|
|
23
|
+
|
|
24
|
+
- **`sig` blocks** (bare or with a receiver such as `T::Sig::WithoutRuntime.sig`). Sigs are type metadata whose correctness `srb tc` owns; coverage should measure behavior. Skipping them sharpens the signal: an untested method reports its *body* as the miss, without sig-line noise — and `T::Sig::WithoutRuntime` sigs, which never evaluate at runtime, stop being permanent false positives. Detection matches any `sig` call with a multi-line block; a non-Sorbet DSL also named `sig` would be over-matched, an accepted risk in a Sorbet codebase.
|
|
25
|
+
|
|
26
|
+
- **`T.absurd` sends.** Exhaustiveness checks are unreachable by definition when the code is correct — Sorbet statically proves the `else` branch can't be taken — so in healthy code the line is a permanent miss.
|
|
27
|
+
|
|
28
|
+
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](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
29
|
|
|
19
30
|
## Installation
|
|
20
31
|
|
|
@@ -35,7 +46,13 @@ SimpleCov.start
|
|
|
35
46
|
|
|
36
47
|
## How it works
|
|
37
48
|
|
|
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`
|
|
49
|
+
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.
|
|
50
|
+
|
|
51
|
+
### What is deliberately not skipped
|
|
52
|
+
|
|
53
|
+
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.
|
|
54
|
+
|
|
55
|
+
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.
|
|
39
56
|
|
|
40
57
|
## Requirements
|
|
41
58
|
|
|
@@ -49,15 +66,14 @@ This repo is managed with d3mlabs' `dev` tool (`dev up`, `dev test`, `dev style`
|
|
|
49
66
|
|
|
50
67
|
## Releasing a New Version
|
|
51
68
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
```
|
|
69
|
+
From a clean checkout of `main`:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
dev release # auto-increments the patch version (0.1.0 → 0.1.1)
|
|
73
|
+
dev release 0.2.0 # explicit version
|
|
74
|
+
```
|
|
59
75
|
|
|
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/).
|
|
76
|
+
The script ([bin/release.rb](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](.github/workflows/release.yml) — which 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/) — until the publish succeeds.
|
|
61
77
|
|
|
62
78
|
## License
|
|
63
79
|
|
data/bin/release.rb
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Use PATH ruby (rbenv) if >= 3.1, fall back to Homebrew Ruby for bootstrapping.
|
|
3
|
+
if command -v ruby >/dev/null 2>&1; then
|
|
4
|
+
if ruby -e 'exit(Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.1") ? 0 : 1)' 2>/dev/null; then
|
|
5
|
+
exec ruby -x "$0" "$@"
|
|
6
|
+
fi
|
|
7
|
+
fi
|
|
8
|
+
if command -v brew >/dev/null 2>&1; then
|
|
9
|
+
brew_ruby="$(brew --prefix ruby 2>/dev/null)/bin/ruby"
|
|
10
|
+
if [ -x "$brew_ruby" ]; then
|
|
11
|
+
exec "$brew_ruby" -x "$0" "$@"
|
|
12
|
+
fi
|
|
13
|
+
fi
|
|
14
|
+
echo "release: no ruby found. Install rbenv and a Ruby version, or brew install ruby." >&2
|
|
15
|
+
exit 1
|
|
16
|
+
|
|
17
|
+
#!ruby
|
|
18
|
+
# frozen_string_literal: true
|
|
19
|
+
|
|
20
|
+
# Release a new version of simplecov-sorbet: bump version.rb + Gemfile.lock,
|
|
21
|
+
# commit, tag, push, create GitHub release, then watch the tag-triggered
|
|
22
|
+
# workflow publish the gem to rubygems.org via trusted publishing.
|
|
23
|
+
#
|
|
24
|
+
# Usage:
|
|
25
|
+
# ./bin/release.rb # auto-increments patch (0.1.0 → 0.1.1)
|
|
26
|
+
# ./bin/release.rb 0.2.0 # explicit version
|
|
27
|
+
# ./bin/release.rb "Release notes" # auto-increment with custom notes
|
|
28
|
+
# ./bin/release.rb --yes # skip the confirmation (non-interactive runs)
|
|
29
|
+
|
|
30
|
+
require "pathname"
|
|
31
|
+
require "open3"
|
|
32
|
+
|
|
33
|
+
GEM_NAME = "simplecov-sorbet"
|
|
34
|
+
GEM_ROOT = Pathname.new(File.expand_path("..", __dir__))
|
|
35
|
+
VERSION_FILE = GEM_ROOT.join("lib", "simplecov", "sorbet", "version.rb")
|
|
36
|
+
GEMFILE_LOCK = GEM_ROOT.join("Gemfile.lock")
|
|
37
|
+
|
|
38
|
+
def main
|
|
39
|
+
Dir.chdir(GEM_ROOT)
|
|
40
|
+
ensure_clean_tree!
|
|
41
|
+
ensure_on_main!
|
|
42
|
+
|
|
43
|
+
# --yes skips the interactive confirmation, which would otherwise hang a
|
|
44
|
+
# piped or backgrounded run waiting for input it can never receive.
|
|
45
|
+
assume_yes = !ARGV.delete("--yes").nil?
|
|
46
|
+
|
|
47
|
+
current = current_version
|
|
48
|
+
new_version, notes = parse_args(current)
|
|
49
|
+
commits = commits_since_last_tag
|
|
50
|
+
|
|
51
|
+
print_summary(current, new_version, notes, commits)
|
|
52
|
+
abort "Aborted." unless assume_yes || confirm?("Proceed?")
|
|
53
|
+
puts
|
|
54
|
+
|
|
55
|
+
step("Bumping version #{current} → #{new_version}") do
|
|
56
|
+
bump_version(current, new_version)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
step("Committing and tagging v#{new_version}") do
|
|
60
|
+
commit_and_tag(new_version, notes)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
step("Pushing main + tag v#{new_version}") do
|
|
64
|
+
push(new_version)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
step("Creating GitHub release v#{new_version}") do
|
|
68
|
+
create_release(new_version, notes)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The tag push triggers .github/workflows/release.yml, which publishes to
|
|
72
|
+
# rubygems.org via trusted publishing. Watch it so a failed publish (e.g.
|
|
73
|
+
# a missing trusted-publisher configuration) can't slip by silently.
|
|
74
|
+
step("Waiting for the rubygems publish workflow") do
|
|
75
|
+
watch_publish_workflow(new_version)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
puts "v#{new_version} released: https://rubygems.org/gems/#{GEM_NAME}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def parse_args(current)
|
|
82
|
+
case ARGV.length
|
|
83
|
+
when 0
|
|
84
|
+
[auto_increment(current), default_notes]
|
|
85
|
+
when 1
|
|
86
|
+
arg = ARGV[0]
|
|
87
|
+
if arg.match?(/\A\d+\.\d+\.\d+\z/)
|
|
88
|
+
[arg, default_notes]
|
|
89
|
+
else
|
|
90
|
+
[auto_increment(current), arg]
|
|
91
|
+
end
|
|
92
|
+
when 2
|
|
93
|
+
[ARGV[0], ARGV[1]]
|
|
94
|
+
else
|
|
95
|
+
abort "Usage: #{$PROGRAM_NAME} [version] [notes]"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def current_version
|
|
100
|
+
match = VERSION_FILE.read.match(/VERSION = "(\d+\.\d+\.\d+)"/)
|
|
101
|
+
abort "Could not find VERSION in #{VERSION_FILE}" unless match
|
|
102
|
+
|
|
103
|
+
match[1]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def auto_increment(version)
|
|
107
|
+
parts = version.split(".").map(&:to_i)
|
|
108
|
+
parts[-1] += 1
|
|
109
|
+
parts.join(".")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def default_notes
|
|
113
|
+
log = `git log --oneline #{latest_tag}..HEAD`.strip
|
|
114
|
+
return log unless log.empty?
|
|
115
|
+
|
|
116
|
+
"Maintenance release."
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def latest_tag
|
|
120
|
+
`git describe --tags --abbrev=0 2>/dev/null`.strip
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def ensure_clean_tree!
|
|
124
|
+
status = `git status --porcelain`.strip
|
|
125
|
+
return if status.empty?
|
|
126
|
+
|
|
127
|
+
abort "Working tree is not clean. Commit or stash changes first.\n#{status}"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def ensure_on_main!
|
|
131
|
+
branch = `git branch --show-current`.strip
|
|
132
|
+
return if branch == "main"
|
|
133
|
+
|
|
134
|
+
abort "Must be on main branch (currently on #{branch})."
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def commits_since_last_tag
|
|
138
|
+
tag = latest_tag
|
|
139
|
+
return [] if tag.empty?
|
|
140
|
+
|
|
141
|
+
`git log --oneline #{tag}..HEAD`.strip.lines.map(&:strip)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def print_summary(current, new_version, notes, commits)
|
|
145
|
+
puts "Release: #{current} → #{new_version}"
|
|
146
|
+
puts "Notes: #{notes}"
|
|
147
|
+
puts
|
|
148
|
+
if commits.empty?
|
|
149
|
+
puts "Commits: (none since #{latest_tag})"
|
|
150
|
+
else
|
|
151
|
+
puts "Commits since #{latest_tag}:"
|
|
152
|
+
commits.each { |c| puts " #{c}" }
|
|
153
|
+
end
|
|
154
|
+
puts
|
|
155
|
+
puts "Steps:"
|
|
156
|
+
puts " 1. Bump version.rb + Gemfile.lock"
|
|
157
|
+
puts " 2. Commit + tag v#{new_version}"
|
|
158
|
+
puts " 3. Push main + tag to origin"
|
|
159
|
+
puts " 4. Create GitHub release"
|
|
160
|
+
puts " 5. Watch the trusted-publishing workflow push to rubygems.org"
|
|
161
|
+
puts
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def confirm?(question)
|
|
165
|
+
print "#{question} (y/N) "
|
|
166
|
+
($stdin.gets || "").strip.downcase == "y"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# One release step, aborting the whole release when it fails so a broken push
|
|
170
|
+
# can't cascade into later steps publishing state that never reached GitHub.
|
|
171
|
+
def step(title)
|
|
172
|
+
puts "--- #{title}"
|
|
173
|
+
yield
|
|
174
|
+
rescue StandardError => e
|
|
175
|
+
abort "Release aborted: '#{title}' failed: #{e.message}\nFix the issue, then finish the remaining steps manually."
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def run!(*cmd)
|
|
179
|
+
out, err, status = Open3.capture3(*cmd)
|
|
180
|
+
raise "#{cmd.join(" ")} failed: #{err}" unless status.success?
|
|
181
|
+
|
|
182
|
+
out
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def bump_version(current, new_version)
|
|
186
|
+
VERSION_FILE.write(VERSION_FILE.read.sub(%(VERSION = "#{current}"), %(VERSION = "#{new_version}")))
|
|
187
|
+
|
|
188
|
+
# The gem is a PATH source in its own Gemfile.lock, pinned in both the PATH
|
|
189
|
+
# specs block and the DEPENDENCIES section; gsub updates the pair.
|
|
190
|
+
lock = GEMFILE_LOCK.read
|
|
191
|
+
GEMFILE_LOCK.write(lock.gsub("#{GEM_NAME} (#{current})", "#{GEM_NAME} (#{new_version})"))
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def commit_and_tag(version, notes)
|
|
195
|
+
run!("git", "add", VERSION_FILE.to_s, GEMFILE_LOCK.to_s)
|
|
196
|
+
run!("git", "commit", "-m", "Bump version to #{version}\n\n#{notes}")
|
|
197
|
+
run!("git", "tag", "v#{version}")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def push(version)
|
|
201
|
+
run!("git", "push", "origin", "main")
|
|
202
|
+
run!("git", "push", "origin", "v#{version}")
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def create_release(version, notes)
|
|
206
|
+
run!("gh", "release", "create", "v#{version}",
|
|
207
|
+
"--title", "v#{version}", "--notes", notes)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Find the workflow run the tag push triggered (retrying while GitHub
|
|
211
|
+
# registers it) and block until it finishes, failing loudly if it failed.
|
|
212
|
+
def watch_publish_workflow(version)
|
|
213
|
+
run_id = nil
|
|
214
|
+
10.times do
|
|
215
|
+
run_id = `gh run list --workflow=release.yml --branch v#{version} --limit 1 --json databaseId --jq '.[0].databaseId'`.strip
|
|
216
|
+
break unless run_id.empty?
|
|
217
|
+
|
|
218
|
+
sleep 3
|
|
219
|
+
end
|
|
220
|
+
raise "could not find the release.yml run for v#{version}" if run_id.empty?
|
|
221
|
+
|
|
222
|
+
run!("gh", "run", "watch", run_id, "--exit-status")
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
main
|
data/dev.yml
CHANGED
|
@@ -5,35 +5,35 @@ module SimpleCov
|
|
|
5
5
|
module Sorbet
|
|
6
6
|
# Prepended onto SimpleCov::Directive's singleton class. Directive.disabled_ranges is the one choke point both
|
|
7
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.
|
|
9
|
-
# categories:
|
|
8
|
+
# extending its result covers every path SimpleCov classifies lines through. Ignored ranges join all three
|
|
9
|
+
# categories: type-level constructs are not behavior, so any line, branch, or method inside them is skippable.
|
|
10
10
|
module DirectiveExtension
|
|
11
11
|
extend T::Sig
|
|
12
12
|
|
|
13
13
|
sig { params(src_lines: T::Array[String]).returns(T::Hash[Symbol, T::Array[T::Range[Integer]]]) }
|
|
14
14
|
def disabled_ranges(src_lines)
|
|
15
15
|
ranges = T.let(super, T::Hash[Symbol, T::Array[T::Range[Integer]]])
|
|
16
|
-
|
|
17
|
-
return ranges if
|
|
16
|
+
sorbet_ranges = ignored_ranges(src_lines.join)
|
|
17
|
+
return ranges if sorbet_ranges.empty?
|
|
18
18
|
|
|
19
|
-
ranges.each_value { |category_ranges| category_ranges.concat(
|
|
19
|
+
ranges.each_value { |category_ranges| category_ranges.concat(sorbet_ranges) }
|
|
20
20
|
ranges
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
private
|
|
24
24
|
|
|
25
|
-
# Scans +source+ for
|
|
26
|
-
# never take down a suite's reporting, and a file Ruby executed but this parser
|
|
27
|
-
# could trust anyway. The rescue is deliberately wide — Prism parses most broken
|
|
28
|
-
# escapes is not Parser::SyntaxError but arbitrary errors from the whitequark
|
|
29
|
-
# error-recovered trees (e.g. NoMethodError in join_exprs).
|
|
25
|
+
# Scans +source+ for type-level Sorbet constructs (see IgnoredRanges). Unparseable source yields no ranges:
|
|
26
|
+
# coverage annotation must never take down a suite's reporting, and a file Ruby executed but this parser
|
|
27
|
+
# rejects has no constructs we could trust anyway. The rescue is deliberately wide — Prism parses most broken
|
|
28
|
+
# source tolerantly, and what escapes is not Parser::SyntaxError but arbitrary errors from the whitequark
|
|
29
|
+
# builder choking on error-recovered trees (e.g. NoMethodError in join_exprs).
|
|
30
30
|
#
|
|
31
31
|
# @param source [String] The Ruby source to scan.
|
|
32
32
|
#
|
|
33
|
-
# @return [Array<Range<Integer>>] One line range per
|
|
33
|
+
# @return [Array<Range<Integer>>] One line range per ignored construct, in source order.
|
|
34
34
|
sig { params(source: String).returns(T::Array[T::Range[Integer]]) }
|
|
35
|
-
def
|
|
36
|
-
|
|
35
|
+
def ignored_ranges(source)
|
|
36
|
+
IgnoredRanges.new.run(source_parser.parse(source)).ranges
|
|
37
37
|
rescue StandardError
|
|
38
38
|
[]
|
|
39
39
|
end
|
|
@@ -0,0 +1,86 @@
|
|
|
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 type-level Sorbet constructs coverage should ignore.
|
|
7
|
+
# Detection is purely syntactic — this gem never loads Sorbet's type system. Three constructs are collected:
|
|
8
|
+
#
|
|
9
|
+
# - +T.type_alias+ blocks: sorbet-runtime resolves aliases lazily and collection checks are shallow, so a
|
|
10
|
+
# multi-line alias body never executes and reads as a permanent coverage miss.
|
|
11
|
+
# - +sig+ blocks (bare or with a receiver, e.g. +T::Sig::WithoutRuntime.sig+): sigs are type metadata whose
|
|
12
|
+
# correctness +srb tc+ owns; coverage should measure behavior. Only multi-line blocks are collected — a
|
|
13
|
+
# single-line +sig { ... }+ occupies the send's own line, which executes at load.
|
|
14
|
+
# - +T.absurd+ sends: unreachable by definition when exhaustiveness holds, so in correct code the line is a
|
|
15
|
+
# permanent coverage miss.
|
|
16
|
+
class IgnoredRanges < ASTTransform::AbstractAnalysis
|
|
17
|
+
extend T::Sig
|
|
18
|
+
|
|
19
|
+
# Structural patterns for the +T+ receiver (node equality ignores source locations): +T+ and its explicit
|
|
20
|
+
# top-level form +::T+.
|
|
21
|
+
T_RECEIVERS = T.let(
|
|
22
|
+
[
|
|
23
|
+
s(:const, nil, :T),
|
|
24
|
+
s(:const, s(:cbase), :T),
|
|
25
|
+
].freeze,
|
|
26
|
+
T::Array[Parser::AST::Node],
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# One line range per ignored construct, in source order.
|
|
30
|
+
sig { returns(T::Array[T::Range[Integer]]) }
|
|
31
|
+
attr_reader :ranges
|
|
32
|
+
|
|
33
|
+
sig { void }
|
|
34
|
+
def initialize
|
|
35
|
+
@ranges = T.let([], T::Array[T::Range[Integer]])
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Collects the send's full expression range when it is a +T.absurd+ call.
|
|
40
|
+
#
|
|
41
|
+
# @param node [Parser::AST::Node] The send node being visited.
|
|
42
|
+
#
|
|
43
|
+
# @return [Parser::AST::Node] The rebuilt node (discarded by AbstractAnalysis#run).
|
|
44
|
+
sig { params(node: Parser::AST::Node).returns(Parser::AST::Node) }
|
|
45
|
+
def on_send(node)
|
|
46
|
+
receiver, method_name = node.children
|
|
47
|
+
@ranges << line_range(node) if method_name == :absurd && T_RECEIVERS.include?(receiver)
|
|
48
|
+
|
|
49
|
+
super
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Collects the block's full expression range when the block's send is a +T.type_alias+ or a multi-line +sig+.
|
|
53
|
+
#
|
|
54
|
+
# @param node [Parser::AST::Node] The block node being visited.
|
|
55
|
+
#
|
|
56
|
+
# @return [Parser::AST::Node] The rebuilt node (discarded by AbstractAnalysis#run).
|
|
57
|
+
sig { params(node: Parser::AST::Node).returns(Parser::AST::Node) }
|
|
58
|
+
def on_block(node)
|
|
59
|
+
receiver, method_name = node.children.first.children
|
|
60
|
+
range = line_range(node)
|
|
61
|
+
|
|
62
|
+
case method_name
|
|
63
|
+
when :type_alias
|
|
64
|
+
@ranges << range if T_RECEIVERS.include?(receiver)
|
|
65
|
+
when :sig
|
|
66
|
+
# Any receiver qualifies: bare sig blocks and forms like T::Sig::WithoutRuntime.sig are all sigs. A
|
|
67
|
+
# non-Sorbet DSL also named `sig` would be over-matched, an accepted risk documented in the README.
|
|
68
|
+
@ranges << range if range.first < range.last
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
super
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# @param node [Parser::AST::Node] The node whose source span to convert.
|
|
77
|
+
#
|
|
78
|
+
# @return [Range<Integer>] The node's 1-indexed first-to-last line range.
|
|
79
|
+
sig { params(node: Parser::AST::Node).returns(T::Range[Integer]) }
|
|
80
|
+
def line_range(node)
|
|
81
|
+
expression = node.loc.expression
|
|
82
|
+
expression.first_line..expression.last_line
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
data/lib/simplecov/sorbet.rb
CHANGED
|
@@ -7,13 +7,13 @@ require "ast_transform"
|
|
|
7
7
|
require "ast_transform/abstract_analysis"
|
|
8
8
|
|
|
9
9
|
require "simplecov/sorbet/version"
|
|
10
|
-
require "simplecov/sorbet/
|
|
10
|
+
require "simplecov/sorbet/ignored_ranges"
|
|
11
11
|
require "simplecov/sorbet/directive_extension"
|
|
12
12
|
|
|
13
13
|
module SimpleCov
|
|
14
|
-
# SimpleCov extension for Sorbet codebases: skips
|
|
15
|
-
#
|
|
16
|
-
# so requiring it more than once is harmless.
|
|
14
|
+
# SimpleCov extension for Sorbet codebases: skips type-level Sorbet constructs coverage should not measure —
|
|
15
|
+
# multi-line +T.type_alias+ blocks, +sig+ blocks, and +T.absurd+ sends. Requiring this file installs the
|
|
16
|
+
# extension; Module#prepend is idempotent, so requiring it more than once is harmless.
|
|
17
17
|
module Sorbet
|
|
18
18
|
end
|
|
19
19
|
end
|
data/simplecov-sorbet.gemspec
CHANGED
|
@@ -10,10 +10,10 @@ Gem::Specification.new do |spec|
|
|
|
10
10
|
spec.authors = ["Jean-Philippe Duchesne"]
|
|
11
11
|
spec.email = ["jpduchesne89@gmail.com"]
|
|
12
12
|
|
|
13
|
-
spec.summary = "SimpleCov extension that skips
|
|
14
|
-
spec.description = "
|
|
15
|
-
"
|
|
16
|
-
"ranges into SimpleCov's skip machinery."
|
|
13
|
+
spec.summary = "SimpleCov extension that skips type-level Sorbet constructs (T.type_alias, sig, T.absurd)."
|
|
14
|
+
spec.description = "Type-level Sorbet constructs read as coverage misses: multi-line T.type_alias and sig " \
|
|
15
|
+
"blocks evaluate lazily or never, and T.absurd is unreachable by definition. This extension detects them " \
|
|
16
|
+
"syntactically and feeds their line ranges into SimpleCov's skip machinery."
|
|
17
17
|
spec.homepage = "https://github.com/d3mlabs/simplecov-sorbet"
|
|
18
18
|
spec.license = "MIT"
|
|
19
19
|
spec.files = %x(git ls-files -z).split("\x0").reject do |f|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: simplecov-sorbet
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jean-Philippe Duchesne
|
|
@@ -52,9 +52,10 @@ dependencies:
|
|
|
52
52
|
- - ">="
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
54
|
version: '0'
|
|
55
|
-
description:
|
|
56
|
-
|
|
57
|
-
them syntactically and feeds their line ranges into SimpleCov's
|
|
55
|
+
description: 'Type-level Sorbet constructs read as coverage misses: multi-line T.type_alias
|
|
56
|
+
and sig blocks evaluate lazily or never, and T.absurd is unreachable by definition.
|
|
57
|
+
This extension detects them syntactically and feeds their line ranges into SimpleCov''s
|
|
58
|
+
skip machinery.'
|
|
58
59
|
email:
|
|
59
60
|
- jpduchesne89@gmail.com
|
|
60
61
|
executables: []
|
|
@@ -72,13 +73,14 @@ files:
|
|
|
72
73
|
- LICENSE.txt
|
|
73
74
|
- README.md
|
|
74
75
|
- Rakefile
|
|
76
|
+
- bin/release.rb
|
|
75
77
|
- bin/tapioca
|
|
76
78
|
- dependencies.rb
|
|
77
79
|
- dev.yml
|
|
78
80
|
- lib/simplecov-sorbet.rb
|
|
79
81
|
- lib/simplecov/sorbet.rb
|
|
80
82
|
- lib/simplecov/sorbet/directive_extension.rb
|
|
81
|
-
- lib/simplecov/sorbet/
|
|
83
|
+
- lib/simplecov/sorbet/ignored_ranges.rb
|
|
82
84
|
- lib/simplecov/sorbet/version.rb
|
|
83
85
|
- simplecov-sorbet.gemspec
|
|
84
86
|
homepage: https://github.com/d3mlabs/simplecov-sorbet
|
|
@@ -103,6 +105,6 @@ requirements: []
|
|
|
103
105
|
rubygems_version: 3.5.22
|
|
104
106
|
signing_key:
|
|
105
107
|
specification_version: 4
|
|
106
|
-
summary: SimpleCov extension that skips
|
|
107
|
-
|
|
108
|
+
summary: SimpleCov extension that skips type-level Sorbet constructs (T.type_alias,
|
|
109
|
+
sig, T.absurd).
|
|
108
110
|
test_files: []
|
|
@@ -1,49 +0,0 @@
|
|
|
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
|