rigortype 0.1.19 → 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.
Files changed (166) hide show
  1. checksums.yaml +4 -4
  2. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -23
  3. data/lib/rigor/analysis/check_rules/rule_walk.rb +3 -21
  4. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
  5. data/lib/rigor/analysis/check_rules.rb +492 -71
  6. data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
  7. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
  8. data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
  9. data/lib/rigor/analysis/fact_store.rb +5 -4
  10. data/lib/rigor/analysis/rule_catalog.rb +153 -6
  11. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +17 -17
  12. data/lib/rigor/analysis/runner/project_pre_passes.rb +9 -8
  13. data/lib/rigor/analysis/runner.rb +17 -6
  14. data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
  15. data/lib/rigor/analysis/worker_session.rb +10 -14
  16. data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
  17. data/lib/rigor/cache/store.rb +5 -3
  18. data/lib/rigor/cli/annotate_command.rb +28 -7
  19. data/lib/rigor/cli/baseline_command.rb +4 -3
  20. data/lib/rigor/cli/check_command.rb +115 -16
  21. data/lib/rigor/cli/coverage_command.rb +148 -16
  22. data/lib/rigor/cli/coverage_scan.rb +57 -0
  23. data/lib/rigor/cli/explain_command.rb +2 -0
  24. data/lib/rigor/cli/lsp_command.rb +3 -7
  25. data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
  26. data/lib/rigor/cli/mutation_protection_report.rb +73 -0
  27. data/lib/rigor/cli/options.rb +9 -0
  28. data/lib/rigor/cli/plugins_command.rb +2 -1
  29. data/lib/rigor/cli/protection_renderer.rb +63 -0
  30. data/lib/rigor/cli/protection_report.rb +68 -0
  31. data/lib/rigor/cli/sig_gen_command.rb +2 -1
  32. data/lib/rigor/cli/trace_command.rb +2 -1
  33. data/lib/rigor/cli/triage_command.rb +2 -1
  34. data/lib/rigor/cli/type_of_command.rb +1 -1
  35. data/lib/rigor/cli/type_scan_command.rb +2 -1
  36. data/lib/rigor/cli.rb +3 -2
  37. data/lib/rigor/configuration/dependencies.rb +2 -4
  38. data/lib/rigor/configuration.rb +45 -7
  39. data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
  40. data/lib/rigor/environment/class_registry.rb +4 -3
  41. data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
  42. data/lib/rigor/environment/lockfile_resolver.rb +1 -1
  43. data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
  44. data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
  45. data/lib/rigor/environment/rbs_loader.rb +49 -5
  46. data/lib/rigor/environment.rb +17 -7
  47. data/lib/rigor/flow_contribution/fact.rb +1 -1
  48. data/lib/rigor/flow_contribution.rb +3 -5
  49. data/lib/rigor/inference/acceptance.rb +17 -9
  50. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  51. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
  52. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
  53. data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
  54. data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
  55. data/lib/rigor/inference/expression_typer.rb +20 -28
  56. data/lib/rigor/inference/hkt_body.rb +8 -11
  57. data/lib/rigor/inference/hkt_body_parser.rb +10 -12
  58. data/lib/rigor/inference/hkt_registry.rb +10 -11
  59. data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
  60. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +156 -21
  61. data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
  62. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
  63. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
  64. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
  65. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
  66. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
  67. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
  68. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +90 -15
  69. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
  70. data/lib/rigor/inference/method_dispatcher.rb +40 -48
  71. data/lib/rigor/inference/mutation_widening.rb +5 -11
  72. data/lib/rigor/inference/narrowing.rb +14 -16
  73. data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
  74. data/lib/rigor/inference/project_patched_methods.rb +4 -7
  75. data/lib/rigor/inference/project_patched_scanner.rb +2 -13
  76. data/lib/rigor/inference/protection_scanner.rb +86 -0
  77. data/lib/rigor/inference/scope_indexer.rb +129 -55
  78. data/lib/rigor/inference/statement_evaluator.rb +244 -114
  79. data/lib/rigor/inference/struct_fold_safety.rb +181 -0
  80. data/lib/rigor/inference/synthetic_method.rb +7 -7
  81. data/lib/rigor/language_server/completion_provider.rb +6 -12
  82. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
  83. data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
  84. data/lib/rigor/language_server/hover_provider.rb +2 -3
  85. data/lib/rigor/language_server/hover_renderer.rb +2 -11
  86. data/lib/rigor/language_server/server.rb +9 -17
  87. data/lib/rigor/language_server.rb +4 -5
  88. data/lib/rigor/plugin/base.rb +10 -8
  89. data/lib/rigor/plugin/macro/block_as_method.rb +3 -4
  90. data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
  91. data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
  92. data/lib/rigor/plugin/macro.rb +4 -5
  93. data/lib/rigor/plugin/manifest.rb +45 -66
  94. data/lib/rigor/plugin/registry.rb +6 -7
  95. data/lib/rigor/plugin/type_node_resolver.rb +6 -8
  96. data/lib/rigor/protection/mutation_scanner.rb +120 -0
  97. data/lib/rigor/protection/mutator.rb +246 -0
  98. data/lib/rigor/rbs_extended.rb +24 -36
  99. data/lib/rigor/reflection.rb +4 -7
  100. data/lib/rigor/scope/discovery_index.rb +14 -2
  101. data/lib/rigor/scope.rb +54 -11
  102. data/lib/rigor/sig_gen/observed_call.rb +3 -3
  103. data/lib/rigor/sig_gen/writer.rb +40 -2
  104. data/lib/rigor/source/constant_path.rb +62 -0
  105. data/lib/rigor/source.rb +1 -0
  106. data/lib/rigor/type/bound_method.rb +2 -11
  107. data/lib/rigor/type/combinator.rb +16 -3
  108. data/lib/rigor/type/constant.rb +2 -11
  109. data/lib/rigor/type/data_class.rb +2 -11
  110. data/lib/rigor/type/data_instance.rb +2 -11
  111. data/lib/rigor/type/hash_shape.rb +2 -11
  112. data/lib/rigor/type/integer_range.rb +2 -11
  113. data/lib/rigor/type/intersection.rb +2 -11
  114. data/lib/rigor/type/nominal.rb +2 -11
  115. data/lib/rigor/type/plain_lattice.rb +37 -0
  116. data/lib/rigor/type/refined.rb +72 -13
  117. data/lib/rigor/type/singleton.rb +2 -11
  118. data/lib/rigor/type/struct_class.rb +75 -0
  119. data/lib/rigor/type/struct_instance.rb +93 -0
  120. data/lib/rigor/type/tuple.rb +5 -15
  121. data/lib/rigor/type.rb +2 -0
  122. data/lib/rigor/version.rb +1 -1
  123. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  124. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
  125. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +3 -3
  126. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
  127. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
  128. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +7 -10
  129. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
  130. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
  131. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +6 -8
  132. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
  133. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -2
  134. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
  135. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
  136. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
  137. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
  138. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
  139. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
  140. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
  141. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +7 -9
  142. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
  143. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
  144. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +3 -3
  145. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
  146. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
  147. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
  148. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +1 -1
  149. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
  150. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
  151. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +5 -5
  152. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
  153. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
  154. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  155. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +19 -14
  156. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
  157. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
  158. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
  159. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
  160. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
  161. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
  162. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
  163. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +28 -41
  164. data/sig/rigor/scope.rbs +9 -1
  165. data/sig/rigor/type.rbs +36 -1
  166. metadata +19 -1
@@ -11,15 +11,13 @@ require_relative "rspec/let_type_resolver"
11
11
  module Rigor
12
12
  module Plugin
13
13
  # rigor-rspec — validates RSpec `let` / `subject`
14
- # declarations within each describe / context scope.
14
+ # declarations within each describe / context scope,
15
+ # narrows `expect(x).to MATCHER` assertions downstream,
16
+ # and binds `let`/`subject` locals to their inferred
17
+ # return types inside `it` bodies.
15
18
  #
16
19
  # Tier 3A of the [Rails plugins roadmap](../../../../docs/design/20260508-rails-plugins-roadmap.md).
17
- # Deliberately scoped — the roadmap describes a much
18
- # larger plugin (let-typo detection in `it` bodies,
19
- # `expect(x).to receive(:method)` mock-target
20
- # validation). Both are out of scope for v0.1.0; this
21
- # plugin ships the two checks that have the lowest
22
- # false-positive risk:
20
+ # Ships four checks:
23
21
  #
24
22
  # 1. **Duplicate `let` / `subject` declarations** in
25
23
  # the same scope (`warning`). RSpec's runtime lets
@@ -31,14 +29,21 @@ module Rigor
31
29
  # (`error`). At runtime this infinite-loops; users
32
30
  # typically meant to call a different method or
33
31
  # forgot to introduce a `super`.
32
+ # 3. **`expect(x).to MATCHER` narrowing** — narrows
33
+ # the named local `x` on the post-call edge for
34
+ # eight matchers (see `MatcherAnalyzer`).
35
+ # 4. **`let`/`subject` local binding** — binds `let`
36
+ # / `subject` names in `it` bodies to the block's
37
+ # inferred return type (see `LetTypeResolver`).
34
38
  #
35
39
  # ## Configuration
36
40
  #
37
- # No knobs in v0.1.0. The plugin walks every analysed
38
- # file looking for `RSpec.describe ... do` blocks; spec
39
- # files outside the project's `paths:` are not scanned.
41
+ # No configuration knobs. The plugin walks every
42
+ # analysed file for `RSpec.describe ... do` blocks;
43
+ # spec files outside the project's `paths:` are not
44
+ # scanned.
40
45
  #
41
- # ## Limitations (v0.1.0)
46
+ # ## Limitations
42
47
  #
43
48
  # - **No let-typo detection.** Detecting an `it`
44
49
  # block's reference to a misspelled `let` name
@@ -102,14 +107,14 @@ module Rigor
102
107
  Analyzer.diagnose(path: path, root: root).map { |diag| build_diagnostic(diag) }
103
108
  end
104
109
 
105
- # ADR-37 slice 2 — Pillar 2 Slice 1 matcher narrowing
110
+ # ADR-37 slice 2 — matcher narrowing
106
111
  # (`expect(x).to be_a(T)` → `post_return_facts` on `x`),
107
112
  # method-gated by the engine on the expectation verbs.
108
113
  type_specifier methods: %i[to not_to to_not] do |call_node, scope|
109
114
  MatcherAnalyzer.contribution_for(call_node, environment: scope&.environment)&.post_return_facts
110
115
  end
111
116
 
112
- # Pillar 2 Slice 2 / ADR-52 slice 5a — binds local reads in `it` /
117
+ # ADR-52 slice 5a — binds local reads in `it` /
113
118
  # spec bodies to their `let(:name) { ... }` block's inferred return
114
119
  # type. The name set varies per file (each spec file's
115
120
  # `describe`/`let` structure), so the rule gates on the per-file
@@ -130,7 +135,7 @@ module Rigor
130
135
  let_scope_index_for(path)&.let_names || []
131
136
  end
132
137
 
133
- # Pillar 2 Slice 2 — when the call node is a no-receiver
138
+ # When the call node is a no-receiver
134
139
  # method call (`user`, `subject`, etc.) inside an RSpec
135
140
  # `describe` block whose lets include a matching name,
136
141
  # return the let block's inferred type.
@@ -31,7 +31,6 @@ module Rigor
31
31
  # validate_absence_of(:col)
32
32
  # validate_format_of(:col)
33
33
  # validate_confirmation_of(:col)
34
- # allow_value(...).for(:col)
35
34
  # have_db_column(:col)
36
35
  # have_db_index(:col)
37
36
  #
@@ -9,10 +9,11 @@ module Rigor
9
9
  # analyzer can validate `Worker.perform_async(...)`
10
10
  # call sites.
11
11
  #
12
- # Same envelope shape as `rigor-activejob`'s
13
- # `JobIndex::Entry`: `min_arity` / `max_arity` form a
14
- # closed range (`Float::INFINITY` for the upper bound
15
- # when `*args` is present).
12
+ # Uses the same `min_arity` / `max_arity` closed-range
13
+ # envelope as `rigor-activejob`'s `JobIndex::Entry`
14
+ # (`Float::INFINITY` upper bound when `*args` is present);
15
+ # Sidekiq workers serialize args to JSON so keyword arity
16
+ # is not tracked here.
16
17
  class WorkerIndex
17
18
  Entry = Data.define(:class_name, :min_arity, :max_arity) do
18
19
  def arity_label
@@ -168,9 +168,8 @@ module Rigor
168
168
  # contribution mirrors `T.must` minus the nil-stripping:
169
169
  # the call's return type is the inner expression's
170
170
  # inferred type. The companion diagnostic is emitted by
171
- # the plugin's `diagnostics_for_file` hook through
172
- # {RevealTypeRecognizer}; the recogniser here is
173
- # contribution-only.
171
+ # the plugin's `diagnostics_for_file` hook; this
172
+ # method handles the contribution only.
174
173
  def resolve_reveal_type(call_node, scope)
175
174
  inner = nth_argument(call_node, 0)
176
175
  return Rigor::Type::Combinator.untyped if inner.nil? || scope.nil?
@@ -4,22 +4,18 @@ module Rigor
4
4
  module Plugin
5
5
  class Sorbet < Rigor::Plugin::Base
6
6
  # Frozen description of one Sorbet `sig` block as parsed by
7
- # {SigParser}. Holds enough to reconstruct the method's
8
- # call-site return type (slice 1's deliverable) plus the
9
- # parameter shape and modifier list (kept for slice 2+ when
10
- # we begin checking call-site argument types and override
11
- # compatibility).
7
+ # {SigParser}. Holds the return type, parameter shape, and
8
+ # modifier list. Call-site argument-type checking and
9
+ # override-compatibility validation are deferred; only the
10
+ # return-type contribution is active.
12
11
  #
13
12
  # `kind` distinguishes `def foo` (`:instance`) from
14
13
  # `def self.foo` / `class << self; def foo; end`
15
14
  # (`:singleton`).
16
15
  #
17
- # `modifiers` is the set of `sig`-level modifiers we
18
- # observed: `:abstract`, `:override`, `:overridable`,
19
- # `:final`. Slice 1 records them but does not act on them;
20
- # later slices wire `:abstract` into the existing
21
- # `def.return-type-mismatch` check and `:override` into
22
- # override-compatibility validation.
16
+ # `modifiers` records the `sig`-level flags observed:
17
+ # `:abstract`, `:override`, `:overridable`, `:final`.
18
+ # Currently stored but not acted on.
23
19
  MethodSignature = Data.define(
24
20
  :class_name, :method_name, :kind, :params, :return_type, :modifiers
25
21
  )
@@ -26,9 +26,8 @@ module Rigor
26
26
  # whatever it recognises (`params` / `returns` / `void` /
27
27
  # `abstract` / `override` / `overridable` / `final` /
28
28
  # `type_parameters` / `checked` / `on_failure`) into a
29
- # frozen result hash. Slice 1 wires the parsed structure
30
- # into {MethodSignature}; later slices will start *acting*
31
- # on the modifiers and `type_parameters`.
29
+ # frozen result hash stored in {MethodSignature}. Modifiers
30
+ # and `type_parameters` are recorded but not yet acted on.
32
31
  #
33
32
  # The parser is intentionally tolerant — unknown chain
34
33
  # nodes degrade to "the rest of the chain is opaque" rather
@@ -93,8 +92,8 @@ module Rigor
93
92
  when :params
94
93
  accumulator[:params].merge!(parse_params(current))
95
94
  when :type_parameters
96
- # Slice 1: recognise to suppress the degraded
97
- # path; widen translation in slice 3.
95
+ # Recognised to suppress the degraded path;
96
+ # payload intentionally discarded (deferred).
98
97
  when *RECOGNISED_MODIFIERS
99
98
  accumulator[:modifiers] << current.name
100
99
  when *RUNTIME_ONLY_STEPS
@@ -17,15 +17,12 @@ module Rigor
17
17
  # default) when no sigil is present, matching how Sorbet
18
18
  # treats sigil-less files.
19
19
  #
20
- # Slice 5 of ADR-11 uses this purely at catalog-harvest
21
- # time: `# typed: ignore` files are skipped entirely (the
22
- # plugin records no sigs from them). The other levels are
23
- # detected for forward compatibility but treated
24
- # identically per-call-site sigil honouring (e.g. only
25
- # firing `T.let` recognition in `# typed: true`+ files)
26
- # requires threading the file path through
27
- # the per-call recognition path, which lives behind a
28
- # future plugin-contract widening slice.
20
+ # ADR-11 slice 5 uses this at catalog-harvest time:
21
+ # `# typed: ignore` files are skipped entirely; other
22
+ # levels gate both sig contributions (`:true`/`:strict`/
23
+ # `:strong` only) and per-call assertion recognition
24
+ # (`T.let` / `T.cast` / `T.must` / etc.) via
25
+ # `Sorbet#assertion_enforced_here?`.
29
26
  module SigilDetector
30
27
  # Sorbet's strictness-level names. Stored as symbols to
31
28
  # match the analyzer's existing convention for level
@@ -9,14 +9,6 @@ module Rigor
9
9
  # block's `params(...)` and `returns(...)` clauses) into
10
10
  # Rigor's internal type carriers.
11
11
  #
12
- # Slice 1 covered the minimum vocabulary that lets a
13
- # typical `sig { params(x: Integer).returns(String) }`
14
- # round-trip; slice 3 widens it to cover the dense middle
15
- # of Sorbet's surface — generic class applications
16
- # (`T::Array[E]`, `T::Hash[K, V]`, etc.), class-object
17
- # types (`T.class_of(C)`, `T::Class[T]`), tuples, and
18
- # shapes:
19
- #
20
12
  # | Sorbet form | Rigor carrier |
21
13
  # | ------------------------ | ---------------------------------------- |
22
14
  # | `Integer` etc. | `Nominal["Integer"]` |
@@ -42,10 +34,9 @@ module Rigor
42
34
  #
43
35
  # Anything else (`T.proc`, `T.attached_class`,
44
36
  # `T.self_type`, `T.type_parameter`, `T::Struct` / `T::Enum`
45
- # subclasses, …) degrades to `Dynamic[top]`. The degraded
46
- # path stays silent for now per ADR-11's slice plan; a
47
- # later slice surfaces the gap as a `dynamic.sorbet.unsupported`
48
- # diagnostic.
37
+ # subclasses, …) degrades silently to `Dynamic[top]`. The
38
+ # `dynamic.sorbet.unsupported` diagnostic for degraded
39
+ # forms is deferred.
49
40
  module TypeTranslator
50
41
  BOOLEAN_NAME = "Boolean"
51
42
 
@@ -235,9 +226,8 @@ module Rigor
235
226
  # analogue (Singleton names a specific class); the
236
227
  # closest faithful translation is `Singleton[name]`
237
228
  # when `T` is a constant, or `Singleton[Object]` for
238
- # broader applications. Lossy translation; emitted as
239
- # `dynamic.sorbet.degraded` once slice 3's diagnostic
240
- # surface lands.
229
+ # broader applications. Lossy the `dynamic.sorbet.degraded`
230
+ # diagnostic for this case is deferred.
241
231
  def translate_t_class_subscript(args)
242
232
  inner = args.first
243
233
  return Rigor::Type::Combinator.singleton_of("Class") if inner.nil?
@@ -29,11 +29,10 @@ module Rigor
29
29
  # nesting; `def self.foo` is recognised as a singleton
30
30
  # method.
31
31
  #
32
- # Slice 1 vocabulary is the bare minimum to round-trip the
33
- # most common sig shapes; the {TypeTranslator} table
34
- # documents what's covered. Anything else (T.proc / T::Array
35
- # / T.class_of / T::Struct) degrades silently to
36
- # `Dynamic[top]` for now — slice 3 widens the translator.
32
+ # The {TypeTranslator} table documents coverage. Most of
33
+ # Sorbet's vocabulary translates; remaining gaps (`T.proc`,
34
+ # `T::Struct` subclasses, `T.attached_class`, etc.) degrade
35
+ # silently to `Dynamic[top]`.
37
36
  #
38
37
  # Architecture: per-run `Catalog` is built lazily on first
39
38
  # access by walking every configured `paths:` entry's `.rb`
@@ -99,20 +98,15 @@ module Rigor
99
98
  # `false` to record every file's sigs regardless of
100
99
  # sigil (current behaviour pre-this-config).
101
100
  @enforce_sigil = config.fetch("enforce_sigil")
102
- # ADR-11 deferred follow-up — per-call-site assertion
103
- # gating. Catalog harvest's `@sigil_by_path` cache is
104
- # consulted at every per-call recognition so
105
- # `T.let` / `T.cast` / `T.must` / `T.bind` /
106
- # `T.assert_type!` only fire in files Sorbet itself
107
- # would enforce (`# typed: true` / `:strict` /
108
- # `:strong`). When `@enforce_sigil` is off (the user
109
- # opted out at harvest time), the gate also opens at
110
- # every call site — current behaviour. Files whose
111
- # sigil hasn't been observed yet (e.g. the catalog
112
- # hasn't run, or the call site is in a fixture /
113
- # synthetic path the harvest didn't see) treat
114
- # missing-info as enforced — failing-open is friendlier
115
- # for spec ergonomics than failing-closed.
101
+ # Per-call-site assertion gating: `@sigil_by_path` (built
102
+ # during catalog harvest) is consulted so `T.let` /
103
+ # `T.cast` / `T.must` / `T.bind` / `T.assert_type!` only
104
+ # fire in files Sorbet itself would enforce (`:true` /
105
+ # `:strict` / `:strong`). With `enforce_sigil: false` the
106
+ # gate is open everywhere. Missing-sigil paths (synthetic
107
+ # fixtures, out-of-tree call sites) default to enforced —
108
+ # failing-open suits spec ergonomics better than
109
+ # failing-closed. See `assertion_enforced_here?`.
116
110
  @sigil_by_path = {}
117
111
  @catalog = nil
118
112
  @parse_errors_by_path = {}
@@ -160,8 +154,7 @@ module Rigor
160
154
  diagnostics
161
155
  end
162
156
 
163
- # ADR-52 slice 4 — the per-call return-type path, migrated off
164
- # the legacy `flow_contribution_for` hook onto the
157
+ # ADR-52 slice 4 — per-call return-type path via the
165
158
  # method-name-gated `dynamic_return` DSL. The recognised name
166
159
  # set is only known at run time (the catalog's `def` names come
167
160
  # from the lazy catalog build), so it is declared as a
@@ -170,17 +163,15 @@ module Rigor
170
163
  # resolved Symbol Set. The gate is a safe over-approximation —
171
164
  # a project method merely *named* `cast` or `find` passes it
172
165
  # and is declined by the block's own `T.`-receiver / catalog
173
- # checks, exactly as the ungated hook used to decline.
166
+ # checks.
174
167
  dynamic_return methods: -> { recognised_method_names } do |call_node, scope|
175
168
  contribution_return_type(call_node, scope)
176
169
  end
177
170
 
178
171
  # ADR-52 slice 4 — `T.bind(self, T)`'s self-narrowing fact,
179
- # migrated off the legacy hook's `post_return_facts` slot onto
180
- # the method-gated `type_specifier` DSL (once
181
- # `flow_contribution_for` is gone, the statement evaluator
182
- # consults only this path for narrowing facts). The
183
- # return-type half (`Constant[nil]`) flows through the
172
+ # contributed via the method-gated `type_specifier` DSL. The
173
+ # statement evaluator consults this path for narrowing facts.
174
+ # The return-type half (`Constant[nil]`) flows through the
184
175
  # `dynamic_return` rule above; the block re-checks the `T.`
185
176
  # receiver via the recogniser, so an unrelated `bind` call
186
177
  # contributes nothing.
@@ -190,8 +181,8 @@ module Rigor
190
181
 
191
182
  private
192
183
 
193
- # ADR-52 slice 4 — the run-time method-name gate for the
194
- # `dynamic_return` rule: the static assertion vocabulary
184
+ # Run-time method-name gate for the `dynamic_return` rule
185
+ # (ADR-52 slice 4): the static assertion vocabulary
195
186
  # (`T.let` / `T.cast` / …), `T.absurd`, and every method name
196
187
  # the catalog carries a sig for.
197
188
  def recognised_method_names
@@ -202,20 +193,17 @@ module Rigor
202
193
  names
203
194
  end
204
195
 
205
- # The migrated body of the legacy `flow_contribution_for`
206
- # same recognition order, but returns the bare `Rigor::Type`
207
- # the `dynamic_return` contract expects. Resolves the receiver
208
- # in two passes:
196
+ # Main contribution body for the `dynamic_return` rule.
197
+ # Returns the bare `Rigor::Type` the contract expects.
198
+ # Resolves the receiver in three passes:
209
199
  #
210
200
  # 1. Constant receiver (`User.find(...)`) → singleton-side
211
201
  # catalog lookup.
212
202
  # 2. Nominal receiver-type (`user.name` where `user`'s
213
203
  # inferred type is `Nominal["User"]`) → instance-side
214
204
  # catalog lookup.
215
- #
216
- # Implicit-self calls (no receiver, current-class method)
217
- # are deferred to slice 2 — slice 1 covers the common case
218
- # where the sig is on the called method's own class.
205
+ # 3. Implicit-self (receiver-less inside a method body) →
206
+ # current-class lookup via `implicit_self_lookup`.
219
207
  def contribution_return_type(call_node, scope)
220
208
  return nil unless call_node.is_a?(Prism::CallNode)
221
209
 
@@ -286,9 +274,8 @@ module Rigor
286
274
  contribution&.post_return_facts
287
275
  end
288
276
 
289
- # ADR-11 deferred follow-up — per-call-site assertion
290
- # gating. With `enforce_sigil: false`, the gate is fully
291
- # open (matches the pre-feature behaviour). With
277
+ # Per-call-site assertion gating (ADR-11). With
278
+ # `enforce_sigil: false` the gate is fully open. With
292
279
  # `enforce_sigil: true` (default), the caller file's
293
280
  # sigil must reach `:true` / `:strict` / `:strong` for
294
281
  # assertions to fire. Three honest fallbacks:
@@ -334,7 +321,7 @@ module Rigor
334
321
  end
335
322
  end
336
323
 
337
- # ADR-11 slice 2 (deferred from slice 1) — implicit-self calls.
324
+ # ADR-11 slice 2 — implicit-self calls.
338
325
  # A receiver-less call inside a method body resolves against the
339
326
  # engine's own `scope.self_type`: `Nominal[Foo]` inside an
340
327
  # instance method (instance-side lookup), `Singleton[Foo]` inside
data/sig/rigor/scope.rbs CHANGED
@@ -11,6 +11,7 @@ module Rigor
11
11
  attr_reader indexed_narrowings: Hash[IndexedKey, Type::t]
12
12
  attr_reader method_chain_narrowings: Hash[ChainKey, Type::t]
13
13
  attr_reader source_path: String?
14
+ attr_reader struct_fold_safe_locals: Set[Symbol]
14
15
 
15
16
  # ADR-53 Track A — the seed-time discovery tables live on the
16
17
  # DiscoveryIndex; Scope keeps per-table readers as delegates.
@@ -29,6 +30,8 @@ module Rigor
29
30
  def discovered_includes: () -> Hash[String, Array[String]]
30
31
  def discovered_class_sources: () -> Hash[String, Set[String]]
31
32
  def data_member_layouts: () -> Hash[String, Array[Symbol]]
33
+ def struct_member_layouts: () -> Hash[String, { members: Array[Symbol], keyword_init: bool }]
34
+ def param_inferred_types: () -> Hash[[String, Symbol, Symbol], Hash[Symbol, Type::t]]
32
35
 
33
36
  class DiscoveryIndex
34
37
  attr_reader declared_types: Hash[untyped, Type::t]
@@ -46,10 +49,12 @@ module Rigor
46
49
  attr_reader discovered_includes: Hash[String, Array[String]]
47
50
  attr_reader discovered_class_sources: Hash[String, Set[String]]
48
51
  attr_reader data_member_layouts: Hash[String, Array[Symbol]]
52
+ attr_reader struct_member_layouts: Hash[String, { members: Array[Symbol], keyword_init: bool }]
53
+ attr_reader param_inferred_types: Hash[[String, Symbol, Symbol], Hash[Symbol, Type::t]]
49
54
 
50
55
  EMPTY: DiscoveryIndex
51
56
 
52
- def with: (?declared_types: Hash[untyped, Type::t], ?class_ivars: Hash[String, Hash[Symbol, Type::t]], ?class_cvars: Hash[String, Hash[Symbol, Type::t]], ?program_globals: Hash[Symbol, Type::t], ?discovered_classes: Hash[String, Type::Singleton], ?in_source_constants: Hash[String, Type::t], ?discovered_methods: Hash[String, Hash[Symbol, Symbol]], ?discovered_def_nodes: Hash[String, Hash[Symbol, untyped]], ?discovered_singleton_def_nodes: Hash[String, Hash[Symbol, untyped]], ?discovered_def_sources: Hash[String, Hash[Symbol, String]], ?discovered_method_visibilities: Hash[String, Hash[Symbol, Symbol]], ?discovered_superclasses: Hash[String, String], ?discovered_includes: Hash[String, Array[String]], ?discovered_class_sources: Hash[String, Set[String]], ?data_member_layouts: Hash[String, Array[Symbol]]) -> DiscoveryIndex
57
+ def with: (?declared_types: Hash[untyped, Type::t], ?class_ivars: Hash[String, Hash[Symbol, Type::t]], ?class_cvars: Hash[String, Hash[Symbol, Type::t]], ?program_globals: Hash[Symbol, Type::t], ?discovered_classes: Hash[String, Type::Singleton], ?in_source_constants: Hash[String, Type::t], ?discovered_methods: Hash[String, Hash[Symbol, Symbol]], ?discovered_def_nodes: Hash[String, Hash[Symbol, untyped]], ?discovered_singleton_def_nodes: Hash[String, Hash[Symbol, untyped]], ?discovered_def_sources: Hash[String, Hash[Symbol, String]], ?discovered_method_visibilities: Hash[String, Hash[Symbol, Symbol]], ?discovered_superclasses: Hash[String, String], ?discovered_includes: Hash[String, Array[String]], ?discovered_class_sources: Hash[String, Set[String]], ?data_member_layouts: Hash[String, Array[Symbol]], ?struct_member_layouts: Hash[String, { members: Array[Symbol], keyword_init: bool }], ?param_inferred_types: Hash[[String, Symbol, Symbol], Hash[Symbol, Type::t]]) -> DiscoveryIndex
53
58
  end
54
59
 
55
60
  class IndexedKey
@@ -68,6 +73,8 @@ module Rigor
68
73
 
69
74
  def initialize: (environment: Environment, locals: Hash[Symbol, Type::t], ?fact_store: Analysis::FactStore, ?self_type: Type::t?, ?ivars: Hash[Symbol, Type::t], ?cvars: Hash[Symbol, Type::t], ?globals: Hash[Symbol, Type::t], ?discovery: DiscoveryIndex, ?source_path: String?) -> void
70
75
  def with_source_path: (String? path) -> Scope
76
+ def with_struct_fold_safe: (Set[Symbol] locals) -> Scope
77
+ def struct_fold_safe?: (String | Symbol name) -> bool
71
78
  def with_discovery: (DiscoveryIndex index) -> Scope
72
79
  def local: (String | Symbol name) -> Type::t?
73
80
  def ivar: (String | Symbol name) -> Type::t?
@@ -94,6 +101,7 @@ module Rigor
94
101
  def discovered_method_visibility: (String | Symbol class_name, String | Symbol method_name) -> Symbol?
95
102
  def superclass_of: (String | Symbol class_name) -> String?
96
103
  def data_member_layout: (String | Symbol class_name) -> Array[Symbol]?
104
+ def struct_member_layout: (String | Symbol class_name) -> { members: Array[Symbol], keyword_init: bool }?
97
105
  def includes_of: (String | Symbol class_name) -> Array[String]
98
106
  def indexed_narrowing: (Symbol receiver_kind, String | Symbol receiver_name, untyped key) -> Type::t?
99
107
  def with_indexed_narrowing: (Symbol receiver_kind, String | Symbol receiver_name, untyped key, Type::t type) -> Scope
data/sig/rigor/type.rbs CHANGED
@@ -1,6 +1,6 @@
1
1
  module Rigor
2
2
  module Type
3
- type t = Top | Bot | Dynamic | Constant | IntegerRange | Nominal | Singleton | Union | Difference | Tuple | HashShape | DataClass | DataInstance
3
+ type t = Top | Bot | Dynamic | Constant | IntegerRange | Nominal | Singleton | Union | Difference | Tuple | HashShape | DataClass | DataInstance | StructClass | StructInstance
4
4
 
5
5
  type accepts_mode = :strict | :gradual | :loose
6
6
 
@@ -277,6 +277,39 @@ module Rigor
277
277
  def inspect: () -> String
278
278
  end
279
279
 
280
+ class StructClass
281
+ attr_reader members: Array[Symbol]
282
+ attr_reader class_name: String?
283
+ attr_reader keyword_init: bool
284
+ def initialize: (Array[Symbol] members, ?String? class_name, ?keyword_init: bool) -> void
285
+ def describe: (?Symbol verbosity) -> String
286
+ def erase_to_rbs: () -> String
287
+ def top: () -> Trinary
288
+ def bot: () -> Trinary
289
+ def dynamic: () -> Trinary
290
+ def accepts: (Type::t other, ?mode: accepts_mode) -> AcceptsResult
291
+ def ==: (untyped other) -> bool
292
+ def hash: () -> Integer
293
+ def inspect: () -> String
294
+ end
295
+
296
+ class StructInstance
297
+ attr_reader members: Hash[Symbol, Type::t]
298
+ attr_reader class_name: String?
299
+ def initialize: (Hash[Symbol, Type::t] members, ?String? class_name) -> void
300
+ def member_names: () -> Array[Symbol]
301
+ def member_type: (Symbol name) -> Type::t?
302
+ def describe: (?Symbol verbosity) -> String
303
+ def erase_to_rbs: () -> String
304
+ def top: () -> Trinary
305
+ def bot: () -> Trinary
306
+ def dynamic: () -> Trinary
307
+ def accepts: (Type::t other, ?mode: accepts_mode) -> AcceptsResult
308
+ def ==: (untyped other) -> bool
309
+ def hash: () -> Integer
310
+ def inspect: () -> String
311
+ end
312
+
280
313
  class AcceptsResult
281
314
  attr_reader trinary: Trinary
282
315
  attr_reader mode: accepts_mode
@@ -339,6 +372,8 @@ module Rigor
339
372
  def self?.hash_shape_of: (?Hash[untyped, Type::t]? pairs, **untyped options) -> HashShape
340
373
  def self?.data_class_of: (members: Array[Symbol], ?class_name: String?) -> DataClass
341
374
  def self?.data_instance_of: (members: Hash[Symbol, Type::t], ?class_name: String?) -> DataInstance
375
+ def self?.struct_class_of: (members: Array[Symbol], ?class_name: String?, ?keyword_init: bool) -> StructClass
376
+ def self?.struct_instance_of: (members: Hash[Symbol, Type::t], ?class_name: String?) -> StructInstance
342
377
  def self?.union: (*Type::t types) -> Type::t
343
378
  def self?.key_of: (Type::t type) -> Type::t
344
379
  def self?.value_of: (Type::t type) -> Type::t
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rigortype
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.19
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rigor contributors
@@ -322,6 +322,7 @@ files:
322
322
  - lib/rigor/bleeding_edge.rb
323
323
  - lib/rigor/builtins/hkt_builtins.rb
324
324
  - lib/rigor/builtins/imported_refinements.rb
325
+ - lib/rigor/builtins/predefined_constant_refinements.rb
325
326
  - lib/rigor/builtins/regex_refinement.rb
326
327
  - lib/rigor/builtins/static_return_refinements.rb
327
328
  - lib/rigor/cache/descriptor.rb
@@ -344,16 +345,21 @@ files:
344
345
  - lib/rigor/cli/coverage_command.rb
345
346
  - lib/rigor/cli/coverage_renderer.rb
346
347
  - lib/rigor/cli/coverage_report.rb
348
+ - lib/rigor/cli/coverage_scan.rb
347
349
  - lib/rigor/cli/diagnostic_formats.rb
348
350
  - lib/rigor/cli/diff_command.rb
349
351
  - lib/rigor/cli/explain_command.rb
350
352
  - lib/rigor/cli/lsp_command.rb
351
353
  - lib/rigor/cli/mcp_command.rb
354
+ - lib/rigor/cli/mutation_protection_renderer.rb
355
+ - lib/rigor/cli/mutation_protection_report.rb
352
356
  - lib/rigor/cli/options.rb
353
357
  - lib/rigor/cli/plugin_command.rb
354
358
  - lib/rigor/cli/plugins_command.rb
355
359
  - lib/rigor/cli/plugins_renderer.rb
356
360
  - lib/rigor/cli/prism_colorizer.rb
361
+ - lib/rigor/cli/protection_renderer.rb
362
+ - lib/rigor/cli/protection_report.rb
357
363
  - lib/rigor/cli/renderable.rb
358
364
  - lib/rigor/cli/show_bleedingedge_command.rb
359
365
  - lib/rigor/cli/sig_gen_command.rb
@@ -373,6 +379,7 @@ files:
373
379
  - lib/rigor/environment.rb
374
380
  - lib/rigor/environment/bundle_sig_discovery.rb
375
381
  - lib/rigor/environment/class_registry.rb
382
+ - lib/rigor/environment/constant_type_cache_holder.rb
376
383
  - lib/rigor/environment/hkt_registry_holder.rb
377
384
  - lib/rigor/environment/lockfile_resolver.rb
378
385
  - lib/rigor/environment/rbs_collection_discovery.rb
@@ -436,6 +443,7 @@ files:
436
443
  - lib/rigor/inference/method_dispatcher/kernel_dispatch.rb
437
444
  - lib/rigor/inference/method_dispatcher/literal_string_folding.rb
438
445
  - lib/rigor/inference/method_dispatcher/math_folding.rb
446
+ - lib/rigor/inference/method_dispatcher/member_shape_projection.rb
439
447
  - lib/rigor/inference/method_dispatcher/method_folding.rb
440
448
  - lib/rigor/inference/method_dispatcher/overload_selector.rb
441
449
  - lib/rigor/inference/method_dispatcher/rbs_dispatch.rb
@@ -446,18 +454,22 @@ files:
446
454
  - lib/rigor/inference/method_dispatcher/shape_dispatch.rb
447
455
  - lib/rigor/inference/method_dispatcher/shellwords_folding.rb
448
456
  - lib/rigor/inference/method_dispatcher/singleton_folding.rb
457
+ - lib/rigor/inference/method_dispatcher/struct_folding.rb
449
458
  - lib/rigor/inference/method_dispatcher/time_folding.rb
450
459
  - lib/rigor/inference/method_dispatcher/uri_folding.rb
451
460
  - lib/rigor/inference/method_parameter_binder.rb
452
461
  - lib/rigor/inference/multi_target_binder.rb
453
462
  - lib/rigor/inference/mutation_widening.rb
454
463
  - lib/rigor/inference/narrowing.rb
464
+ - lib/rigor/inference/parameter_inference_collector.rb
455
465
  - lib/rigor/inference/precision_scanner.rb
456
466
  - lib/rigor/inference/project_patched_methods.rb
457
467
  - lib/rigor/inference/project_patched_scanner.rb
468
+ - lib/rigor/inference/protection_scanner.rb
458
469
  - lib/rigor/inference/rbs_type_translator.rb
459
470
  - lib/rigor/inference/scope_indexer.rb
460
471
  - lib/rigor/inference/statement_evaluator.rb
472
+ - lib/rigor/inference/struct_fold_safety.rb
461
473
  - lib/rigor/inference/synthetic_method.rb
462
474
  - lib/rigor/inference/synthetic_method_index.rb
463
475
  - lib/rigor/inference/synthetic_method_scanner.rb
@@ -507,6 +519,8 @@ files:
507
519
  - lib/rigor/plugin/source_rbs_synthesis_reporter.rb
508
520
  - lib/rigor/plugin/trust_policy.rb
509
521
  - lib/rigor/plugin/type_node_resolver.rb
522
+ - lib/rigor/protection/mutation_scanner.rb
523
+ - lib/rigor/protection/mutator.rb
510
524
  - lib/rigor/rbs_extended.rb
511
525
  - lib/rigor/rbs_extended/conformance_checker.rb
512
526
  - lib/rigor/rbs_extended/hkt_directives.rb
@@ -527,6 +541,7 @@ files:
527
541
  - lib/rigor/sig_gen/write_result.rb
528
542
  - lib/rigor/sig_gen/writer.rb
529
543
  - lib/rigor/source.rb
544
+ - lib/rigor/source/constant_path.rb
530
545
  - lib/rigor/source/literals.rb
531
546
  - lib/rigor/source/node_locator.rb
532
547
  - lib/rigor/source/node_walker.rb
@@ -551,8 +566,11 @@ files:
551
566
  - lib/rigor/type/integer_range.rb
552
567
  - lib/rigor/type/intersection.rb
553
568
  - lib/rigor/type/nominal.rb
569
+ - lib/rigor/type/plain_lattice.rb
554
570
  - lib/rigor/type/refined.rb
555
571
  - lib/rigor/type/singleton.rb
572
+ - lib/rigor/type/struct_class.rb
573
+ - lib/rigor/type/struct_instance.rb
556
574
  - lib/rigor/type/top.rb
557
575
  - lib/rigor/type/tuple.rb
558
576
  - lib/rigor/type/union.rb