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
@@ -20,10 +20,18 @@ module Rigor
20
20
  #
21
21
  # Issue #1740 — {OwnMethod} shares {.levels} with the walk below and asks the opposite question for the two
22
22
  # argument checks: does the receiver's own method answer ahead of every refinement in effect?
23
+ #
24
+ # Issue #1752 — a class object is walked through its own lookup ({ClassObject}): its singleton class and each
25
+ # superclass's, then `Class`, `Module`, `Object`, `Kernel` and `BasicObject`, so a refinement of `Object` answers
26
+ # `Integer.oo` while `Integer.sqrt` keeps answering ahead of one.
23
27
  module RefinedDispatch
24
28
  # The refinement that answers: `module_name` refines `refined_class` (a class or module on the walk).
25
29
  Winner = Data.define(:module_name, :refined_class)
26
30
 
31
+ # Issue #1752 — the singleton class `#<Class:of>` as an entry of a class object's walk ({ClassObject}): it
32
+ # defines the name through `of`'s own singleton methods, and no refinement table key names it.
33
+ SingletonClass = Data.define(:of)
34
+
27
35
  # The answer when the in-effect list carries {InEffectRefinements::UNKNOWN} and some refinement defines the
28
36
  # name on a class the walk reaches: any of them may answer.
29
37
  UNKNOWN = :unknown
@@ -37,14 +45,16 @@ module Rigor
37
45
  def wildcard = Scope::DiscoveryIndex::REFINEMENT_WILDCARD
38
46
 
39
47
  # A {Winner}, {UNKNOWN}, or nil when no in-effect refinement answers `method_name` on an instance of
40
- # `class_name` (the class's own lookup does). `list` is the call site's in-effect refinements.
48
+ # `class_name` (the class's own lookup does), or with `kind: :singleton` on the class object `class_name`
49
+ # itself (issue #1752). `list` is the call site's in-effect refinements.
41
50
  #
42
51
  # ADR-121 WD7 — a row the walk could not read never yields a {Winner}: an in-effect module's row for the name
43
52
  # whose class the walk could not name reaches every receiver, and a names-wildcard row of a level the walk
44
53
  # reaches before a definer may define the name, so both answer {UNKNOWN}. So does a winner whose module shares
45
54
  # its last segment with another listed module (A1): which of a `using`'s declared candidates Ruby finds can
46
- # depend on load order.
47
- def winner(scope, class_name, method_name, list)
55
+ # depend on load order. A class object's walk always ends at `Object`, so where it cannot be read a row that
56
+ # may sit on it answers {UNKNOWN} too.
57
+ def winner(scope, class_name, method_name, list, kind: :instance)
48
58
  refinements = scope.discovered_refinements
49
59
  return UNKNOWN if class_unknown_row?(scope, refinements, method_name, list)
50
60
 
@@ -53,13 +63,26 @@ module Rigor
53
63
  unknown = list.include?(InEffectRefinements::UNKNOWN)
54
64
  return nil if targets.nil? && unread.nil? && !unknown
55
65
 
56
- levels = levels(scope, class_name)
66
+ levels = levels(scope, class_name, kind: kind)
67
+ return unread_class_object(scope, refinements, method_name, list) if levels.nil? && kind == :singleton
57
68
  return nil if levels.nil?
58
69
  return UNKNOWN if unknown && unknown_reaches?(scope, refinements, method_name, levels)
59
70
 
60
71
  walk_targets(scope, levels, method_name, [targets, unread], list)
61
72
  end
62
73
 
74
+ # {UNKNOWN} when some row for `method_name` (or a names-wildcard row) that `list` puts in effect, or any row
75
+ # under the unknown marker, names a class or module a class object may reach ({ClassObject.reachable?}).
76
+ def unread_class_object(scope, refinements, method_name, list)
77
+ unknown = list.include?(InEffectRefinements::UNKNOWN)
78
+ reaches = refinements.any? do |refined, methods|
79
+ rows = [methods[method_name], methods[wildcard]].compact
80
+ !rows.empty? && (unknown || rows.any? { |modules| in_effect?(modules, list) }) &&
81
+ ClassObject.reachable?(scope, refined)
82
+ end
83
+ reaches ? UNKNOWN : nil
84
+ end
85
+
63
86
  def walk_targets(scope, levels, method_name, (targets, unread), list)
64
87
  return nil if targets.nil? && unread.nil?
65
88
 
@@ -165,20 +188,40 @@ module Rigor
165
188
  # refinement first, as any level is; past that, the mixin may define the name, so a refinement further up the
166
189
  # walk may or may not win and the answer is {UNKNOWN}, never the replaced method's. ADR-121 WD7: a level an
167
190
  # in-effect module has a names-wildcard row for (`unread`) may define the name too, so it answers {UNKNOWN}.
191
+ #
192
+ # Issue #1752 — on a class object's walk a singleton level that only an entry no table names may answer
193
+ # (`:continue_unsure`) makes a refinement reached past it {UNKNOWN}: that entry may define the name.
168
194
  def walk(scope, levels, method_name, targets, unread)
169
- levels.each_with_index do |(level_class, entries, mixed), index|
170
- return UNKNOWN if level_class && unread.key?(level_class)
171
-
172
- refining = level_class && targets[level_class]
173
- return Winner.new(module_name: refining, refined_class: level_class) if refining
174
- return targeted_after?(levels, index, targets, unread) ? UNKNOWN : nil if mixed
195
+ unsure = false
196
+ levels.each_index do |index|
197
+ answer = level_answer(scope, levels, index, method_name, [targets, unread])
198
+ next unsure = true if answer == :continue_unsure
199
+ next if answer == :continue
175
200
 
176
- answer = walk_entries(scope, entries, level_class, method_name, targets, unread)
177
- return answer unless answer == :continue
201
+ return unsure && answer.is_a?(Winner) ? UNKNOWN : answer
178
202
  end
179
203
  nil
180
204
  end
181
205
 
206
+ def level_answer(scope, levels, index, method_name, (targets, unread))
207
+ level_class, entries, mixed, every = levels[index]
208
+ return UNKNOWN if level_class && unread.key?(level_class)
209
+
210
+ refining = level_class && targets[level_class]
211
+ return Winner.new(module_name: refining, refined_class: level_class) if refining
212
+ return targeted_after?(levels, index, targets, unread) ? UNKNOWN : nil if mixed
213
+
214
+ walk_level(scope, [entries, every], level_class, method_name, [targets, unread])
215
+ end
216
+
217
+ def walk_level(scope, (entries, every), level_class, method_name, (targets, unread))
218
+ if level_class.is_a?(SingletonClass)
219
+ return ClassObject.walk_level(scope, every || entries, method_name, targets, unread)
220
+ end
221
+
222
+ walk_entries(scope, entries, level_class, method_name, targets, unread)
223
+ end
224
+
182
225
  # A level's prepended modules, its own method and its included modules, in that order: `:continue` when none
183
226
  # answers, so the walk moves on.
184
227
  def walk_entries(scope, entries, level_class, method_name, targets, unread)
@@ -205,7 +248,10 @@ module Rigor
205
248
  # name. `every entry` (project levels only) is the level's entries in lookup order with each one that
206
249
  # neither the project's resolver nor RBS can name kept as its candidate names (an Array) where `entries`
207
250
  # leaves it out: the typer never reads it, and {OwnMethod} declines where a refinement may name one.
208
- def levels(scope, class_name)
251
+ # `kind: :singleton` answers the class object `class_name` instead ({ClassObject.levels}, issue #1752).
252
+ def levels(scope, class_name, kind: :instance)
253
+ return ClassObject.levels(scope, class_name) if kind == :singleton
254
+
209
255
  environment = scope.environment
210
256
  return rbs_levels(scope, class_name) if environment&.class_known?(class_name)
211
257
 
@@ -257,8 +303,9 @@ module Rigor
257
303
 
258
304
  # Does `name` define `method_name` on its instance side itself: a project `def` (its dependency edge recorded
259
305
  # through `Scope#user_def_for`), another project definer (`attr_*`, `define_method`), or an RBS declaration
260
- # written on `name` rather than on an ancestor?
306
+ # written on `name` rather than on an ancestor? A {SingletonClass} asks its own singleton methods.
261
307
  def defines?(scope, name, method_name)
308
+ return ClassObject.defines?(scope, name.of, method_name) if name.is_a?(SingletonClass)
262
309
  return true if scope.user_def_for(name, method_name)
263
310
  return true if scope.discovered_method?(name, method_name, :instance)
264
311
 
@@ -269,4 +316,5 @@ module Rigor
269
316
  end
270
317
  end
271
318
 
319
+ require_relative "refined_dispatch/class_object"
272
320
  require_relative "refined_dispatch/own_method"
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../type"
6
+
7
+ module Rigor
8
+ module Inference
9
+ # Issue #1889 — `instance_variable_set` called on `self` writes an instance variable that no write node shows.
10
+ # Left unseen, the binding the call site holds outlived it: `@n = 0; instance_variable_set(:@n, 5)` kept `@n`
11
+ # on `0`, and a block that wrote `@n` only this way never entered the captured-binding name set, so
12
+ # `[1, 2].inject(0) { |a, e| instance_variable_set(:@n, @n + 1); a + @n }` folded to a constant.
13
+ #
14
+ # A call counts when its receiver is implicit or `self` and it passes any argument ({.self_write?}). With
15
+ # exactly two plain positional arguments, the first a Symbol or String literal naming an instance variable
16
+ # (`:@n`, `"@n"`; Ruby accepts both), it is a write of that variable ({.literal_name}). Any other argument list
17
+ # — a computed name, a splat (`instance_variable_set(*args)`), a keyword hash, or a literal Ruby rejects with
18
+ # `NameError` (`:n`) — names an ivar the analysis cannot see, so every ivar the scope binds may have been
19
+ # written ({.apply}; for a rejected literal this over-approximates, which only widens).
20
+ # `obj.instance_variable_set` writes another object's ivar and does not count; neither does the reflective
21
+ # `send(:instance_variable_set, …)`.
22
+ module ReflectiveIvarWrite
23
+ METHOD_NAME = :instance_variable_set
24
+ private_constant :METHOD_NAME
25
+
26
+ IVAR_NAME = /\A@[A-Za-z_]\w*\z/
27
+ private_constant :IVAR_NAME
28
+
29
+ REPEATING_KEY = :__rigor_reflective_ivar_write_repeating__
30
+ private_constant :REPEATING_KEY
31
+
32
+ module_function
33
+
34
+ # True when `node` is an `instance_variable_set` on `self` with at least one argument.
35
+ def self_write?(node)
36
+ return false unless node.is_a?(Prism::CallNode) && node.name == METHOD_NAME
37
+ return false unless node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
38
+
39
+ arguments = node.arguments&.arguments
40
+ !arguments.nil? && !arguments.empty?
41
+ end
42
+
43
+ # `:@n` for `instance_variable_set(:@n, v)` or `instance_variable_set("@n", v)` on `self`, else nil.
44
+ def literal_name(node)
45
+ return nil unless self_write?(node)
46
+
47
+ arguments = node.arguments.arguments
48
+ return nil unless arguments.size == 2 && arguments.none? { |arg| splat_or_keywords?(arg) }
49
+
50
+ name = arguments.first
51
+ text =
52
+ case name
53
+ when Prism::SymbolNode, Prism::StringNode then name.unescaped
54
+ end
55
+ text && IVAR_NAME.match?(text) ? text.to_sym : nil
56
+ end
57
+
58
+ # True for a call {.self_write?} takes whose target {.literal_name} cannot name: it may write any ivar.
59
+ def unknown_target?(node)
60
+ self_write?(node) && literal_name(node).nil?
61
+ end
62
+
63
+ # The value argument of a call {.literal_name} names.
64
+ def value_node(node)
65
+ node.arguments.arguments[1]
66
+ end
67
+
68
+ # Runs the block with the evaluator inside a `while` / `until` / `for` body. The loop's rebind fixpoint widens
69
+ # locals only, so a literal-name write there must not replace the binding with one pass's value
70
+ # ({.apply}).
71
+ def in_repeating_body
72
+ depth = Thread.current[REPEATING_KEY] || 0
73
+ Thread.current[REPEATING_KEY] = depth + 1
74
+ begin
75
+ yield
76
+ ensure
77
+ Thread.current[REPEATING_KEY] = depth
78
+ end
79
+ end
80
+
81
+ def repeating_body?
82
+ (Thread.current[REPEATING_KEY] || 0).positive?
83
+ end
84
+
85
+ # `scope` after `node` runs. A literal name binds that ivar to the value's type (yielded), as `@n = v` does.
86
+ # Inside a loop body ({.in_repeating_body}) the value and `Dynamic[top]` are joined into the current binding
87
+ # instead: the loop's fixpoint widens locals only, so its single pass over an ivar types the value from one
88
+ # iteration's locals (`instance_variable_set(:@n, i + 1)` stored `1`) and would leave that. Any other call joins
89
+ # `Dynamic[top]` into every ivar `scope` binds: any one of them may now hold the value, and joining the
90
+ # value's own type would report a method the old value has and the new one lacks. An ivar on its ADR-58
91
+ # declaration seed keeps its mark, which stops a nil in the seed from fuelling a nil-receiver diagnostic.
92
+ def apply(node, scope)
93
+ return scope unless self_write?(node)
94
+
95
+ name = literal_name(node)
96
+ return bind_literal(scope, name, yield) if name
97
+
98
+ floor = Type::Combinator.untyped
99
+ scope.ivars.reduce(scope) do |acc, (ivar, type)|
100
+ widened = Type::Combinator.union(type, floor)
101
+ rebound = acc.with_ivar(ivar, widened).without_ivar_guard(ivar)
102
+ scope.declaration_sourced?(:ivar, ivar) ? rebound.seed_declaration_sourced_ivar(ivar, widened) : rebound
103
+ end
104
+ end
105
+
106
+ def bind_literal(scope, name, value)
107
+ type = repeating_body? ? Type::Combinator.union(*scope.ivar(name), value, Type::Combinator.untyped) : value
108
+ scope.with_ivar(name, type).without_ivar_guard(name)
109
+ end
110
+
111
+ def splat_or_keywords?(arg)
112
+ arg.is_a?(Prism::SplatNode) || arg.is_a?(Prism::KeywordHashNode) || arg.is_a?(Prism::BlockArgumentNode)
113
+ end
114
+ end
115
+ end
116
+ end