rigortype 0.3.1 → 0.3.2

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.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -10
  3. data/data/core_overlay/pathname.rbs +5 -0
  4. data/data/core_overlay/resolv.rbs +31 -0
  5. data/data/core_overlay/string_scanner.rbs +5 -0
  6. data/data/gem_overlay/activesupport/core_ext.rbs +10 -6
  7. data/data/vendored_gem_sigs/bundler/bundler.rbs +39 -27
  8. data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +6 -0
  9. data/data/vendored_gem_sigs/prism/prism_supplement.rbs +7 -0
  10. data/data/vendored_gem_sigs/racc/racc.rbs +67 -0
  11. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +20 -9
  12. data/docs/manual/02-cli-reference.md +71 -11
  13. data/docs/manual/03-configuration.md +16 -1
  14. data/docs/manual/12-caching.md +17 -5
  15. data/docs/manual/15-type-protection-coverage.md +122 -1
  16. data/docs/manual/plugins/rigor-rbs-inline.md +22 -0
  17. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -0
  18. data/lib/rigor/analysis/check_rules.rb +45 -6
  19. data/lib/rigor/analysis/incremental_session.rb +156 -11
  20. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +14 -0
  21. data/lib/rigor/analysis/run_cache_key.rb +17 -1
  22. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +201 -0
  23. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +28 -10
  24. data/lib/rigor/analysis/runner/pool_coordinator.rb +7 -10
  25. data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -1
  26. data/lib/rigor/analysis/runner.rb +78 -7
  27. data/lib/rigor/bleeding_edge.rb +132 -7
  28. data/lib/rigor/cache/descriptor.rb +6 -1
  29. data/lib/rigor/cache/engine_source.rb +162 -0
  30. data/lib/rigor/cache/incremental_snapshot.rb +87 -18
  31. data/lib/rigor/cli/check_command.rb +49 -28
  32. data/lib/rigor/cli/coverage_command.rb +15 -2
  33. data/lib/rigor/cli/coverage_mutation.rb +242 -11
  34. data/lib/rigor/cli/fused_protection_renderer.rb +10 -0
  35. data/lib/rigor/cli/fused_protection_report.rb +12 -3
  36. data/lib/rigor/cli/mutation_fork_scan.rb +64 -0
  37. data/lib/rigor/cli/mutation_protection_renderer.rb +12 -0
  38. data/lib/rigor/cli/mutation_protection_report.rb +26 -5
  39. data/lib/rigor/cli/protection_renderer.rb +13 -0
  40. data/lib/rigor/cli/protection_report.rb +11 -3
  41. data/lib/rigor/cli/show_bleedingedge_command.rb +17 -4
  42. data/lib/rigor/cli/sig_gen_command.rb +5 -4
  43. data/lib/rigor/configuration.rb +45 -6
  44. data/lib/rigor/environment/bundle_sig_discovery.rb +2 -1
  45. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  46. data/lib/rigor/environment/rbs_loader.rb +434 -75
  47. data/lib/rigor/environment.rb +28 -13
  48. data/lib/rigor/inference/expression_typer.rb +15 -0
  49. data/lib/rigor/inference/fork_map.rb +9 -0
  50. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +50 -2
  51. data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
  52. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +35 -1
  53. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +16 -0
  54. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +64 -5
  55. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +137 -9
  56. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +3 -2
  57. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +124 -1
  58. data/lib/rigor/inference/mutation_widening.rb +30 -25
  59. data/lib/rigor/inference/narrowing.rb +3 -1
  60. data/lib/rigor/inference/optimistic_origin.rb +58 -0
  61. data/lib/rigor/inference/protection_scanner.rb +18 -2
  62. data/lib/rigor/inference/receiver_alias.rb +57 -0
  63. data/lib/rigor/inference/scope_indexer.rb +78 -27
  64. data/lib/rigor/inference/statement_evaluator.rb +40 -2
  65. data/lib/rigor/language_server/buffer_table.rb +22 -0
  66. data/lib/rigor/language_server/diagnostic_publisher.rb +203 -4
  67. data/lib/rigor/language_server/project_context.rb +45 -0
  68. data/lib/rigor/language_server/publish_batcher.rb +82 -0
  69. data/lib/rigor/language_server/server.rb +20 -1
  70. data/lib/rigor/language_server.rb +1 -0
  71. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  72. data/lib/rigor/protection/closure_kill_oracle.rb +171 -0
  73. data/lib/rigor/protection/dependency_closure.rb +59 -0
  74. data/lib/rigor/protection/diagnostic_oracle.rb +12 -8
  75. data/lib/rigor/protection/discovery_seed.rb +116 -0
  76. data/lib/rigor/protection/kill_signature.rb +31 -0
  77. data/lib/rigor/protection/mutation_cache.rb +355 -0
  78. data/lib/rigor/protection/mutation_scanner.rb +55 -11
  79. data/lib/rigor/protection/mutator.rb +26 -4
  80. data/lib/rigor/runtime/jit.rb +63 -1
  81. data/lib/rigor/scope/discovery_index.rb +9 -0
  82. data/lib/rigor/scope.rb +52 -8
  83. data/lib/rigor/sig_gen/generator.rb +130 -31
  84. data/lib/rigor/sig_gen/layout_index.rb +6 -0
  85. data/lib/rigor/sig_gen/meta_class_shape.rb +84 -0
  86. data/lib/rigor/sig_gen/renderer.rb +20 -1
  87. data/lib/rigor/sig_gen/write_result.rb +6 -4
  88. data/lib/rigor/sig_gen/writer.rb +12 -0
  89. data/lib/rigor/sig_gen.rb +1 -0
  90. data/lib/rigor/version.rb +1 -1
  91. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +6 -6
  92. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/result_shape.rb +146 -0
  93. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +177 -12
  94. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +74 -7
  95. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +257 -0
  96. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +69 -0
  97. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +87 -11
  98. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +34 -4
  99. data/sig/rigor/inference/void_origin.rbs +6 -3
  100. data/sig/rigor/scope.rbs +8 -0
  101. metadata +17 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b23cb8236c12aeeee7e95afef03a5db32b6644769af9d70bb9514c969aac7a4
4
- data.tar.gz: acc54aa19e19a77279730981492509fc0bfec4436d7d24e7ffb7fc454c761c26
3
+ metadata.gz: 53ce9543e32229f3a80491f33886ac27cc0e1d3c0bb834536c703ab669188dd2
4
+ data.tar.gz: f9bb40e6e3a44a8bd362cee2b5f46a406d36391c5cf3053982950f4acae574df
5
5
  SHA512:
6
- metadata.gz: 107bf59c35a02a22eca658fda615288c6f607ff94338c7c5b7671c799080b77a74386317a7d6f2f6c0ba73d7890114460e910a465c8f5a7eb7ac56ceaff67b11
7
- data.tar.gz: eebc42e786b36caacc03112d93be9d5f91261ff2d3766b902e9bf61e8d26f1bb069776b94791d2d8ca43b662140c66b73bee0f7ac915c71da9a2f35a1d999eac
6
+ metadata.gz: f233af3b211b096772e29ee29eda2ef7a7bca353e02f300c0f6ed36cbfb66deb73df709486896207f9459a0fbb5203c155ed257790c7d8122562780b1a532391
7
+ data.tar.gz: 21ed0e73b967f692d6df3a65a9cd6444b5b801b545cb82cb59e4cec8d0effdcec4e8f9b97ebd7a811a0ebe8a08366bdaabbb07df1338a59a939e5d855281b2be
data/README.md CHANGED
@@ -108,7 +108,7 @@ Ready-made prompts — 日本語, 简体中文, 繁體中文, 한국어, Españo
108
108
  Português, Français, Deutsch, Italiano, Tiếng Việt, ภาษาไทย,
109
109
  Bahasa Indonesia, Polski, Українська, Русский, Română, Türkçe,
110
110
  العربية, فارسی — are in
111
- the [installation guide](https://rigor.typedduck.fail/reference/manual/01-installation/#set-up-in-your-language).
111
+ the [installation guide](https://rigor.typedduck.fail/manual/01-installation/#set-up-in-your-language).
112
112
 
113
113
  **Manual install** — Rigor is a tool, not a library: install it
114
114
  independently, **not** in your project's `Gemfile`. Install it
@@ -123,8 +123,8 @@ mise use -g ruby@4.0 gem:rigortype # or: gem install rigortype
123
123
 
124
124
  The gem is named `rigortype` (the name `rigor` was taken on RubyGems);
125
125
  the executable is `rigor`. `asdf`, dev containers, and CI templates are
126
- in the [installation guide](https://rigor.typedduck.fail/reference/manual/01-installation/)
127
- and [CI guide](https://rigor.typedduck.fail/reference/manual/11-ci/).
126
+ in the [installation guide](https://rigor.typedduck.fail/manual/01-installation/)
127
+ and [CI guide](https://rigor.typedduck.fail/manual/11-ci/).
128
128
 
129
129
  ### Daily commands
130
130
 
@@ -145,7 +145,7 @@ TeamCity).
145
145
  1. **Types are facts, not wishes.** Hand-written annotations drift the
146
146
  moment they are written. Rigor infers from the code itself — every
147
147
  type is derived from what your source actually produces. When you do
148
- want RBS in `sig/`, [`rigor sig-gen`](https://rigor.typedduck.fail/reference/adr/14-rbs-sig-generation/)
148
+ want RBS in `sig/`, [`rigor sig-gen`](https://rigor.typedduck.fail/adr/14-rbs-sig-generation/)
149
149
  emits it from inference results so the written form starts in sync
150
150
  with reality.
151
151
  2. **A false positive is the worst bug.** Your program works; a
@@ -161,7 +161,7 @@ TeamCity).
161
161
  literal values, integer ranges, refinements like
162
162
  `non-empty-string`, and per-position tuple / hash shapes — and
163
163
  exposes a plugin API plus a
164
- [macro / DSL expansion substrate](https://rigor.typedduck.fail/reference/adr/16-macro-expansion/)
164
+ [macro / DSL expansion substrate](https://rigor.typedduck.fail/adr/16-macro-expansion/)
165
165
  so Rails-shape DSLs become first-class type sources.
166
166
 
167
167
  ## Works with your stack
@@ -202,10 +202,10 @@ interop, so adopting Rigor is strictly additive. When you want to
202
202
  in `sig/` files tighten types while staying a no-op for ordinary RBS
203
203
  tools.
204
204
 
205
- The twelve-chapter [handbook](https://rigor.typedduck.fail/reference/handbook/)
205
+ The twelve-chapter [handbook](https://rigor.typedduck.fail/handbook/)
206
206
  walks the whole type model; the
207
- [type specification](https://rigor.typedduck.fail/reference/type-specification/)
208
- and [user manual](https://rigor.typedduck.fail/reference/manual/) are
207
+ [type specification](https://rigor.typedduck.fail/type-specification/)
208
+ and [user manual](https://rigor.typedduck.fail/manual/) are
209
209
  the reference companions.
210
210
 
211
211
  **Stuck on Rigor — or just curious how it works? Ask your agent in
@@ -231,7 +231,7 @@ rigor docs --list # list every bundled page
231
231
 
232
232
  ## Status
233
233
 
234
- Current release: **`v0.3.1`** (2026-07-29) — on the
234
+ Current release: **`v0.3.2`** (2026-08-08) — on the
235
235
  `0.3.x` line, continuing the evaluation era opened by `v0.2.0`, the first
236
236
  publicly-announced (general / evaluation) release. The line publishes
237
237
  an enumerated [compatibility surface](docs/compatibility.md) as a
@@ -241,7 +241,7 @@ Mastodon, Redmine, and GitLab FOSS, and the deliberately conservative
241
241
  rule catalogue grows only as fast as the zero-false-positive bar
242
242
  allows. Release history: [`CHANGELOG.md`](CHANGELOG.md) ·
243
243
  forward-looking commitments:
244
- [Roadmap](https://rigor.typedduck.fail/reference/roadmap/).
244
+ [Roadmap](https://github.com/rigortype/rigor/milestones).
245
245
 
246
246
  ## Contributing
247
247
 
@@ -19,6 +19,11 @@
19
19
  #
20
20
  # The overlay widens `dir` to `String | Pathname`, matching the runtime
21
21
  # delegation and the `File.expand_path` `path` alias used elsewhere in RBS.
22
+ #
23
+ # The `| ...` continuation is safe with no library loaded — unlike the
24
+ # `RbsLoader::LIBRARY_SUPPLEMENT_CORE_OVERLAYS` files, its base declaration
25
+ # lives in rbs's `core/pathname.rbs` on every supported rbs release
26
+ # (3.10 / 4.0 / 4.1), not in the `pathname` stdlib library.
22
27
  class Pathname
23
28
  def expand_path: (?String | Pathname dir) -> Pathname
24
29
  | ...
@@ -0,0 +1,31 @@
1
+ # Rigor core overlay — supplemental core signatures.
2
+ #
3
+ # --- Resolv#initialize ---
4
+ #
5
+ # `rbs` before 4.1 declares `Resolv#initialize` as only:
6
+ #
7
+ # def initialize: (?Resolv::Hosts | Resolv::DNS resolvers) -> untyped
8
+ #
9
+ # But `Resolv.new` takes an *array* of resolvers — the runtime default is
10
+ # `[Hosts.new, DNS.new]`, and `each_address` iterates the argument — so the
11
+ # documented, idiomatic call passes an Array:
12
+ #
13
+ # Resolv.new([Resolv::Hosts.new, Resolv::DNS.new(nameserver: ...)])
14
+ #
15
+ # Against the pre-4.1 signature every overload rejects that Array and
16
+ # `call.argument-type-mismatch` false-fires (measured on Mastodon's
17
+ # `app/lib/request.rb`).
18
+ #
19
+ # `rbs` 4.1.0 fixed the signature upstream (ruby/rbs#2960); the overlay
20
+ # backports that overload for the older releases the gemspec still supports
21
+ # (`>= 3.0, < 5.0`). On 4.1 the appended overload duplicates the upstream
22
+ # one, which costs nothing. Drop this file when the rbs floor reaches 4.1.
23
+ #
24
+ # Loaded ONLY when the `resolv` library resolves
25
+ # (`RbsLoader::LIBRARY_SUPPLEMENT_CORE_OVERLAYS`): the `| ...` overload
26
+ # continuation is invalid without the base declaration, and the resulting
27
+ # `InvalidOverloadMethodError` would take the whole class down.
28
+ class Resolv
29
+ def initialize: (?(Array[Resolv::Hosts | Resolv::DNS] | Hash[Symbol, untyped])? resolvers, ?use_ipv6: bool?) -> untyped
30
+ | ...
31
+ end
@@ -23,6 +23,11 @@
23
23
  # earns its keep for a consumer resolving an older `rbs` inside the
24
24
  # gemspec's `>= 3.0, < 5.0` range. It stays until that floor moves;
25
25
  # reopening with an identical overload costs nothing on 4.1.
26
+ #
27
+ # Loaded ONLY when the `strscan` library resolves
28
+ # (`RbsLoader::LIBRARY_SUPPLEMENT_CORE_OVERLAYS`): the `| ...` overload
29
+ # continuation is invalid without the base declaration, and the resulting
30
+ # `InvalidOverloadMethodError` would take the whole class down.
26
31
  class StringScanner
27
32
  def []: (Integer | String | Symbol) -> String?
28
33
  | ...
@@ -245,10 +245,10 @@ class Time
245
245
  def self.zone: () -> untyped # ActiveSupport::TimeZone | nil
246
246
  def self.zone=: (String | Symbol | untyped) -> untyped
247
247
 
248
- # `core_ext/time/conversions` — also `Time.parse` from stdlib
249
- # `time`, which is already in core RBS.
250
- def self.httpdate: (String) -> Time
251
- def self.iso8601: (String) -> Time
248
+ # `core_ext/time/conversions` — nothing to declare. `Time.parse`, `Time.httpdate` and `Time.iso8601`
249
+ # all come from stdlib `time`, which rbs types; ActiveSupport only re-opens them. Declaring them
250
+ # here raised `RBS::DuplicatedMethodDefinitionError` and collapsed the whole `Time` definition to
251
+ # `Dynamic[top]`.
252
252
 
253
253
  # `core_ext/time/calculations`
254
254
  def yesterday: () -> Time
@@ -278,7 +278,10 @@ class Time
278
278
  def midday: () -> Time
279
279
  def midnight: () -> Time
280
280
  def noon: () -> Time
281
- def utc?: () -> bool
281
+ # NOTE: `Time#utc?` is NOT declared here — it is Ruby core, already typed by rbs's `core/time.rbs`.
282
+ # Re-declaring it raised `RBS::DuplicatedMethodDefinitionError` and took the WHOLE `Time` definition
283
+ # down with it (every `Time` call degraded to `Dynamic[top]`). This overlay only ADDS what
284
+ # ActiveSupport adds.
282
285
  def acts_like_time?: () -> true
283
286
  end
284
287
 
@@ -457,7 +460,8 @@ end
457
460
 
458
461
  class DateTime
459
462
  def utc: () -> Time
460
- def to_time: () -> Time
463
+ # NOTE: `DateTime#to_time` is NOT declared here — stdlib `date` already types it, and re-declaring it
464
+ # collapsed the whole `DateTime` definition (see the `Time#utc?` note above).
461
465
  def in_time_zone: (?String | Symbol zone) -> untyped
462
466
  def yesterday: () -> DateTime
463
467
  def tomorrow: () -> DateTime
@@ -2,14 +2,12 @@
2
2
  # Rigor-side vendored RBS stub for the `bundler` gem.
3
3
  #
4
4
  # The `bundler` gem does not ship its own `sig/` and is not
5
- # covered by `ruby/gem_rbs_collection`. The shim in
6
- # `references/rbs/sig/shims/bundler.rbs` (rbs gem upstream)
7
- # carries a tiny subset (`LockfileParser`, `LazySpecification`,
8
- # `Dependency`, `Definition`, `default_lockfile`, `definition`,
9
- # `Source::Gemspec`) but is only loaded when the user opts in via
10
- # `library "bundler"` — that's rare in Rigor `.rigor.yml` setups,
11
- # so most projects analyse bundler-using code with NO `Bundler`
12
- # RBS in scope.
5
+ # covered by `ruby/gem_rbs_collection`. The rbs gem's own
6
+ # `sig/shims/bundler.rbs` carries a tiny subset (`LockfileParser`,
7
+ # `LazySpecification`, `Dependency`, `Definition`,
8
+ # `default_lockfile`, `definition`, `Source::Gemspec`) — enough to
9
+ # type rbs's own use of bundler, not enough to analyse a project
10
+ # that uses it.
13
11
  #
14
12
  # This stub covers the Bundler module's top-25 singleton methods
15
13
  # surfaced by the `references/ruby/lib` survey
@@ -21,13 +19,23 @@
21
19
  # silence `call.undefined-method` rather than infer precise
22
20
  # returns.
23
21
  #
24
- # Declarations are intentionally a SUPERSET of the
25
- # `references/rbs/sig/shims/bundler.rbs` shim. When the user
26
- # opts into `library "bundler"`, the shim's typed declarations
27
- # (`LockfileParser#specs -> Array[LazySpecification]`, etc.)
28
- # would conflict; users who want both should drop the
29
- # `library "bundler"` entry from their config so this vendored
30
- # stub wins.
22
+ # The `rbs` gem's own `sig/shims/bundler.rbs` IS always in scope:
23
+ # `rbs` sits in `Environment::DEFAULT_LIBRARIES`, so its `sig/`
24
+ # tree shims included loads on every run. This file therefore
25
+ # declares only what the shim does NOT, and never re-declares a
26
+ # shim method: a duplicate raises
27
+ # `RBS::DuplicatedMethodDefinitionError` inside
28
+ # `RBS::DefinitionBuilder` and collapses the ENTIRE class to
29
+ # `Dynamic[top]` (issue #299). Where the shim's type is weaker
30
+ # than Rigor's would have been, the shim still wins: an RBS
31
+ # overload continuation (`| ...`) would append rather than
32
+ # collide, but it raises `InvalidOverloadMethodError` when the
33
+ # base declaration is absent, which breaks every environment
34
+ # built without the `rbs` library. Those spots are marked below.
35
+ #
36
+ # Keep this file and `sig/shims/bundler.rbs` in sync on an `rbs`
37
+ # bump — `spec/rigor/environment/bundled_rbs_definition_build_spec.rb`
38
+ # fails the suite if a new collision reappears.
31
39
  #
32
40
  module Bundler
33
41
  # `Bundler.<name>` — accessors / module-level entry points.
@@ -46,7 +54,13 @@ module Bundler
46
54
  def self.require: (*untyped groups) -> untyped
47
55
  def self.load: () -> untyped
48
56
  def self.environment: () -> untyped
49
- def self.definition: (?untyped unlock, ?untyped lockfile) -> Definition
57
+ # `self.definition` declared by `sig/shims/bundler.rbs`. Note that the shim types it `() ->
58
+ # Definition` while the real `Bundler.definition(unlock = nil, lockfile = default_lockfile)` takes
59
+ # two optional arguments; an `| ...` overload continuation here would restore that arity, but a
60
+ # continuation with no base declaration raises `RBS::InvalidOverloadMethodError` — so any
61
+ # environment built WITHOUT the `rbs` library would lose the whole `Bundler` module to
62
+ # `Dynamic[top]`. The narrower arity is the cheaper of the two failure modes; the fix belongs
63
+ # upstream in the shim.
50
64
  def self.frozen_bundle?: () -> bool
51
65
  def self.locked_gems: () -> untyped
52
66
  def self.ruby_scope: () -> String
@@ -78,7 +92,7 @@ module Bundler
78
92
  def self.local_platform: () -> untyped
79
93
  def self.generic_local_platform: () -> untyped
80
94
  def self.default_gemfile: () -> Pathname
81
- def self.default_lockfile: () -> Pathname
95
+ # `self.default_lockfile` declared by `sig/shims/bundler.rbs` with the same type.
82
96
  def self.default_bundle_dir: () -> Pathname?
83
97
  def self.system_bindir: () -> String?
84
98
  def self.preferred_gemfile_name: () -> String
@@ -104,10 +118,9 @@ module Bundler
104
118
 
105
119
  def initialize: (*untyped) -> void
106
120
  def gemfiles: () -> Array[Pathname]
107
- def lockfile: () -> Pathname
121
+ # `#lockfile`, `#locked_gems`, `#dependencies` — declared by `sig/shims/bundler.rbs` with the
122
+ # same types.
108
123
  def lockfile=: (untyped) -> untyped
109
- def locked_gems: () -> LockfileParser
110
- def dependencies: () -> Array[Dependency]
111
124
  def current_dependencies: () -> Array[Dependency]
112
125
  def requested_dependencies: () -> Array[Dependency]
113
126
  def dependencies_for: (untyped groups) -> Array[Dependency]
@@ -134,8 +147,7 @@ module Bundler
134
147
  end
135
148
 
136
149
  class LockfileParser
137
- def initialize: (String body) -> void
138
- def specs: () -> Array[LazySpecification]
150
+ # `#initialize`, `#specs` declared by `sig/shims/bundler.rbs` with the same types.
139
151
  def platforms: () -> Array[untyped]
140
152
  def sources: () -> Array[untyped]
141
153
  def dependencies: () -> Array[Dependency]
@@ -144,16 +156,16 @@ module Bundler
144
156
  end
145
157
 
146
158
  class LazySpecification
147
- def name: () -> String
148
- def version: () -> Gem::Version
159
+ # `#name`, `#version`, `#dependencies` — declared by `sig/shims/bundler.rbs`. The shim types
160
+ # `#version` as `() -> String` where bundler returns a `Gem::Version`; see the note on
161
+ # `Bundler.definition` above for why that is left alone rather than corrected with an `| ...`
162
+ # overload continuation.
149
163
  def platform: () -> untyped
150
- def dependencies: () -> Array[Gem::Dependency]
151
164
  def source: () -> untyped
152
165
  end
153
166
 
154
167
  class Dependency < Gem::Dependency
155
- attr_reader autorequire: Array[String]?
156
- attr_reader source: untyped
168
+ # `#autorequire`, `#source` — declared by `sig/shims/bundler.rbs` with the same types.
157
169
  attr_reader groups: Array[Symbol]
158
170
  attr_reader platforms: Array[Symbol]
159
171
  end
@@ -29,6 +29,12 @@
29
29
  # `include CGI::Util` + `extend CGI::Util`; adding a sibling
30
30
  # `include CGI::QueryExtension` is purely additive.
31
31
  #
32
+ # Loaded ONLY when the `cgi` stdlib library resolves
33
+ # (`RbsLoader::LIBRARY_SUPPLEMENT_VENDORED_DIRS`): without it the
34
+ # mixin target does not exist, `RBS::DefinitionBuilder` raises
35
+ # `NoMixinFoundError`, and the whole `CGI` class silently degrades
36
+ # to `Dynamic[top]`.
37
+ #
32
38
  class CGI
33
39
  include CGI::QueryExtension
34
40
  end
@@ -21,6 +21,13 @@
21
21
  # — bound as singleton methods by the C extension; the
22
22
  # gem's RBS only declares the matching instance methods.
23
23
  #
24
+ # Loaded ONLY when the `prism` library resolves
25
+ # (`RbsLoader::LIBRARY_SUPPLEMENT_VENDORED_DIRS`): the
26
+ # `ParseResult < Result` re-opening names a superclass only the
27
+ # gem's own RBS declares, so without it `RBS::DefinitionBuilder`
28
+ # raises `NoSuperclassFoundError` and the whole class silently
29
+ # degrades to `Dynamic[top]`.
30
+ #
24
31
  module Prism
25
32
  class ParseResult < Result
26
33
  # Walk the parse result's tree, attaching every comment to
@@ -0,0 +1,67 @@
1
+ #
2
+ # Rigor-side vendored RBS stub for the `racc` gem's runtime.
3
+ #
4
+ # Vendored for ONE structural reason: `data/vendored_gem_sigs/nokogiri/nokogiri.rbs` declares
5
+ # `class Nokogiri::CSS::Parser < Racc::Parser`, and `RBS::DefinitionBuilder` raises
6
+ # `RBS::NoSuperclassFoundError` when a declared superclass has no declaration anywhere in the
7
+ # environment — which took the whole `Nokogiri::CSS::Parser` definition down to `Dynamic[top]`
8
+ # (issue #299). Dropping the superclass from the nokogiri sig would have hidden the inheritance
9
+ # instead of typing it, and `Racc::Parser` is a real, small, stable class, so it is declared here.
10
+ #
11
+ # Scope is the documented runtime surface a racc-generated parser inherits, plus `Racc::ParseError`
12
+ # (which `Nokogiri::CSS::SyntaxError` and every generated parser raise). The `_racc_*` methods are
13
+ # private implementation detail of the generated parse loop and are deliberately NOT declared —
14
+ # nothing outside racc itself calls them.
15
+ #
16
+ # Returns are `untyped` wherever the value is a grammar-defined semantic value: racc has no way to
17
+ # know what a generated parser's actions produce, and guessing would cost false positives.
18
+ #
19
+ module Racc
20
+ # Raised by a generated parser on an unrecoverable syntax error.
21
+ class ParseError < StandardError
22
+ end
23
+
24
+ # The runtime base class every racc-generated parser inherits from.
25
+ class Parser
26
+ # Runs the parse loop, pulling tokens from the subclass's `#next_token`. Returns the grammar's
27
+ # start-rule value.
28
+ def do_parse: () -> untyped
29
+
30
+ # Token source: a generated parser's user code overrides this to return `[token_symbol, value]`,
31
+ # or `nil` / `[false, ...]` at end of input.
32
+ def next_token: () -> untyped
33
+
34
+ # Runs the parse loop pulling tokens by calling `recv.__send__(mid)` instead of `#next_token`.
35
+ def yyparse: (untyped recv, Symbol | String mid) -> untyped
36
+
37
+ # Error hook: called with the offending token id, its value, and the value stack. The default
38
+ # implementation raises `Racc::ParseError`; user code overrides it to report and recover.
39
+ def on_error: (untyped error_token_id, untyped error_value, untyped value_stack) -> void
40
+
41
+ # Explicitly signals a syntax error from inside a parser action.
42
+ def yyerror: () -> void
43
+
44
+ # Accepts the input from inside a parser action, ending the parse.
45
+ def yyaccept: () -> void
46
+
47
+ # Clears the error state after `#on_error`, resuming normal parsing.
48
+ def yyerrok: () -> void
49
+
50
+ # Human-readable name for a token id, or `nil` when the id is unknown.
51
+ def token_to_str: (untyped t) -> String?
52
+
53
+ # Debug-trace hooks. Enabled by a generated parser built with racc's `--debug`; harmless no-ops
54
+ # otherwise.
55
+ def racc_read_token: (untyped t, untyped tok, untyped val) -> void
56
+ def racc_shift: (untyped tok, untyped tstack, untyped vstack) -> void
57
+ def racc_reduce: (untyped toks, untyped sim, untyped tstack, untyped vstack) -> void
58
+ def racc_accept: () -> void
59
+ def racc_e_pop: (untyped state, untyped tstack, untyped vstack) -> void
60
+ def racc_next_state: (untyped curstate, untyped state) -> void
61
+ def racc_print_stacks: (untyped t, untyped v) -> void
62
+ def racc_print_states: (untyped s) -> void
63
+ def racc_token2str: (untyped tok) -> String
64
+
65
+ def self.racc_runtime_type: () -> String
66
+ end
67
+ end
@@ -8,6 +8,17 @@
8
8
  # the upstream RBS already covers, so the loader does not raise
9
9
  # `RBS::DuplicatedDeclarationError`.
10
10
  #
11
+ # "Upstream RBS" here means BOTH `core/rubygems/*.rbs` and the
12
+ # `rbs` gem's `sig/shims/rubygems.rbs`: `rbs` is in
13
+ # `Environment::DEFAULT_LIBRARIES`, so its whole `sig/` tree —
14
+ # shims included — is in scope on every run. A duplicate against
15
+ # either raises `RBS::DuplicatedMethodDefinitionError` inside
16
+ # `RBS::DefinitionBuilder` and collapses the ENTIRE class to
17
+ # `Dynamic[top]` (issue #299), so a shim-declared reader is left
18
+ # to the shim and only the writer half is declared here.
19
+ # `spec/rigor/environment/bundled_rbs_definition_build_spec.rb`
20
+ # fails the suite if a new collision reappears on an `rbs` bump.
21
+ #
11
22
  # Driven by the `references/ruby/lib` survey
12
23
  # (`docs/CURRENT_WORK.md` § "Queued engine items") which
13
24
  # clusters `undefined-method` on Gem-side selectors most-touched
@@ -62,7 +73,7 @@ class Gem::Platform
62
73
  def match_spec?: (untyped) -> bool
63
74
  def match_gem?: (untyped, untyped) -> bool
64
75
 
65
- def self.local: () -> Platform
76
+ def self.local: () -> Gem::Platform
66
77
  def self.match_spec?: (untyped) -> bool
67
78
  def self.match_gem?: (untyped, untyped) -> bool
68
79
  def self.installable?: (untyped) -> bool
@@ -75,7 +86,9 @@ class Gem::Dependency
75
86
  # permissive splat keeps the canonical and the
76
87
  # `(name, requirements, type)` invocation forms both silent.
77
88
  def initialize: (*untyped) -> void
78
- attr_accessor name: String
89
+ # `#name`'s READER is declared by `sig/shims/rubygems.rbs` (`() -> String`); only the writer is
90
+ # ours, so this is a `def name=` rather than the `attr_accessor` the other members use.
91
+ def name=: (String) -> String
79
92
  attr_accessor requirement: Gem::Requirement
80
93
  attr_accessor type: Symbol
81
94
  attr_accessor prerelease: bool
@@ -111,7 +124,9 @@ class Gem::Specification
111
124
  def required_ruby_version: () -> Gem::Requirement
112
125
  def require_paths: () -> Array[String]
113
126
  attr_accessor name: String
114
- attr_accessor version: Gem::Version
127
+ # `#version`'s READER is declared by `sig/shims/rubygems.rbs` (`attr_reader version (): Version`);
128
+ # only the writer is ours.
129
+ def version=: (Gem::Version) -> Gem::Version
115
130
  attr_accessor full_gem_path: String
116
131
  attr_accessor loaded_from: String?
117
132
  attr_accessor post_install_message: String?
@@ -131,7 +146,7 @@ end
131
146
 
132
147
  class Gem::Requirement
133
148
  def requirements: () -> Array[untyped]
134
- def self.create: (*untyped) -> Gem::Requirement
149
+ # `.create` declared by `core/rubygems/requirement.rbs`, and more precisely than this file did.
135
150
  end
136
151
 
137
152
  class Gem::ConfigFile
@@ -172,7 +187,7 @@ class Gem::SourceList
172
187
  def include?: (untyped) -> bool
173
188
  def each: () { (untyped) -> untyped } -> void
174
189
  def to_a: () -> Array[untyped]
175
- def from: (untyped) -> SourceList
190
+ def from: (untyped) -> Gem::SourceList
176
191
  end
177
192
 
178
193
  class Gem::RequestSet
@@ -217,10 +232,6 @@ class Gem::Installer
217
232
  def initialize: (*untyped) -> void
218
233
  end
219
234
 
220
- class Gem::DependencyInstaller
221
- def initialize: (*untyped) -> void
222
- end
223
-
224
235
  class Gem::MissingSpecError < StandardError
225
236
  def initialize: (*untyped) -> void
226
237
  end
@@ -48,11 +48,38 @@ the `paths:` list from the configuration file.
48
48
  | `--treat-all-as-inline-rbs` | Force-load `rigor-rbs-inline` with `require_magic_comment: false`, so every analysed file is treated as inline-RBS without the `# rbs_inline: enabled` comment (ADR-32). |
49
49
  | `--bleeding-edge[=ids]` | Adopt the bleeding-edge overlay for this run, overriding the configured [`bleeding_edge:`](03-configuration.md) selection (ADR-50 § WD2). Bare adopts every queued feature; `--bleeding-edge=a,b` adopts only the named feature ids. Inspect it with [`rigor show-bleedingedge`](#rigor-show-bleedingedge). |
50
50
  | `--no-bleeding-edge` | Ignore any configured `bleeding_edge:` selection for this run (adopt none). |
51
- | `--tmp-file=PATH --instead-of=PATH` | Editor mode: analyse `PATH` using the buffer in `--tmp-file`. Both required together. |
51
+ | `--tmp-file=PATH --instead-of=PATH` | Editor mode: analyse `PATH` using the buffer in `--tmp-file`. Both required together. Alone, only the buffer's own file produces diagnostics; add `--incremental` for whole-project scope (see below). |
52
52
 
53
53
  Exit `0` when no error-severity diagnostics remain, `1` when
54
54
  any are reported, `64` on a usage error.
55
55
 
56
+ ### Editor mode scope
57
+
58
+ `--tmp-file` / `--instead-of` on their own analyse **only** the
59
+ buffer's file: fast, and blind to what the unsaved edit does to the
60
+ rest of the project.
61
+
62
+ Adding `--incremental` analyses the **whole project with the buffer
63
+ substituted** — the edited file and the files that depend on it are
64
+ re-analysed, everything else is served from the incremental snapshot.
65
+ An unsaved change to a method's return type therefore surfaces in its
66
+ callers, not just in the file being edited.
67
+
68
+ Two things to know:
69
+
70
+ - It needs a snapshot to reuse. Run `rigor check --incremental` once
71
+ (it is also what a normal terminal check should use) and every later
72
+ editor invocation gets whole-project scope. Without one, Rigor says
73
+ so on stderr and analyses the buffer alone.
74
+ - An editor-mode run never writes the snapshot — the buffer's bytes
75
+ exist only in your editor, and recording them would make the next
76
+ ordinary run believe it had already analysed a file in a state it
77
+ was never in.
78
+
79
+ `--verify-incremental` refuses a buffer: it compares against a full
80
+ analysis of the files on disk, which a buffer contradicts by
81
+ construction.
82
+
56
83
  ## `rigor init`
57
84
 
58
85
  Write a starter configuration file.
@@ -333,7 +360,10 @@ the effectiveness ratio and `--format=json` carries `mode`,
333
360
  `killed`, `survived`, `effectiveness_ratio`, per-file rows, and
334
361
  `add_a_type_here`. It is the truth tier behind the static
335
362
  `--protection` proxy, at the cost of many analyses — an opt-in CI
336
- deep-dive, not an interactive check.
363
+ deep-dive, not an interactive check. `--workers=N` fork-parallelizes
364
+ this tier too (the whole-project pre-pass is paid once on the parent
365
+ and the per-file measurement is spread across workers), with the
366
+ same precedence chain and the same byte-identical output.
337
367
 
338
368
  ```sh
339
369
  rigor coverage --protection --mutation [paths]
@@ -352,7 +382,10 @@ cost is proportional to the protection hole. `--format=json`
352
382
  carries `mode` (`protection-fused`), `type_killed`,
353
383
  `test_killed`, `unprotected`, `protected_ratio`, per-file rows,
354
384
  and `add_protection_here`; `--threshold` gates on the fused
355
- ratio.
385
+ ratio. This tier is always sequential — the suite hook shells out
386
+ to your test runner, and concurrent runs would race over one
387
+ working tree — so `--workers` does not apply and an explicit one
388
+ is reported as ignored on stderr.
356
389
 
357
390
  `--test-command=CMD` is the runner hook (default
358
391
  `bundle exec rake`). The suite must pass on clean code first, or
@@ -378,6 +411,18 @@ to a deterministic sample of `N` mutations per file, bounding the
378
411
  cost on large files. Per-file ratios then become estimates, noted
379
412
  on stderr so `--format=json` stdout stays clean.
380
413
 
414
+ A re-run is served from a per-file measurement cache while
415
+ nothing that could change a file's number has moved: the file
416
+ itself, any file it was recorded as reading from, the resolved
417
+ configuration, `sig/`, the gem set, the engine version, `--limit`
418
+ / `--seed`, and the adopted bleeding-edge features. It reads the
419
+ cross-file edges a `rigor check --incremental` run records, so
420
+ warm one once per project; without a usable snapshot every file
421
+ is measured, never silently served. `--no-cache` measures
422
+ everything from scratch. A one-line stderr report says which of
423
+ those happened — see
424
+ [Type-protection coverage](15-type-protection-coverage.md).
425
+
381
426
  ```sh
382
427
  rigor coverage --protection --mutation --with-tests \
383
428
  --test-command "bundle exec rspec" --include-dynamic [paths]
@@ -578,7 +623,7 @@ pages from the installed gem with no HTTP request.
578
623
  ## `rigor show-bleedingedge`
579
624
 
580
625
  Print the **bleeding-edge overlay** — the Rigor-maintained set of
581
- the next major's queued diagnostic disciplines ([ADR-50](../adr/50-release-engineering-and-stability-strategy.md)
626
+ the next major's queued changes ([ADR-50](../adr/50-release-engineering-and-stability-strategy.md)
582
627
  § WD2) — and report which of them the project's
583
628
  [`bleeding_edge:`](03-configuration.md) configuration adopts. Read-only:
584
629
  it loads `.rigor.yml` to resolve the active selection but runs no
@@ -593,16 +638,31 @@ rigor show-bleedingedge [--config PATH] [--format text|json]
593
638
  | `--config PATH` | Use this `.rigor.yml` instead of auto-discovery. |
594
639
  | `--format text\|json` | Output format. Default `text`. |
595
640
 
596
- Each queued feature appears with its stable id, the severity it imposes,
597
- and whether your configuration adopts it. See
598
- [`docs/compatibility.md`](../compatibility.md) for how bleeding-edge fits
599
- the stability model.
641
+ Each queued feature appears with its stable id, its **kind** `severity`
642
+ or `behaviour` — and whether your configuration adopts it. A `severity`
643
+ feature also prints the rule → severity diff it imposes; a `behaviour`
644
+ feature changes a measurement, an algorithm, or a default without moving
645
+ any rule's severity, so it has no such diff and its summary is the whole
646
+ description. See [`docs/compatibility.md`](../compatibility.md) for how
647
+ bleeding-edge fits the stability model.
600
648
 
601
649
  Queued today:
602
650
 
603
- | Feature id | What it changes |
604
- | --- | --- |
605
- | `reject-unparseable-signatures` | An unparseable `.rbs` under `signature_paths:` **fails the run** (`rbs.coverage.quarantined-signature` → `error`) instead of being skipped with a warning. |
651
+ | Feature id | Kind | What it changes |
652
+ | --- | --- | --- |
653
+ | `reject-unparseable-signatures` | severity | An unparseable `.rbs` under `signature_paths:` **fails the run** (`rbs.coverage.quarantined-signature` → `error`) instead of being skipped with a warning. |
654
+ | `use-of-void-value` | severity | Using a value recovered from an author-declared `-> void` return in value context is reported as `static.value-use.void` (`warning`). |
655
+ | `discovery-seeded-mutation-sites` | behaviour | [`rigor coverage --protection --mutation`](15-type-protection-coverage.md) measures against the same cross-file project discovery Tier 1 already uses — both when picking the sites and when deciding whether a breakage was caught — so a call on a project class declared in a *sibling* file is measured instead of dropped, and a breakage there can actually be caught. **Adds sites to the denominator, so the reported effectiveness ratio moves** — check it against any `--threshold` you pin in CI before adopting. |
656
+ | `dependent-closure-kill-oracle` | behaviour | [`rigor coverage --protection --mutation`](15-type-protection-coverage.md) counts a breakage as caught when the diagnostic appears anywhere in the mutated file **or the files that depend on it**, instead of in the mutated file alone — so changing what a method returns counts as caught when the error lands in its callers. Can only **add** kills, so the ratio moves up or not at all; it costs about a third more wall time per mutant, and a ratio measured under it is not comparable with one measured without it. |
657
+
658
+ Once a feature **graduates** — it becomes the default at a major
659
+ ([ADR-50](../adr/50-release-engineering-and-stability-strategy.md) § WD7)
660
+ — it leaves the queued list and appears under `Graduated`, an
661
+ acknowledgement that naming it in `bleeding_edge:` no longer does
662
+ anything: the behaviour is on for everyone. The section is absent while
663
+ nothing has graduated. In `--format json` the same information is the
664
+ `graduated` array, alongside `overlay` (every queued feature, each with
665
+ its `kind`), `active`, and `unknown_selected`.
606
666
 
607
667
  ## `rigor doctor`
608
668
 
@@ -116,7 +116,22 @@ statically and may resolve at run time. The same findings appear in the
116
116
  | `severity_profile` | String | `"balanced"` | `lenient`, `balanced`, or `strict` — see [Diagnostics](04-diagnostics.md). |
117
117
  | `severity_overrides` | Hash | `{}` | Per-rule / per-family severity, e.g. `{ call: warning, flow.always-truthy-condition: off }`. |
118
118
  | `baseline` | String / `false` | `nil` | Path to a `.rigor-baseline.yml`, or `false` to disable an inherited one. See [Baselines](06-baseline.md). |
119
- | `bleeding_edge` | Boolean / Array / Hash | `false` | Adopt the next major's queued diagnostic disciplines early ([ADR-50](../adr/50-release-engineering-and-stability-strategy.md) § WD2). `false` adopts none; `true` adopts the whole overlay; a list of feature ids adopts only those; `{ all: true, except: [ids] }` adopts all but the named. Orthogonal to `severity_profile`. Override it for a single run with [`rigor check --bleeding-edge[=ids]`](02-cli-reference.md#rigor-check) / `--no-bleeding-edge`. Inspect with [`rigor show-bleedingedge`](02-cli-reference.md#rigor-show-bleedingedge). |
119
+ | `bleeding_edge` | Boolean / Array / Hash | `false` | Adopt the next major's queued changes early ([ADR-50](../adr/50-release-engineering-and-stability-strategy.md) § WD2). `false` adopts none; `true` adopts the whole overlay; a list of feature ids adopts only those; `{ all: true, except: [ids] }` adopts all but the named. Orthogonal to `severity_profile`. Override it for a single run with [`rigor check --bleeding-edge[=ids]`](02-cli-reference.md#rigor-check) / `--no-bleeding-edge`. Inspect with [`rigor show-bleedingedge`](02-cli-reference.md#rigor-show-bleedingedge). |
120
+
121
+ A queued feature is one of two kinds, and `bleeding_edge:` selects both the
122
+ same way:
123
+
124
+ - A **severity** feature promotes one or more rules — a discipline Rigor
125
+ already reports quietly becomes an error or a warning. Its diff is a list
126
+ of rule ids, printed by `rigor show-bleedingedge`.
127
+ - A **behaviour** feature changes a measurement, an algorithm, or a
128
+ default, and moves no rule's severity. There is no rule-id diff to read,
129
+ so its summary in `rigor show-bleedingedge` is the whole description of
130
+ what adopting it does.
131
+
132
+ An id you name that this version of Rigor does not know is ignored rather
133
+ than rejected, so a shared `.rigor.yml` can name a feature that only some
134
+ of the versions in use have queued.
120
135
 
121
136
  ### Dependency RBS discovery
122
137