rigortype 0.1.18 → 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 (210) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +159 -224
  3. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -3
  4. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
  5. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +32 -23
  6. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
  7. data/lib/rigor/analysis/check_rules/rule_walk.rb +151 -23
  8. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
  9. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +9 -3
  10. data/lib/rigor/analysis/check_rules.rb +756 -132
  11. data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
  12. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
  13. data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
  14. data/lib/rigor/analysis/diagnostic.rb +8 -0
  15. data/lib/rigor/analysis/fact_store.rb +5 -4
  16. data/lib/rigor/analysis/rule_catalog.rb +153 -6
  17. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +19 -18
  18. data/lib/rigor/analysis/runner/project_pre_passes.rb +13 -9
  19. data/lib/rigor/analysis/runner.rb +75 -27
  20. data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
  21. data/lib/rigor/analysis/worker_session.rb +31 -25
  22. data/lib/rigor/bleeding_edge.rb +123 -0
  23. data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
  24. data/lib/rigor/cache/descriptor.rb +86 -8
  25. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  26. data/lib/rigor/cache/store.rb +5 -3
  27. data/lib/rigor/cli/annotate_command.rb +122 -16
  28. data/lib/rigor/cli/baseline_command.rb +4 -3
  29. data/lib/rigor/cli/check_command.rb +118 -16
  30. data/lib/rigor/cli/coverage_command.rb +148 -16
  31. data/lib/rigor/cli/coverage_scan.rb +57 -0
  32. data/lib/rigor/cli/explain_command.rb +2 -0
  33. data/lib/rigor/cli/lsp_command.rb +3 -7
  34. data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
  35. data/lib/rigor/cli/mutation_protection_report.rb +73 -0
  36. data/lib/rigor/cli/options.rb +9 -0
  37. data/lib/rigor/cli/plugins_command.rb +4 -5
  38. data/lib/rigor/cli/plugins_renderer.rb +0 -2
  39. data/lib/rigor/cli/protection_renderer.rb +63 -0
  40. data/lib/rigor/cli/protection_report.rb +68 -0
  41. data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
  42. data/lib/rigor/cli/sig_gen_command.rb +2 -1
  43. data/lib/rigor/cli/trace_command.rb +2 -1
  44. data/lib/rigor/cli/triage_command.rb +8 -4
  45. data/lib/rigor/cli/triage_renderer.rb +15 -1
  46. data/lib/rigor/cli/type_of_command.rb +1 -1
  47. data/lib/rigor/cli/type_scan_command.rb +2 -1
  48. data/lib/rigor/cli.rb +12 -3
  49. data/lib/rigor/configuration/dependencies.rb +2 -4
  50. data/lib/rigor/configuration/severity_profile.rb +13 -1
  51. data/lib/rigor/configuration.rb +100 -6
  52. data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
  53. data/lib/rigor/environment/class_registry.rb +4 -3
  54. data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
  55. data/lib/rigor/environment/lockfile_resolver.rb +1 -1
  56. data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
  57. data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
  58. data/lib/rigor/environment/rbs_loader.rb +74 -5
  59. data/lib/rigor/environment.rb +17 -7
  60. data/lib/rigor/flow_contribution/fact.rb +1 -1
  61. data/lib/rigor/flow_contribution.rb +3 -5
  62. data/lib/rigor/inference/acceptance.rb +17 -9
  63. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  64. data/lib/rigor/inference/body_fixpoint.rb +89 -0
  65. data/lib/rigor/inference/budget_trace.rb +29 -2
  66. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
  67. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
  68. data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
  69. data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
  70. data/lib/rigor/inference/expression_typer.rb +1072 -71
  71. data/lib/rigor/inference/hkt_body.rb +8 -11
  72. data/lib/rigor/inference/hkt_body_parser.rb +10 -12
  73. data/lib/rigor/inference/hkt_registry.rb +10 -11
  74. data/lib/rigor/inference/macro_block_self_type.rb +2 -2
  75. data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
  76. data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
  77. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +210 -35
  78. data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
  79. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
  80. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
  81. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
  82. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
  83. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
  84. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
  85. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
  86. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
  87. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +237 -24
  88. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
  89. data/lib/rigor/inference/method_dispatcher.rb +112 -49
  90. data/lib/rigor/inference/method_parameter_binder.rb +56 -2
  91. data/lib/rigor/inference/multi_target_binder.rb +46 -3
  92. data/lib/rigor/inference/mutation_widening.rb +147 -11
  93. data/lib/rigor/inference/narrowing.rb +284 -53
  94. data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
  95. data/lib/rigor/inference/project_patched_methods.rb +4 -7
  96. data/lib/rigor/inference/project_patched_scanner.rb +2 -13
  97. data/lib/rigor/inference/protection_scanner.rb +86 -0
  98. data/lib/rigor/inference/scope_indexer.rb +821 -76
  99. data/lib/rigor/inference/statement_evaluator.rb +1179 -102
  100. data/lib/rigor/inference/struct_fold_safety.rb +181 -0
  101. data/lib/rigor/inference/synthetic_method.rb +7 -7
  102. data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
  103. data/lib/rigor/language_server/completion_provider.rb +6 -12
  104. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
  105. data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
  106. data/lib/rigor/language_server/hover_provider.rb +2 -3
  107. data/lib/rigor/language_server/hover_renderer.rb +2 -11
  108. data/lib/rigor/language_server/server.rb +9 -17
  109. data/lib/rigor/language_server.rb +4 -5
  110. data/lib/rigor/plugin/base.rb +245 -87
  111. data/lib/rigor/plugin/macro/block_as_method.rb +25 -25
  112. data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
  113. data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
  114. data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
  115. data/lib/rigor/plugin/macro.rb +6 -8
  116. data/lib/rigor/plugin/manifest.rb +49 -90
  117. data/lib/rigor/plugin/node_rule_walk.rb +59 -14
  118. data/lib/rigor/plugin/registry.rb +18 -18
  119. data/lib/rigor/plugin/type_node_resolver.rb +6 -8
  120. data/lib/rigor/protection/mutation_scanner.rb +120 -0
  121. data/lib/rigor/protection/mutator.rb +246 -0
  122. data/lib/rigor/rbs_extended.rb +24 -36
  123. data/lib/rigor/reflection.rb +4 -7
  124. data/lib/rigor/scope/discovery_index.rb +16 -2
  125. data/lib/rigor/scope.rb +185 -16
  126. data/lib/rigor/sig_gen/generator.rb +8 -0
  127. data/lib/rigor/sig_gen/observed_call.rb +3 -3
  128. data/lib/rigor/sig_gen/writer.rb +40 -2
  129. data/lib/rigor/source/constant_path.rb +62 -0
  130. data/lib/rigor/source.rb +1 -0
  131. data/lib/rigor/triage/catalogue.rb +4 -19
  132. data/lib/rigor/triage.rb +69 -1
  133. data/lib/rigor/type/bound_method.rb +2 -11
  134. data/lib/rigor/type/combinator.rb +45 -3
  135. data/lib/rigor/type/constant.rb +2 -11
  136. data/lib/rigor/type/data_class.rb +2 -11
  137. data/lib/rigor/type/data_instance.rb +2 -11
  138. data/lib/rigor/type/hash_shape.rb +2 -11
  139. data/lib/rigor/type/integer_range.rb +2 -11
  140. data/lib/rigor/type/intersection.rb +2 -11
  141. data/lib/rigor/type/nominal.rb +2 -11
  142. data/lib/rigor/type/plain_lattice.rb +37 -0
  143. data/lib/rigor/type/refined.rb +72 -13
  144. data/lib/rigor/type/singleton.rb +2 -11
  145. data/lib/rigor/type/struct_class.rb +75 -0
  146. data/lib/rigor/type/struct_instance.rb +93 -0
  147. data/lib/rigor/type/tuple.rb +5 -15
  148. data/lib/rigor/type.rb +2 -0
  149. data/lib/rigor/version.rb +1 -1
  150. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  151. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
  152. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +16 -32
  153. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
  154. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
  155. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
  156. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +34 -100
  157. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
  158. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
  159. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
  160. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +26 -27
  161. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
  162. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +9 -8
  163. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
  164. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
  165. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
  166. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
  167. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
  168. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
  169. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
  170. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +18 -49
  171. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
  172. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
  173. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +4 -4
  174. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
  175. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
  176. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
  177. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
  178. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +22 -35
  179. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
  180. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
  181. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +16 -23
  182. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
  183. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
  184. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
  185. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  186. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +21 -27
  187. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
  188. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
  189. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
  190. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
  191. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
  192. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
  193. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
  194. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
  195. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
  196. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
  197. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +52 -40
  198. data/sig/rigor/analysis/fact_store.rbs +3 -0
  199. data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
  200. data/sig/rigor/plugin/base.rbs +5 -2
  201. data/sig/rigor/plugin/manifest.rbs +1 -2
  202. data/sig/rigor/scope.rbs +18 -1
  203. data/sig/rigor/type.rbs +37 -1
  204. data/sig/rigor.rbs +1 -1
  205. data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
  206. data/skills/rigor-plugin-author/SKILL.md +6 -4
  207. data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
  208. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
  209. metadata +25 -2
  210. data/lib/rigor/plugin/macro/external_file.rb +0 -143
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../scope"
6
+ require_relative "../inference/scope_indexer"
7
+
8
+ module Rigor
9
+ # ADR-63 Tier 2 — the productized subset of the dev-only mutation-testing
10
+ # harness (`tool/mutation/`, ADR-62). Only the *per-file effectiveness
11
+ # measurement* lives here — the type-visible {Mutator} and the warm-loop
12
+ # {MutationScanner} kill-rate measurement. The dev sweep / fuzz / survivor
13
+ # clustering stay off the frozen surface in `tool/mutation/mutate.rb`
14
+ # (which now reuses this {Mutator} so there is one source of truth).
15
+ module Protection
16
+ # One concrete edit: replace source bytes [start, stop) with `replacement`.
17
+ # `anchor` is the Prism node whose inferred type decides type-relevance —
18
+ # the call receiver whose contract the mutation could violate, or nil when
19
+ # there is no concrete receiver (implicit-self call, literal outside a call).
20
+ # `anchor_type` (the rendered receiver type) and `method_name` are filled in
21
+ # for reporting a surviving site; both may stay nil.
22
+ Mutation = Struct.new(
23
+ :operator, :expected_rule, :start, :stop, :replacement, :line, :label, :anchor,
24
+ :anchor_type, :method_name,
25
+ keyword_init: true
26
+ ) do
27
+ def apply(source)
28
+ prefix = source.byteslice(0, start)
29
+ suffix = source.byteslice(stop, source.bytesize - stop) || ""
30
+ "#{prefix}#{replacement}#{suffix}"
31
+ end
32
+ end
33
+
34
+ # Generates type-visible mutations of a Ruby source string by walking the
35
+ # Prism AST and recording byte-range splices (no unparser needed — Prism
36
+ # hands us exact offsets, and the analyzer re-parses the spliced source).
37
+ #
38
+ # A mutation is "type-visible" when it should trip a diagnostic rule *if*
39
+ # Rigor holds a type at the site: a call-argument literal dropped to `nil`
40
+ # or type-swapped (→ `call.argument-type-mismatch`), or a call site renamed
41
+ # to a missing method (→ `call.undefined-method`). Only call sites and
42
+ # bodies are mutated, never `def` signatures, so a reused project scan stays
43
+ # valid.
44
+ class Mutator
45
+ IDENT = /\A[a-z_][A-Za-z0-9_]*\z/
46
+ QUOTES = ['"', "'"].freeze
47
+ # Mutating an argument to a universal-equality method is always an
48
+ # equivalent mutant: Ruby's `==` / `<=>` family returns false / nil on a
49
+ # type mismatch rather than raising, so the engine exempts them
50
+ # (`UNIVERSAL_EQUALITY_METHODS`). Skip them to keep survivors meaningful.
51
+ UNIVERSAL_EQUALITY = %w[== != eql? equal? <=>].freeze
52
+
53
+ # Every operator the mutator knows. Each maps to the diagnostic rule
54
+ # family it is *engineered* to trip when the mutated value/call sits in a
55
+ # context where Rigor has type knowledge.
56
+ ALL_OPERATORS = %i[nil_inject type_swap undefined_method arity_extra].freeze
57
+
58
+ # The default set. `arity_extra` is excluded: most Ruby methods accept an
59
+ # extra argument (splat / optional), so appending one is usually an
60
+ # equivalent mutant — it contributes almost only noise. Re-enable it
61
+ # explicitly via `operators:` to measure arity teeth. (A signature-arity
62
+ # guard would make it default-worthy — a follow-up.)
63
+ OPERATORS = %i[nil_inject type_swap undefined_method].freeze
64
+
65
+ def initialize(source, operators: OPERATORS)
66
+ @source = source
67
+ @operators = operators
68
+ @parse = Prism.parse(source)
69
+ @anchor_for = {} # literal node -> its enclosing call's receiver node (or nil)
70
+ end
71
+
72
+ def mutations
73
+ return [] unless @parse.success?
74
+
75
+ index_literal_anchors(@parse.value)
76
+ out = []
77
+ walk(@parse.value) { |node| collect(node, out) }
78
+ out
79
+ end
80
+
81
+ # Phase 1.5 — keep only mutations whose anchor types to a concrete,
82
+ # non-Dynamic type, i.e. a site where Rigor actually holds a contract the
83
+ # mutation could violate. Drops implicit-self calls and literals outside a
84
+ # typed call (no contract → guaranteed survival → noise). FP-safe
85
+ # direction: an unresolved/probe-failed type KEEPS the mutation, so the
86
+ # filter never hides a kill it is unsure about — it only removes
87
+ # provably-Dynamic sites. Returns [kept, dropped_count]. Builds the scope
88
+ # index from THIS mutator's parse so anchor node identity matches the keys.
89
+ def filter_by_type(mutations, environment:, path:)
90
+ base = Rigor::Scope.empty(environment: environment, source_path: path)
91
+ index = Rigor::Inference::ScopeIndexer.index(@parse.value, default_scope: base)
92
+ cache = {}
93
+ kept = mutations.select do |mut|
94
+ keep, type = anchor_decision(mut.anchor, index, cache)
95
+ mut.anchor_type = type if keep
96
+ keep
97
+ end
98
+ [kept, mutations.size - kept.size]
99
+ end
100
+
101
+ private
102
+
103
+ def walk(node, &blk)
104
+ return if node.nil?
105
+
106
+ blk.call(node)
107
+ node.compact_child_nodes.each { |child| walk(child, &blk) }
108
+ end
109
+
110
+ def collect(node, out)
111
+ case node
112
+ when Prism::IntegerNode, Prism::FloatNode
113
+ literal_mutations(node, out, numeric: true)
114
+ when Prism::StringNode
115
+ literal_mutations(node, out, numeric: false)
116
+ when Prism::CallNode
117
+ call_mutations(node, out)
118
+ end
119
+ end
120
+
121
+ # Record, for each literal that is a direct call argument, the receiver of
122
+ # the enclosing call — the anchor whose param contract a literal mutation
123
+ # could violate. Literals elsewhere get a nil anchor (filtered out under
124
+ # the type filter).
125
+ def index_literal_anchors(node)
126
+ return if node.nil?
127
+
128
+ if node.is_a?(Prism::CallNode) && node.arguments
129
+ node.arguments.arguments.each do |arg|
130
+ @anchor_for[arg] = [node.receiver, node.name.to_s] if literal?(arg)
131
+ end
132
+ end
133
+ node.compact_child_nodes.each { |child| index_literal_anchors(child) }
134
+ end
135
+
136
+ def literal?(node)
137
+ node.is_a?(Prism::IntegerNode) || node.is_a?(Prism::FloatNode) || node.is_a?(Prism::StringNode)
138
+ end
139
+
140
+ # Returns [keep?, rendered_type]. Keep when `anchor` is a site where Rigor
141
+ # holds a concrete (non-Dynamic/Top) type. FP-safe: an unresolved or
142
+ # probe-failed type keeps the mutation (with a nil rendered type).
143
+ def anchor_decision(anchor, index, cache)
144
+ return [false, nil] if anchor.nil?
145
+ return cache[anchor] if cache.key?(anchor)
146
+
147
+ cache[anchor] = compute_anchor_decision(anchor, index)
148
+ end
149
+
150
+ def compute_anchor_decision(anchor, index)
151
+ scope = index[anchor]
152
+ return [true, nil] if scope.nil? # unresolved scope → keep (FP-safe)
153
+
154
+ type = scope.type_of(anchor)
155
+ return [true, nil] if type.nil?
156
+
157
+ concrete = !non_concrete_type?(type)
158
+ [concrete, concrete ? render_type(type) : nil]
159
+ rescue StandardError
160
+ [true, nil] # never let a probe failure hide a candidate
161
+ end
162
+
163
+ # A receiver type Rigor cannot bite on, so a mutation anchored to it would
164
+ # survive as noise: `Dynamic` / `Top` / `bot`, or a union with any such arm
165
+ # (gradually valid — `Array | Dynamic[top]`.whatever never fires). A union
166
+ # of fully-concrete arms (`String | Symbol`) stays concrete — it now has
167
+ # undefined-method teeth.
168
+ def non_concrete_type?(type)
169
+ return true if type.is_a?(Rigor::Type::Dynamic) || type.is_a?(Rigor::Type::Top) ||
170
+ type.is_a?(Rigor::Type::Bot)
171
+ return type.members.any? { |member| non_concrete_type?(member) } if type.is_a?(Rigor::Type::Union)
172
+
173
+ false
174
+ end
175
+
176
+ def render_type(type)
177
+ type.respond_to?(:describe) ? type.describe(:short) : type.to_s
178
+ rescue StandardError
179
+ type.class.name
180
+ end
181
+
182
+ # Mutate a literal: drop it to nil (possible-nil channel) and swap its
183
+ # type (type-mismatch channel). String literals are only touched when the
184
+ # node is a real quoted string, so we never corrupt `%w[...]` words.
185
+ def literal_mutations(node, out, numeric:)
186
+ return if !numeric && !QUOTES.include?(node.opening_loc&.slice)
187
+
188
+ anchor, method = @anchor_for[node]
189
+ return if UNIVERSAL_EQUALITY.include?(method)
190
+
191
+ loc = node.location
192
+ add(out, :nil_inject, "call.argument-type-mismatch", loc.start_offset, loc.end_offset,
193
+ "nil", loc.start_line, "literal → nil (#{snippet(loc)})", anchor, method)
194
+ swap = numeric ? '"rigor_mutant"' : "0"
195
+ add(out, :type_swap, "call.argument-type-mismatch", loc.start_offset, loc.end_offset,
196
+ swap, loc.start_line, "literal type swap (#{snippet(loc)} → #{swap})", anchor, method)
197
+ end
198
+
199
+ def call_mutations(node, out)
200
+ rename_call(node, out)
201
+ extend_arity(node, out)
202
+ end
203
+
204
+ # Rename the *call site* (not the def) to a method that cannot exist, so a
205
+ # typed receiver trips call.undefined-method. We leave `def` signatures
206
+ # untouched on purpose: the prebuilt ProjectScan still carries the file's
207
+ # original declarations, so mutating only bodies/call-sites keeps it valid.
208
+ # Anchor is the explicit receiver (nil ⇒ implicit self ⇒ filtered out, as
209
+ # call.self-undefined-method ships `:off`).
210
+ def rename_call(node, out)
211
+ name = node.name.to_s
212
+ mloc = node.message_loc
213
+ return unless mloc && IDENT.match?(name)
214
+
215
+ add(out, :undefined_method, "call.undefined-method", mloc.start_offset, mloc.end_offset,
216
+ "#{name}__rigor_absent", mloc.start_line, "call ##{name} → missing method", node.receiver, name)
217
+ end
218
+
219
+ # Append a trailing argument inside explicit `(...)` parens to trip an
220
+ # arity diagnostic against a known fixed-arity signature.
221
+ def extend_arity(node, out)
222
+ open = node.opening_loc
223
+ close = node.closing_loc
224
+ return unless close && open&.slice == "("
225
+
226
+ args = node.arguments&.arguments
227
+ insertion = args && !args.empty? ? ", nil" : "nil"
228
+ add(out, :arity_extra, "call.wrong-arity", close.start_offset, close.start_offset,
229
+ insertion, node.location.start_line, "call ##{node.name} +1 arg", node.receiver, node.name.to_s)
230
+ end
231
+
232
+ def add(out, operator, rule, start, stop, replacement, line, label, anchor, method_name) # rubocop:disable Metrics/ParameterLists
233
+ return unless @operators.include?(operator)
234
+
235
+ out << Mutation.new(operator: operator, expected_rule: rule, start: start, stop: stop,
236
+ replacement: replacement, line: line, label: label, anchor: anchor,
237
+ method_name: method_name)
238
+ end
239
+
240
+ def snippet(loc)
241
+ text = loc.slice.gsub(/\s+/, " ")
242
+ text.length > 30 ? "#{text[0, 27]}..." : text
243
+ end
244
+ end
245
+ end
246
+ end
@@ -7,43 +7,30 @@ require_relative "rbs_extended/reporter"
7
7
  require_relative "rbs_extended/hkt_directives"
8
8
 
9
9
  module Rigor
10
- # Slice 7 phase 15 first-preview reader for the
11
- # `RBS::Extended` annotation surface described in
10
+ # Reader for the `RBS::Extended` annotation surface described in
12
11
  # `docs/type-specification/rbs-extended.md`.
13
12
  #
14
- # This module reads `%a{rigor:v1:<directive> <payload>}`
15
- # annotations off RBS method definitions and returns
16
- # well-typed effect objects the inference engine can
17
- # consume. v0.0.2 recognises:
13
+ # Reads `%a{rigor:v1:<directive> <payload>}` annotations off RBS
14
+ # method definitions and returns well-typed effect objects the
15
+ # inference engine can consume. Implemented directives:
18
16
  #
19
- # - `rigor:v1:predicate-if-true <target> is <ClassName>`
20
- # - `rigor:v1:predicate-if-false <target> is <ClassName>`
21
- # - `rigor:v1:assert <target> is <ClassName>`
22
- # - `rigor:v1:assert-if-true <target> is <ClassName>`
23
- # - `rigor:v1:assert-if-false <target> is <ClassName>`
17
+ # - `rigor:v1:predicate-if-true <target> is <ClassName|refinement>`
18
+ # - `rigor:v1:predicate-if-false <target> is <ClassName|refinement>`
19
+ # - `rigor:v1:assert <target> is <ClassName|refinement>`
20
+ # - `rigor:v1:assert-if-true <target> is <ClassName|refinement>`
21
+ # - `rigor:v1:assert-if-false <target> is <ClassName|refinement>`
22
+ # - `rigor:v1:param <name> <type-expr>` — per-call param narrowing
23
+ # - `rigor:v1:return <type-expr>` — per-call return override
24
+ # - `rigor:v1:conforms-to <InterfaceName>` — structural conformance
24
25
  #
25
- # `predicate-if-*` fires when the call is used as an
26
- # `if` / `unless` condition; `assert` fires unconditionally
27
- # at the call's post-scope; `assert-if-true` /
28
- # `assert-if-false` fire at the post-scope only when the
29
- # call's return value can be observed as truthy / falsey
30
- # (currently: when the call is the predicate of a
31
- # subsequent `if` / `unless`). Other directives in the spec
32
- # (`param`, `return`, `conforms-to`, negation `~T`,
33
- # `target: self` narrowing, ...) remain on the v0.0.x
34
- # roadmap. Annotations whose key is in the `rigor:v1:`
35
- # namespace but whose directive is unrecognised are
36
- # silently ignored at first-preview quality (a future slice
37
- # MAY surface them as diagnostics-on-Rigor-itself per the
38
- # spec's "unsupported metadata" guidance).
39
- #
40
- # The parser is minimal: it accepts a strict shape
41
- # `<target> is <ClassName>` where `<target>` is a Ruby
42
- # identifier (parameter name) or `self`, and `<ClassName>`
43
- # is a single non-namespaced class identifier or a
44
- # `::Foo::Bar` style constant path. Negative refinements
45
- # (`~T`), intersections, and unions are deferred to the
46
- # next iteration.
26
+ # `predicate-if-*` fires when the call is used as an `if` / `unless`
27
+ # condition; `assert` fires unconditionally at the call's post-scope;
28
+ # `assert-if-true` / `assert-if-false` fire at the post-scope only
29
+ # when the call's return value can be observed as truthy / falsey.
30
+ # Negation (`~T`) is supported for both class-name and refinement
31
+ # right-hand sides. Parameterised refinements (`non-empty-array[T]`)
32
+ # are also recognised. Annotations whose directive is unrecognised
33
+ # are silently ignored per the spec's "unsupported metadata" guidance.
47
34
  module RbsExtended # rubocop:disable Metrics/ModuleLength
48
35
  DIRECTIVE_PREFIX = "rigor:v1:"
49
36
 
@@ -58,9 +45,10 @@ module Rigor
58
45
  # a kebab-case refinement name (`non-empty-string`,
59
46
  # `lowercase-string`, …) instead of a Capitalised class
60
47
  # name. The narrowing tier substitutes the carrier for the
61
- # current local type; `class_name` is then nil and
62
- # `negative` is false (refinement-form directives do not
63
- # support `~T` negation in v0.0.4).
48
+ # current local type; `class_name` is then nil. `negative`
49
+ # may be true for refinement-form directives `~T` negation
50
+ # is supported; the narrowing tier computes the complement
51
+ # decomposition (see `AssertEffect` docs below).
64
52
  class PredicateEffect < Data.define(:edge, :target_kind, :target_name, :class_name, :negative, :refinement_type)
65
53
  def truthy_only? = edge == :truthy_only
66
54
  def falsey_only? = edge == :falsey_only
@@ -17,11 +17,8 @@ module Rigor
17
17
  # classes / modules, in-source constants, discovered method
18
18
  # nodes, class ivar / cvar declarations).
19
19
  #
20
- # This module is the **stable read shape** that v0.1.0's plugin
21
- # API will be designed against. ADR-2 (`docs/adr/2-extension-api.md`)
22
- # calls out a unified reflection layer as a prerequisite for the
23
- # extension protocols, and `docs/design/20260505-v0.1.0-readiness.md`
24
- # nominates this module as the highest-leverage cold-start slice.
20
+ # This module is the **stable read shape** the plugin API is
21
+ # designed against (ADR-2, `docs/adr/2-extension-api.md`).
25
22
  #
26
23
  # The facade is **read-only and additive**. Existing call sites
27
24
  # that read directly from `Rigor::Scope` or
@@ -52,8 +49,8 @@ module Rigor
52
49
  # defined in the analyzed sources?
53
50
  #
54
51
  # The provenance side of the API (which source family contributed
55
- # each fact) is explicitly out of scope for the v0.0.7 first
56
- # pass. v0.1.0's plugin API adds it as a separate concern.
52
+ # each fact) is explicitly out of scope for the v0.0.7 first pass;
53
+ # v0.1.0's plugin API added it as a separate concern.
57
54
  module Reflection
58
55
  module_function
59
56
 
@@ -22,12 +22,15 @@ module Rigor
22
22
  :in_source_constants,
23
23
  :discovered_methods,
24
24
  :discovered_def_nodes,
25
+ :discovered_singleton_def_nodes,
25
26
  :discovered_def_sources,
26
27
  :discovered_method_visibilities,
27
28
  :discovered_superclasses,
28
29
  :discovered_includes,
29
30
  :discovered_class_sources,
30
- :data_member_layouts
31
+ :data_member_layouts,
32
+ :struct_member_layouts,
33
+ :param_inferred_types
31
34
  )
32
35
 
33
36
  class DiscoveryIndex
@@ -46,12 +49,23 @@ module Rigor
46
49
  in_source_constants: EMPTY_TABLE,
47
50
  discovered_methods: EMPTY_TABLE,
48
51
  discovered_def_nodes: EMPTY_TABLE,
52
+ discovered_singleton_def_nodes: EMPTY_TABLE,
49
53
  discovered_def_sources: EMPTY_TABLE,
50
54
  discovered_method_visibilities: EMPTY_TABLE,
51
55
  discovered_superclasses: EMPTY_TABLE,
52
56
  discovered_includes: EMPTY_TABLE,
53
57
  discovered_class_sources: EMPTY_TABLE,
54
- data_member_layouts: EMPTY_TABLE
58
+ data_member_layouts: EMPTY_TABLE,
59
+ struct_member_layouts: EMPTY_TABLE,
60
+ # ADR-67 WD3 — the call-site parameter-inference table, keyed by
61
+ # `[class_name, method_name, kind]` (the same `(class, method, kind)`
62
+ # triple {Inference::ParameterInferenceCollector} records and that
63
+ # `build_method_entry_scope` reconstructs from the lexical class path).
64
+ # The value is a `{param_name(Symbol) => Rigor::Type}` map of the union
65
+ # of resolved call-site argument types. Empty on every normal run; only
66
+ # the `coverage --protection` collection pass populates it today, so a
67
+ # `check` run leaves it empty and seeds nothing (byte-identical).
68
+ param_inferred_types: EMPTY_TABLE
55
69
  )
56
70
  end
57
71
  end