rigortype 0.4.2 → 0.4.4

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 (128) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/core_overlay/ostruct.rbs +40 -0
  4. data/docs/handbook/11-sig-gen.md +28 -5
  5. data/docs/manual/02-cli-reference.md +23 -4
  6. data/docs/manual/03-configuration.md +21 -8
  7. data/docs/manual/04-diagnostics.md +44 -2
  8. data/docs/manual/05-inspecting-types.md +50 -0
  9. data/docs/manual/07-plugins.md +1 -1
  10. data/docs/manual/10-mcp-server.md +32 -3
  11. data/docs/manual/12-caching.md +18 -1
  12. data/docs/manual/14-rails-quickstart.md +2 -2
  13. data/docs/manual/18-removing-dead-code.md +26 -9
  14. data/docs/manual/plugins/rigor-rails-routes.md +2 -2
  15. data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
  16. data/exe/rigor +12 -17
  17. data/lib/rigor/analysis/check_rules/rule_ids.rb +64 -0
  18. data/lib/rigor/analysis/check_rules.rb +68 -57
  19. data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
  20. data/lib/rigor/analysis/incremental.rb +12 -0
  21. data/lib/rigor/analysis/incremental_run_slot.rb +3 -3
  22. data/lib/rigor/analysis/incremental_session.rb +207 -40
  23. data/lib/rigor/analysis/path_expansion.rb +62 -6
  24. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
  25. data/lib/rigor/analysis/reachability/graph.rb +139 -15
  26. data/lib/rigor/analysis/reachability/scan.rb +374 -173
  27. data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
  28. data/lib/rigor/analysis/rule_catalog.rb +6 -1
  29. data/lib/rigor/analysis/run_cache_key.rb +31 -7
  30. data/lib/rigor/analysis/run_cache_probe.rb +6 -4
  31. data/lib/rigor/analysis/run_stats.rb +28 -6
  32. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +74 -13
  33. data/lib/rigor/analysis/runner/pool_coordinator.rb +133 -19
  34. data/lib/rigor/analysis/runner/project_pre_passes.rb +46 -1
  35. data/lib/rigor/analysis/runner.rb +62 -18
  36. data/lib/rigor/analysis/severity_stamp.rb +34 -1
  37. data/lib/rigor/analysis/worker_session.rb +58 -2
  38. data/lib/rigor/cache/descriptor.rb +40 -6
  39. data/lib/rigor/cache/engine_source.rb +21 -7
  40. data/lib/rigor/cache/incremental_snapshot.rb +56 -12
  41. data/lib/rigor/cli/check_command.rb +138 -39
  42. data/lib/rigor/cli/docs_command.rb +5 -4
  43. data/lib/rigor/cli/lens_command.rb +144 -0
  44. data/lib/rigor/cli/lsp_command.rb +5 -2
  45. data/lib/rigor/cli/mcp_command.rb +1 -1
  46. data/lib/rigor/cli/skill_command.rb +3 -3
  47. data/lib/rigor/cli/skill_describe.rb +1 -1
  48. data/lib/rigor/cli/type_of_command.rb +2 -1
  49. data/lib/rigor/cli/unused_command.rb +157 -24
  50. data/lib/rigor/cli.rb +30 -4
  51. data/lib/rigor/config_audit.rb +111 -15
  52. data/lib/rigor/configuration/dependencies.rb +19 -11
  53. data/lib/rigor/configuration.rb +15 -0
  54. data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
  55. data/lib/rigor/environment/installed_gem_set.rb +38 -11
  56. data/lib/rigor/environment/lockfile_resolver.rb +37 -6
  57. data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
  58. data/lib/rigor/environment/rbs_loader.rb +79 -10
  59. data/lib/rigor/environment/required_features.rb +4 -2
  60. data/lib/rigor/environment.rb +12 -5
  61. data/lib/rigor/inference/block_call_timing.rb +5 -0
  62. data/lib/rigor/inference/builtins/cruby_definers.rb +22 -5
  63. data/lib/rigor/inference/captured_locals.rb +69 -6
  64. data/lib/rigor/inference/definer_resolution.rb +16 -7
  65. data/lib/rigor/inference/expression_typer.rb +117 -29
  66. data/lib/rigor/inference/guard_rebinding.rb +7 -2
  67. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +21 -17
  68. data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
  69. data/lib/rigor/inference/match_rebinding.rb +1 -1
  70. data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
  71. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
  72. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
  73. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
  74. data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
  75. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -16
  76. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
  77. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
  78. data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
  79. data/lib/rigor/inference/method_dispatcher.rb +52 -3
  80. data/lib/rigor/inference/module_function_state.rb +80 -13
  81. data/lib/rigor/inference/narrowing.rb +184 -68
  82. data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
  83. data/lib/rigor/inference/project_patched_methods.rb +2 -2
  84. data/lib/rigor/inference/project_patched_scanner.rb +38 -24
  85. data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
  86. data/lib/rigor/inference/refined_dispatch/own_method.rb +112 -40
  87. data/lib/rigor/inference/refined_dispatch.rb +62 -14
  88. data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
  89. data/lib/rigor/inference/repeated_or_writes.rb +416 -1
  90. data/lib/rigor/inference/scope_indexer.rb +565 -61
  91. data/lib/rigor/inference/self_call_ivar_writes.rb +341 -0
  92. data/lib/rigor/inference/statement_evaluator.rb +307 -60
  93. data/lib/rigor/inference/untyped_rejoin.rb +75 -0
  94. data/lib/rigor/language_server/project_context.rb +56 -5
  95. data/lib/rigor/lens/anchor.rb +115 -0
  96. data/lib/rigor/lens/contract_reader.rb +133 -0
  97. data/lib/rigor/lens/document.rb +113 -0
  98. data/lib/rigor/lens/file_reader.rb +802 -0
  99. data/lib/rigor/lens/members.rb +203 -0
  100. data/lib/rigor/lens/parameters.rb +91 -0
  101. data/lib/rigor/lens/plugins.rb +40 -0
  102. data/lib/rigor/lens/run.rb +322 -0
  103. data/lib/rigor/lens/slot.rb +79 -0
  104. data/lib/rigor/lens.rb +41 -0
  105. data/lib/rigor/mcp/server.rb +61 -20
  106. data/lib/rigor/plugin/box_probe.rb +154 -7
  107. data/lib/rigor/project_environment.rb +23 -1
  108. data/lib/rigor/protection/mutation_cache.rb +2 -1
  109. data/lib/rigor/scope.rb +33 -17
  110. data/lib/rigor/sig_gen/generator.rb +45 -5
  111. data/lib/rigor/sig_gen/layout_index.rb +36 -0
  112. data/lib/rigor/sig_gen/method_candidate.rb +8 -0
  113. data/lib/rigor/sig_gen/path_mapper.rb +18 -2
  114. data/lib/rigor/sig_gen/writer.rb +47 -34
  115. data/lib/rigor/version.rb +1 -1
  116. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
  117. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
  118. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
  119. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
  120. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +28 -6
  121. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
  122. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
  123. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
  124. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
  125. data/sig/rigor/scope.rbs +3 -0
  126. data/sig/rigor.rbs +6 -3
  127. data/skills/rigor-project-init/references/02-configure.md +2 -2
  128. metadata +21 -1
@@ -156,6 +156,13 @@ module Rigor
156
156
  # first segment appears here as KNOWN and stays silent: these ids are legitimate suppression /
157
157
  # `severity_overrides:` vocabulary the light rule-id table cannot enumerate (plugins load dynamically;
158
158
  # aggregator ids live in the engine-heavy runner), so under-warning is the FP-safe direction.
159
+ # The `pre-eval.*` ids (ADR-17 WD6) the bare family wildcard `pre-eval` expands to. Emitted by the
160
+ # `pre_eval:` pre-pass, outside the CheckRules catalogue, so the family cannot ride `RULE_FAMILIES`
161
+ # (which also drives the catalogue and the config audit over `ALL_RULES`).
162
+ PRE_EVAL_RULES = %w[
163
+ pre-eval.parse-error pre-eval.file-not-found pre-eval.duplicate-declaration
164
+ ].freeze
165
+
159
166
  NON_CHECK_DIAGNOSTIC_FAMILIES = %w[rbs_extended dynamic rbs pre-eval plugin plugin_trust].freeze
160
167
 
161
168
  # Bare (dot-less) diagnostic ids the engine emits outside the catalogue (see the `rule:` literals in
@@ -166,6 +173,63 @@ module Rigor
166
173
  configuration-error load-error pool-degraded runtime-error source-rbs-synthesis-failed
167
174
  source-rbs-annotation-not-honoured
168
175
  ].freeze
176
+
177
+ # Resolves a user-supplied rule token (`undefined-method`, `call.undefined-method`, or the family wildcard
178
+ # `call`) to the set of canonical rule identifiers it disables. Returns `nil` for `"all"` (the existing
179
+ # wildcard meaning "every rule"), or for unknown tokens.
180
+ def self.resolve_rule_token(token)
181
+ return nil if token == "all"
182
+ return [LEGACY_RULE_ALIASES.fetch(token)] if LEGACY_RULE_ALIASES.key?(token)
183
+ return PRE_EVAL_RULES if token == "pre-eval"
184
+ return ALL_RULES.select { |r| r.start_with?("#{token}.") } if RULE_FAMILIES.include?(token)
185
+
186
+ ALL_RULES.include?(token) ? [token] : []
187
+ end
188
+
189
+ # Expands a list of user-supplied rule tokens into the canonical-id set per ADR-8 § "Backward
190
+ # compatibility". `disabled_rules` accepts unprefixed legacy names (`undefined-method`), canonical names
191
+ # (`call.undefined-method`), family wildcards (`call`) and the `all` sentinel.
192
+ def self.expand_rule_tokens(tokens)
193
+ Array(tokens).each_with_object(Set.new) do |token, set|
194
+ set.merge(expand_token(token.to_s))
195
+ end
196
+ end
197
+
198
+ def self.expand_token(token)
199
+ return ["all"] if token == "all"
200
+
201
+ resolved = resolve_rule_token(token)
202
+ resolved.nil? || resolved.empty? ? [token] : resolved
203
+ end
204
+
205
+ # Whether the `.rigor.yml` `disable:` list, expanded by {.expand_rule_tokens}, drops a diagnostic whose rule
206
+ # id is `rule`: the `all` sentinel drops every rule-bearing row, any other entry its exact id. A nil rule (a
207
+ # parse, path or internal error) is never dropped.
208
+ def self.config_disabled?(rule, disabled)
209
+ return false if rule.nil?
210
+
211
+ disabled.include?("all") || disabled.include?(rule)
212
+ end
213
+
214
+ # The source-family prefix the runner stamps on every plugin-emitted row (`plugin.<id>`).
215
+ PLUGIN_FAMILY_PREFIX = "plugin."
216
+
217
+ # Issue #1953 — whether a suppression token set drops a plugin's own row. Such a row answers only to its
218
+ # qualified id: the exact `plugin.<id>.<rule>` (its `qualified_rule`) or the whole plugin's `plugin.<id>`
219
+ # (its `source_family`). Both compare whole strings against the row's own fields, so `plugin.<id>` never
220
+ # reaches a plugin whose id merely starts with `<id>.`. Ids and rules may both hold dots, so plugin `a` with
221
+ # rule `b.c` and plugin `a.b` with rule `c` share `plugin.a.b.c`, and that exact token drops both. A bare rule
222
+ # and `all` do not reach these rows: many plugins share a bare rule (`load-error`, `unknown-column`), and a
223
+ # bare match would hide another plugin's findings. Every other row (built-in, `plugin_loader`, `plugin_trust`)
224
+ # answers false.
225
+ def self.plugin_row_disabled?(diagnostic, tokens)
226
+ return false if diagnostic.rule.nil?
227
+
228
+ family = diagnostic.source_family.to_s
229
+ return false unless family.start_with?(PLUGIN_FAMILY_PREFIX)
230
+
231
+ tokens.include?(family) || tokens.include?(diagnostic.qualified_rule)
232
+ end
169
233
  end
170
234
  end
171
235
  end
@@ -79,18 +79,8 @@ module Rigor
79
79
  # runtime one and `def.override-visibility-reduced` cannot prove a reduction.
80
80
  IMPLICITLY_PRIVATE_METHODS = (LIFECYCLE_HOOKS + %i[respond_to_missing?]).freeze
81
81
 
82
- # Resolves a user-supplied rule token (`undefined-method`,
83
- # `call.undefined-method`, or the family wildcard `call`)
84
- # to the set of canonical rule identifiers it disables.
85
- # Returns `nil` for `"all"` (the existing wildcard meaning
86
- # "every rule"), or for unknown tokens.
87
- def self.resolve_rule_token(token)
88
- return nil if token == "all"
89
- return [LEGACY_RULE_ALIASES.fetch(token)] if LEGACY_RULE_ALIASES.key?(token)
90
- return ALL_RULES.select { |r| r.start_with?("#{token}.") } if RULE_FAMILIES.include?(token)
91
-
92
- ALL_RULES.include?(token) ? [token] : []
93
- end
82
+ # The token expansion (`resolve_rule_token` / `expand_rule_tokens` / `expand_token`) lives in the light
83
+ # `check_rules/rule_ids.rb`, so {SeverityStamp} can apply the `disable:` list on the engine-free hit path.
94
84
 
95
85
  module_function
96
86
 
@@ -479,7 +469,7 @@ module Rigor
479
469
  # - **File-level** (v0.1.2): `# rigor:disable-file <rule1>,
480
470
  # <rule2>` anywhere in the file suppresses the matching
481
471
  # diagnostic for every line. `# rigor:disable-file all`
482
- # suppresses every rule across the file. Convention is
472
+ # suppresses every built-in rule across the file. Convention is
483
473
  # to put the comment near the top, but Rigor accepts it
484
474
  # anywhere — the comment scope is "this file" regardless
485
475
  # of position.
@@ -487,7 +477,8 @@ module Rigor
487
477
  # on the same line as the offending expression
488
478
  # suppresses the matching diagnostic for that line
489
479
  # only. `# rigor:disable all` on a line suppresses
490
- # every rule on that line.
480
+ # every built-in rule on that line. A plugin's rows are
481
+ # not passed here; see {.filter_plugin_suppressed}.
491
482
  #
492
483
  # Both in-source forms are recognised only when the
493
484
  # marker is the FIRST thing in the comment, so a
@@ -506,7 +497,7 @@ module Rigor
506
497
  diagnostics.reject do |diagnostic|
507
498
  rule = diagnostic.rule
508
499
  next false if rule.nil?
509
- next true if disabled.include?(rule)
500
+ next true if config_disabled?(rule, disabled)
510
501
  next true if file_suppressions.include?("all") || file_suppressions.include?(rule)
511
502
 
512
503
  line_rules = line_suppressions[diagnostic.line]
@@ -514,6 +505,26 @@ module Rigor
514
505
  end
515
506
  end
516
507
 
508
+ # Issue #1953 — the in-source markers over the rows a plugin emitted while `path` was analysed
509
+ # (`#diagnostics_for_file` and node rules). A row answers to a `plugin.<id>.<rule>` or `plugin.<id>` token
510
+ # only ({.plugin_row_disabled?}); a bare id and `all` keep reaching built-in rows alone. `comments` are
511
+ # `path`'s, so a row the plugin positioned in another file is left to the `disable:` list and the baseline.
512
+ # The `disable:` list itself is applied to these rows by {SeverityStamp}, on every run shape.
513
+ def filter_plugin_suppressed(diagnostics, path:, comments:)
514
+ return diagnostics if diagnostics.empty?
515
+
516
+ line_suppressions, file_suppressions = parse_suppression_comments(comments)
517
+ return diagnostics if line_suppressions.empty? && file_suppressions.empty?
518
+
519
+ diagnostics.reject do |diagnostic|
520
+ next false unless diagnostic.path == path
521
+ next true if plugin_row_disabled?(diagnostic, file_suppressions)
522
+
523
+ line_rules = line_suppressions.fetch(diagnostic.line, nil)
524
+ !line_rules.nil? && plugin_row_disabled?(diagnostic, line_rules)
525
+ end
526
+ end
527
+
517
528
  # Every suppression-recognition pattern below is anchored with `\A` against the COMMENT SLICE —
518
529
  # Prism hands us a slice that starts at the `#`, so `\A#` means "the marker is the first thing in
519
530
  # the comment". A real directive always has that shape, in both the whole-line form and the
@@ -654,7 +665,7 @@ module Rigor
654
665
  # `dynamic.*`, `rbs.*`, `pre-eval.*`, and any `plugin.`-prefixed id — plugins load dynamically, so
655
666
  # their rule vocabulary cannot be enumerated here and under-warning is the FP-safe direction).
656
667
  def known_suppression_token?(token)
657
- return true if token == "all"
668
+ return true if %w[all pre-eval].include?(token)
658
669
  return true if ALL_RULES.include?(token) || LEGACY_RULE_ALIASES.key?(token) ||
659
670
  RULE_FAMILIES.include?(token) || NON_CHECK_DIAGNOSTIC_IDS.include?(token)
660
671
 
@@ -703,24 +714,6 @@ module Rigor
703
714
  )
704
715
  end
705
716
 
706
- # Expands a list of user-supplied rule tokens into the
707
- # canonical-id set per ADR-8 § "Backward compatibility".
708
- # `disabled_rules` accepts unprefixed legacy names
709
- # (`undefined-method`), canonical names
710
- # (`call.undefined-method`), and family wildcards (`call`).
711
- def expand_rule_tokens(tokens)
712
- Array(tokens).each_with_object(Set.new) do |token, set|
713
- set.merge(expand_token(token.to_s))
714
- end
715
- end
716
-
717
- def expand_token(token)
718
- return ["all"] if token == "all"
719
-
720
- resolved = resolve_rule_token(token)
721
- resolved.nil? || resolved.empty? ? [token] : resolved
722
- end
723
-
724
717
  # rubocop:disable-next Metrics/ClassLength
725
718
  class << self
726
719
  private
@@ -863,29 +856,23 @@ module Rigor
863
856
 
864
857
  # Is a refinement of `call_node`'s method name into `class_name` (or an ancestor) in effect at the call?
865
858
  # Issue #1120 asks it for a method the class lacks. The two argument rules ask
866
- # {#refined_redefinition_in_effect?} instead (issue #1740), and this only for a class-object receiver.
867
- # Typing the call from the refine body is #1664.
868
- # A refinement answers instance-side receivers only: a refined singleton (`refine X.singleton_class`) names
869
- # no constant target, so nothing records it. Issue #1673 — the answer is derived from the file's in-effect
870
- # refinements ({Inference::InEffectRefinements}), the list the typer reads, with each `using` expanded
871
- # through its module's includes (issue #1671).
859
+ # {#refined_redefinition_in_effect?} instead (issue #1740). Typing the call from the refine body is #1664.
860
+ # Issue #1752 — a class-object receiver (`kind` `:singleton`) is reached by a refinement of any class or module
861
+ # on its lookup: `Class`, `Module`, `Object`, `Kernel`, `BasicObject`, or a module it extends. A refined
862
+ # singleton class (`refine X.singleton_class`) names no constant target, so its rows are class-unknown, which
863
+ # reach every receiver. Issue #1673 — the answer is derived from the file's in-effect refinements
864
+ # ({Inference::InEffectRefinements}), the list the typer reads, with each `using` expanded through its
865
+ # module's includes (issue #1671).
872
866
  #
873
867
  # ADR-121 WD7 — a module in effect whose refinements Rigor cannot read
874
868
  # ({Inference::InEffectRefinements.opaque_module?}: a gem's, one from outside the analysed paths, one with an
875
869
  # unreadable `refine`) may refine any class, a class object's included, so the call declines whatever its
876
- # receiver. Issue #1796; both argument rules ask this predicate too.
870
+ # receiver. Issue #1796.
877
871
  def refined_method_in_effect?(class_name, call_node, scope, kind, lexical_sites)
878
872
  return false if lexical_sites.nil?
879
873
  return true if opaque_refinement_in_effect?(call_node, scope, lexical_sites)
880
874
 
881
- modules =
882
- if kind == :instance
883
- Inference::InEffectRefinements.refining_modules(scope, class_name, call_node.name)
884
- else
885
- # ADR-121 WD7 — a row whose class the walk could not name (`refine(k)` with `k` a singleton class)
886
- # reaches a class object too.
887
- Inference::InEffectRefinements.class_unknown_refining_modules(scope, call_node.name)
888
- end
875
+ modules = Inference::InEffectRefinements.refining_modules(scope, class_name, call_node.name, kind)
889
876
  !modules.nil? &&
890
877
  lexical_sites.refinements.refinement_active?(call_node.location.start_offset, modules,
891
878
  scope.declared_refinements) do |name|
@@ -902,8 +889,8 @@ module Rigor
902
889
  #
903
890
  # A call with nothing in effect is checked. Otherwise it reports only from positive knowledge: the list
904
891
  # carries no unknown marker and no opaque module, and {Inference::RefinedDispatch::OwnMethod} proves the
905
- # receiver's own definer answers first. A class-object receiver keeps {#refined_method_in_effect?}'s
906
- # answer: its singleton-side levels are not walked yet.
892
+ # receiver's own definer answers first. Issue #1752 — a class-object receiver (`kind` `:singleton`) walks its
893
+ # singleton levels: `Integer.sqrt(1, 2)` under `refine Object do def sqrt(a, b)` runs `Integer.sqrt` and raises.
907
894
  def refined_redefinition_in_effect?(class_name, call_node, scope, kind, lexical_sites)
908
895
  return false if lexical_sites.nil?
909
896
 
@@ -911,9 +898,8 @@ module Rigor
911
898
  return false if list.empty?
912
899
  return true if list.include?(Inference::InEffectRefinements::UNKNOWN)
913
900
  return true if opaque_refinement_in_effect?(call_node, scope, lexical_sites)
914
- return refined_method_in_effect?(class_name, call_node, scope, kind, lexical_sites) unless kind == :instance
915
901
 
916
- !Inference::RefinedDispatch::OwnMethod.answers?(scope, class_name, call_node.name, list)
902
+ !Inference::RefinedDispatch::OwnMethod.answers?(scope, class_name, call_node.name, list, kind: kind)
917
903
  end
918
904
 
919
905
  # The in-effect list at the call as the typer reads it: the lexical activations, each `using` expanded,
@@ -1399,6 +1385,21 @@ module Rigor
1399
1385
  GEM_OVERLAY_OPEN_RECEIVERS = Set["ActiveSupport::Duration", "ActiveSupport::Concern"].freeze
1400
1386
  private_constant :GEM_OVERLAY_OPEN_RECEIVERS
1401
1387
 
1388
+ # Issue #1456 — the `data/core_overlay/` twin of {GEM_OVERLAY_OPEN_RECEIVERS}. `OpenStruct` defines each field
1389
+ # as a per-instance singleton method and answers the rest through a private `method_missing` (ostruct 0.6), so
1390
+ # no signature can enumerate its surface. The only other declaration in the environment is json's add-on
1391
+ # reopening (`json_create` / `as_json` / `to_json`), which made the class read as fully declared and every
1392
+ # field access a `call.undefined-method`. `data/core_overlay/ostruct.rbs` declares that `method_missing`, and
1393
+ # an RBS-declared `method_missing` does not by itself open a receiver here, so the class is named. No load
1394
+ # gate is needed: the core overlay loads into every environment that has RBS at all, so the list is active
1395
+ # whenever the declaration it protects is. Both sides of the class are open, as with
1396
+ # {GEM_OVERLAY_OPEN_RECEIVERS}, and an inherited Object / Kernel signature stops deciding `call.wrong-arity`
1397
+ # and `call.argument-type-mismatch` ({#unauthoritative_inherited_signature?}): lost true positives,
1398
+ # accepted. The general rule — a class declared only by another library's partial reopening — is #1978, not
1399
+ # this list.
1400
+ CORE_OVERLAY_OPEN_RECEIVERS = Set["OpenStruct"].freeze
1401
+ private_constant :CORE_OVERLAY_OPEN_RECEIVERS
1402
+
1402
1403
  # ADR-26 — whether `class_name` is declared "open" by a
1403
1404
  # loaded plugin (manifest `open_receivers:`). An open
1404
1405
  # class responds beyond its RBS surface, so the
@@ -1407,11 +1408,14 @@ module Rigor
1407
1408
  # RBS method table, so proving a call "undefined" against it is
1408
1409
  # unsound: a plugin-declared open receiver, a Rigor-bundled-
1409
1410
  # overlay open receiver ({GEM_OVERLAY_OPEN_RECEIVERS}) whose
1410
- # bundled directory this run actually loaded, or a
1411
+ # bundled directory this run actually loaded, a core-overlay
1412
+ # open receiver ({CORE_OVERLAY_OPEN_RECEIVERS}), or a
1411
1413
  # Rigor-synthesized stub type (see `RbsLoader#synthesized_type_names`).
1412
1414
  def unbounded_receiver_surface?(class_name, scope)
1415
+ bare_name = class_name.to_s.sub(/\A::/, "")
1413
1416
  open_receiver?(class_name, scope) || synthesized_stub_receiver?(class_name, scope) ||
1414
- (GEM_OVERLAY_OPEN_RECEIVERS.include?(class_name.to_s.sub(/\A::/, "")) && gem_overlay_loaded?(scope))
1417
+ CORE_OVERLAY_OPEN_RECEIVERS.include?(bare_name) ||
1418
+ (GEM_OVERLAY_OPEN_RECEIVERS.include?(bare_name) && gem_overlay_loaded?(scope))
1415
1419
  end
1416
1420
 
1417
1421
  def open_receiver?(class_name, scope)
@@ -2093,7 +2097,13 @@ module Rigor
2093
2097
  # arm restores the polarity without touching the shared helper:
2094
2098
  # `String | nil` keeps firing, and so does `Dynamic | String | nil` —
2095
2099
  # the nameless arm stays permissive inside the all-arms check, which is
2096
- # only about the arms' method surface.
2100
+ # only about the arms' method surface. That union no longer arises from
2101
+ # a merge re-joining the narrowed edges of one untyped local, which
2102
+ # binds it back to `Dynamic[top]` (issue #1840,
2103
+ # `Inference::UntypedRejoin`); it still does when the program writes
2104
+ # the `nil` (`x = nil if c`) and no `nil?` edge strips it before the
2105
+ # re-joining merge, or when the arms are not a narrowing of one
2106
+ # untyped value.
2097
2107
  def union_has_nameable_non_nil_arm?(union, scope)
2098
2108
  union.members.any? do |m|
2099
2109
  next false if nil_member?(m)
@@ -2195,7 +2205,8 @@ module Rigor
2195
2205
  # ADR-121 WD7 — a refinement in effect (or one Rigor cannot read) may define the name on one arm's class.
2196
2206
  def refined_on_some_arm?(members, call_node, scope, lexical_sites)
2197
2207
  members.any? do |member|
2198
- refined_method_in_effect?(concrete_class_name(member), call_node, scope, :instance, lexical_sites)
2208
+ kind = member.is_a?(Type::Singleton) ? :singleton : :instance
2209
+ refined_method_in_effect?(concrete_class_name(member), call_node, scope, kind, lexical_sites)
2199
2210
  end
2200
2211
  end
2201
2212
 
@@ -80,7 +80,7 @@ module Rigor
80
80
  private
81
81
 
82
82
  def effects_descriptor(paths)
83
- files = PathExpansion.ruby_files(paths, @configuration.exclude_patterns)
83
+ files = PathExpansion.ruby_files(paths, @configuration.exclude_patterns, base: @configuration.exclude_base)
84
84
  base = RunCacheKey.descriptor(
85
85
  configuration: @configuration, files: files, explain: false,
86
86
  rbs_config_entries: RunCacheKey.libraries_config_entries(@configuration)
@@ -120,6 +120,18 @@ module Rigor
120
120
  appeared.freeze
121
121
  end
122
122
 
123
+ # Issue #1459 — the method names some file in `paths` started or stopped defining in this edit, on any class or
124
+ # side. A consumer whose answer asked whether the program defines a name anywhere
125
+ # (`Inference::BlockCallTiming.project_defines_anywhere?`) recorded a `defines:<name>` edge whichever way it
126
+ # answered. `paths` MUST include removed files, whose every name vanished. `before` / `after` map a path to the
127
+ # Set of names it defines (`Inference::ScopeIndexer.defined_method_names`); a path absent from either side
128
+ # defines nothing there.
129
+ def changed_defined_names(paths, before, after)
130
+ moved = Set.new
131
+ paths.each { |path| moved.merge((before[path] || Set.new) ^ (after[path] || Set.new)) }
132
+ moved.freeze
133
+ end
134
+
123
135
  # Issue #1120 — the method names whose refinements moved in this edit. A consumer whose answer consulted the
124
136
  # refinement table recorded a `refinement:<method>` edge whichever way it answered, because the answer is a
125
137
  # function of the whole project's refinements of that name. `paths` MUST include removed files, whose whole
@@ -42,8 +42,8 @@ module Rigor
42
42
  # plugin's identity and the engine's (key slots), so a project whose plugins claim template globs is served
43
43
  # too. The ADR-87 probe misses on one: the plain slot's key carries the `template-units` slot.
44
44
  #
45
- # `explain:` is always false: the session's runners never set it (#1533), and the CLI declines the probe under
46
- # `--explain`.
45
+ # `explain:` is always false: the session writes no slot for an `--explain` run (#1533), and the CLI declines
46
+ # the probe under `--explain`.
47
47
  module IncrementalRunSlot
48
48
  module_function
49
49
 
@@ -115,7 +115,7 @@ module Rigor
115
115
  def serve(configuration:, cache_root:, paths:)
116
116
  return nil if configuration.effects_enabled?
117
117
 
118
- files = PathExpansion.ruby_files(paths, configuration.exclude_patterns)
118
+ files = PathExpansion.ruby_files(paths, configuration.exclude_patterns, base: configuration.exclude_base)
119
119
  slot_key = key(configuration: configuration, target: Target.new(files: files, roots: paths))
120
120
  return nil if slot_key.nil?
121
121