rigortype 0.1.19 → 0.2.1

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 (197) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -6
  3. data/data/core_overlay/numeric.rbs +33 -0
  4. data/data/core_overlay/pathname.rbs +25 -0
  5. data/data/core_overlay/string_scanner.rbs +28 -0
  6. data/data/gem_overlay/activesupport/core_ext.rbs +473 -0
  7. data/data/vendored_gem_sigs/ast/ast.rbs +130 -0
  8. data/data/vendored_gem_sigs/bcrypt/bcrypt.rbs +47 -0
  9. data/data/vendored_gem_sigs/bundler/bundler.rbs +238 -0
  10. data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +34 -0
  11. data/data/vendored_gem_sigs/did_you_mean/did_you_mean_extras.rbs +34 -0
  12. data/data/vendored_gem_sigs/idn-ruby/idn.rbs +54 -0
  13. data/data/vendored_gem_sigs/mysql2/client.rbs +55 -0
  14. data/data/vendored_gem_sigs/mysql2/error.rbs +5 -0
  15. data/data/vendored_gem_sigs/mysql2/result.rbs +31 -0
  16. data/data/vendored_gem_sigs/mysql2/statement.rbs +5 -0
  17. data/data/vendored_gem_sigs/nokogiri/nokogiri.rbs +2332 -0
  18. data/data/vendored_gem_sigs/nokogiri/nokogiri_html5.rbs +47 -0
  19. data/data/vendored_gem_sigs/pg/pg.rbs +212 -0
  20. data/data/vendored_gem_sigs/prism/prism_supplement.rbs +44 -0
  21. data/data/vendored_gem_sigs/redis/errors.rbs +50 -0
  22. data/data/vendored_gem_sigs/redis/future.rbs +5 -0
  23. data/data/vendored_gem_sigs/redis/redis.rbs +348 -0
  24. data/data/vendored_gem_sigs/redis/redis_extras.rbs +130 -0
  25. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +226 -0
  26. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -23
  27. data/lib/rigor/analysis/check_rules/rule_walk.rb +3 -21
  28. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
  29. data/lib/rigor/analysis/check_rules.rb +492 -71
  30. data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
  31. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
  32. data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
  33. data/lib/rigor/analysis/fact_store.rb +5 -4
  34. data/lib/rigor/analysis/rule_catalog.rb +153 -6
  35. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +17 -17
  36. data/lib/rigor/analysis/runner/project_pre_passes.rb +9 -8
  37. data/lib/rigor/analysis/runner.rb +17 -6
  38. data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
  39. data/lib/rigor/analysis/worker_session.rb +10 -14
  40. data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
  41. data/lib/rigor/cache/store.rb +5 -3
  42. data/lib/rigor/cli/annotate_command.rb +28 -7
  43. data/lib/rigor/cli/baseline_command.rb +4 -3
  44. data/lib/rigor/cli/check_command.rb +138 -16
  45. data/lib/rigor/cli/coverage_command.rb +138 -31
  46. data/lib/rigor/cli/coverage_mutation.rb +149 -0
  47. data/lib/rigor/cli/coverage_scan.rb +57 -0
  48. data/lib/rigor/cli/explain_command.rb +2 -0
  49. data/lib/rigor/cli/fused_protection_renderer.rb +67 -0
  50. data/lib/rigor/cli/fused_protection_report.rb +76 -0
  51. data/lib/rigor/cli/lsp_command.rb +3 -7
  52. data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
  53. data/lib/rigor/cli/mutation_protection_report.rb +73 -0
  54. data/lib/rigor/cli/options.rb +9 -0
  55. data/lib/rigor/cli/plugins_command.rb +2 -1
  56. data/lib/rigor/cli/protection_renderer.rb +63 -0
  57. data/lib/rigor/cli/protection_report.rb +68 -0
  58. data/lib/rigor/cli/sig_gen_command.rb +2 -1
  59. data/lib/rigor/cli/trace_command.rb +2 -1
  60. data/lib/rigor/cli/triage_command.rb +2 -1
  61. data/lib/rigor/cli/type_of_command.rb +1 -1
  62. data/lib/rigor/cli/type_scan_command.rb +2 -1
  63. data/lib/rigor/cli.rb +3 -2
  64. data/lib/rigor/config_audit.rb +152 -0
  65. data/lib/rigor/configuration/dependencies.rb +2 -4
  66. data/lib/rigor/configuration.rb +57 -7
  67. data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
  68. data/lib/rigor/environment/class_registry.rb +4 -3
  69. data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
  70. data/lib/rigor/environment/lockfile_resolver.rb +1 -1
  71. data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
  72. data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
  73. data/lib/rigor/environment/rbs_loader.rb +76 -5
  74. data/lib/rigor/environment.rb +66 -8
  75. data/lib/rigor/flow_contribution/fact.rb +1 -1
  76. data/lib/rigor/flow_contribution.rb +3 -5
  77. data/lib/rigor/inference/acceptance.rb +17 -9
  78. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  79. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
  80. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
  81. data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
  82. data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
  83. data/lib/rigor/inference/expression_typer.rb +20 -28
  84. data/lib/rigor/inference/hkt_body.rb +8 -11
  85. data/lib/rigor/inference/hkt_body_parser.rb +10 -12
  86. data/lib/rigor/inference/hkt_registry.rb +10 -11
  87. data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
  88. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +169 -24
  89. data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
  90. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
  91. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
  92. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
  93. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
  94. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
  95. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
  96. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +90 -15
  97. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
  98. data/lib/rigor/inference/method_dispatcher.rb +40 -48
  99. data/lib/rigor/inference/mutation_widening.rb +5 -11
  100. data/lib/rigor/inference/narrowing.rb +14 -16
  101. data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
  102. data/lib/rigor/inference/project_patched_methods.rb +4 -7
  103. data/lib/rigor/inference/project_patched_scanner.rb +2 -13
  104. data/lib/rigor/inference/protection_scanner.rb +86 -0
  105. data/lib/rigor/inference/scope_indexer.rb +129 -55
  106. data/lib/rigor/inference/statement_evaluator.rb +271 -114
  107. data/lib/rigor/inference/struct_fold_safety.rb +181 -0
  108. data/lib/rigor/inference/synthetic_method.rb +7 -7
  109. data/lib/rigor/language_server/completion_provider.rb +6 -12
  110. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
  111. data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
  112. data/lib/rigor/language_server/hover_provider.rb +2 -3
  113. data/lib/rigor/language_server/hover_renderer.rb +2 -11
  114. data/lib/rigor/language_server/server.rb +9 -17
  115. data/lib/rigor/language_server.rb +4 -5
  116. data/lib/rigor/plugin/base.rb +10 -8
  117. data/lib/rigor/plugin/macro/block_as_method.rb +3 -4
  118. data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
  119. data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
  120. data/lib/rigor/plugin/macro.rb +4 -5
  121. data/lib/rigor/plugin/manifest.rb +45 -66
  122. data/lib/rigor/plugin/registry.rb +6 -7
  123. data/lib/rigor/plugin/type_node_resolver.rb +6 -8
  124. data/lib/rigor/protection/diagnostic_oracle.rb +51 -0
  125. data/lib/rigor/protection/mutation_scanner.rb +180 -0
  126. data/lib/rigor/protection/mutator.rb +267 -0
  127. data/lib/rigor/protection/test_suite_oracle.rb +68 -0
  128. data/lib/rigor/rbs_extended.rb +24 -36
  129. data/lib/rigor/reflection.rb +4 -7
  130. data/lib/rigor/scope/discovery_index.rb +14 -2
  131. data/lib/rigor/scope.rb +54 -11
  132. data/lib/rigor/sig_gen/observed_call.rb +3 -3
  133. data/lib/rigor/sig_gen/writer.rb +40 -2
  134. data/lib/rigor/signature_path_audit.rb +92 -0
  135. data/lib/rigor/source/constant_path.rb +62 -0
  136. data/lib/rigor/source.rb +1 -0
  137. data/lib/rigor/type/bound_method.rb +2 -11
  138. data/lib/rigor/type/combinator.rb +16 -3
  139. data/lib/rigor/type/constant.rb +2 -11
  140. data/lib/rigor/type/data_class.rb +2 -11
  141. data/lib/rigor/type/data_instance.rb +2 -11
  142. data/lib/rigor/type/hash_shape.rb +2 -11
  143. data/lib/rigor/type/integer_range.rb +2 -11
  144. data/lib/rigor/type/intersection.rb +2 -11
  145. data/lib/rigor/type/nominal.rb +2 -11
  146. data/lib/rigor/type/plain_lattice.rb +37 -0
  147. data/lib/rigor/type/refined.rb +72 -13
  148. data/lib/rigor/type/singleton.rb +2 -11
  149. data/lib/rigor/type/struct_class.rb +75 -0
  150. data/lib/rigor/type/struct_instance.rb +93 -0
  151. data/lib/rigor/type/tuple.rb +5 -15
  152. data/lib/rigor/type.rb +2 -0
  153. data/lib/rigor/version.rb +1 -1
  154. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  155. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
  156. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +3 -3
  157. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
  158. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
  159. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +7 -10
  160. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
  161. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
  162. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +6 -8
  163. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
  164. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -2
  165. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
  166. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
  167. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
  168. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
  169. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
  170. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
  171. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
  172. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +7 -9
  173. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
  174. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
  175. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +3 -3
  176. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
  177. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
  178. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
  179. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +1 -1
  180. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
  181. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
  182. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +5 -5
  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 +19 -14
  187. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
  188. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
  189. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
  190. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
  191. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
  192. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
  193. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
  194. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +28 -41
  195. data/sig/rigor/scope.rbs +9 -1
  196. data/sig/rigor/type.rbs +36 -1
  197. metadata +49 -1
@@ -23,8 +23,8 @@ module Rigor
23
23
  # - `factory :users, aliases: [:author] do ... end` — alias form
24
24
  #
25
25
  # Inside a factory block, attribute declarations come in
26
- # several shapes. Phase 1 (a) recognises the literal-name
27
- # forms only (Symbol arg / String arg):
26
+ # several shapes. Only literal-name forms are recognised
27
+ # (Symbol arg / String arg):
28
28
  #
29
29
  # - `name { "Alice" }` — implicit attribute via
30
30
  # `method_missing` with a block (FactoryBot's modern
@@ -116,8 +116,8 @@ module Rigor
116
116
  Rigor::Source::Literals.symbol_or_string_name(call_node.arguments&.arguments&.first)
117
117
  end
118
118
 
119
- # Pillar 2 Slice 3 — resolve the model class name for
120
- # the factory. Three sources, in priority order:
119
+ # Resolves the model class name for the factory.
120
+ # Three sources, in priority order:
121
121
  #
122
122
  # 1. Explicit `class: <Const>` keyword arg —
123
123
  # ConstantReadNode / ConstantPathNode value.
@@ -188,11 +188,10 @@ module Rigor
188
188
  attributes
189
189
  end
190
190
 
191
- # Walks the block body collecting attribute names. The
192
- # recogniser looks at top-level statements only
193
- # attributes inside `trait :admin do ... end` or other
194
- # nested blocks are NOT collected in Phase 1 (a)
195
- # (traits ship in a follow-up).
191
+ # Walks the block body collecting attribute names. Only
192
+ # top-level statements are examined attributes inside
193
+ # `trait :admin do ... end` or other nested blocks are
194
+ # not collected (traits deferred to a follow-up).
196
195
  def collect_attributes_from(node, accumulator)
197
196
  return unless node.is_a?(Prism::Node)
198
197
 
@@ -206,8 +205,7 @@ module Rigor
206
205
  def record_attribute(node, accumulator)
207
206
  return unless node.is_a?(Prism::CallNode) && node.receiver.nil?
208
207
  # Skip association / sequence / trait / framework
209
- # methods — Phase 1 (a) only records plain attribute
210
- # declarations.
208
+ # methods — only plain attribute declarations are recorded.
211
209
  return if SKIPPED_METHODS.include?(node.name)
212
210
 
213
211
  name = if node.name == :add_attribute
@@ -4,15 +4,14 @@ module Rigor
4
4
  module Plugin
5
5
  class Factorybot < Rigor::Plugin::Base
6
6
  # Per-run frozen index of discovered FactoryBot factories
7
- # and the attribute keys each declares. Phase 1 (a) keys
8
- # only the **literal symbol/string** factory name + the
9
- # **literal symbol** attribute names; sequences,
10
- # parent/child relationships, traits, and dynamically-
11
- # named factories ship behind later slices.
7
+ # and the attribute keys each declares. Indexes only
8
+ # **literal symbol/string** factory names + **literal
9
+ # symbol** attribute names; sequences, parent/child
10
+ # relationships, traits, and dynamically-named factories
11
+ # are deferred to follow-up slices.
12
12
  #
13
- # v0.2.0 (Pillar 2 Slice 3) adds `model_class` to each
14
- # entry the inferred or explicit class the factory
15
- # builds. Resolved from:
13
+ # Each entry carries a `model_class` the inferred or
14
+ # explicit class the factory builds. Resolved from:
16
15
  #
17
16
  # 1. An explicit `factory :user, class: User do`
18
17
  # keyword option (ConstantReadNode / ConstantPathNode
@@ -13,12 +13,10 @@ module Rigor
13
13
  # attributes_for / *_list family against a per-run index
14
14
  # built from `factory_search_paths`.
15
15
  #
16
- # **Phase 1 (a)** of the FactoryBot plugin family — the
17
- # self-contained slice. Recognises factory NAMES + literal
18
- # ATTRIBUTE KEYS in the call's keyword hash. Phase 1 (c)
19
- # ships the AR column cross-check via the
20
- # `rigor-activerecord` `:model_index` ADR-9 fact, after
21
- # `rigor-activerecord` adds the matching publish hook.
16
+ # Recognises factory NAMES + literal ATTRIBUTE KEYS in
17
+ # the call's keyword hash. The AR column cross-check uses
18
+ # the `rigor-activerecord` `:model_index` ADR-9 fact when
19
+ # that plugin is loaded (optional `consumes:`).
22
20
  # Traits, sequences, parent / child factories, and dynamic
23
21
  # factory names are deferred to follow-up slices.
24
22
  #
@@ -54,9 +52,9 @@ module Rigor
54
52
  # `.build_stubbed_list`. The legacy `FactoryGirl` constant
55
53
  # is recognised identically. Implicit-receiver calls
56
54
  # (`create(:name)` inside an `include FactoryBot::Syntax::Methods`
57
- # context) are NOT recognised in Phase 1 (a) too many
58
- # false positives on plain `create` calls outside test
59
- # files; this needs receiver-type inference (Phase 1 (b)).
55
+ # context) are NOT recognised too many false positives on
56
+ # plain `create` calls outside test files; deferred until
57
+ # receiver-type inference can disambiguate.
60
58
  #
61
59
  # ## What's recognised inside `factory :name do ... end`
62
60
  #
@@ -50,12 +50,13 @@ module Rigor
50
50
 
51
51
  # @param paths [Array<String>] absolute paths to `.rb` files
52
52
  # the project's `paths:` resolves to.
53
- # @return [Hash{Symbol => Hash}] frozen 3-key result with
53
+ # @return [Hash{Symbol => Hash}] frozen 4-key result:
54
54
  # `:types` (per-`Schema::Object` field table),
55
- # `:enums` (per-`Schema::Enum` value list), and
56
- # `:input_objects` (per-`Schema::InputObject` argument
57
- # table). Any subset may be empty when no recognisable
58
- # declaration of that kind is found.
55
+ # `:enums` (per-`Schema::Enum` value list),
56
+ # `:input_objects` (per-`Schema::InputObject` argument table),
57
+ # `:mutations` (per-`Schema::Mutation` arguments+fields table).
58
+ # Any subset may be empty when no recognisable declaration
59
+ # of that kind is found.
59
60
  def scan(paths:)
60
61
  acc = empty_accumulator
61
62
  paths.each do |path|
@@ -97,10 +98,9 @@ module Rigor
97
98
  private_class_method :scan_file
98
99
 
99
100
  # Walks the AST collecting `class X < GraphQL::Schema::Object`,
100
- # `class X < GraphQL::Schema::Enum`, and
101
- # `class X < GraphQL::Schema::InputObject` decls at any
102
- # nesting level. Returns a 3-key hash so the caller can
103
- # publish multiple cross-plugin facts from one walk.
101
+ # `Schema::Enum`, `Schema::InputObject`, and `Schema::Mutation`
102
+ # decls at any nesting level. Returns a 4-key hash so the
103
+ # caller can publish multiple cross-plugin facts from one walk.
104
104
  def collect_definitions(node, qualified_prefix)
105
105
  return empty_accumulator if node.nil?
106
106
 
@@ -203,10 +203,9 @@ module Rigor
203
203
  # The first positional must be a String literal — the
204
204
  # graphql-ruby `value` API also accepts a Symbol form
205
205
  # (`value :ACTIVE`) but the documented idiom is String.
206
- # Slice 2b only stores the GraphQL-side value name; the
207
- # optional `value:` kwarg (Ruby-side override) and
208
- # `description:` stay out of the published table for
209
- # the floor.
206
+ # Only the GraphQL-side value name is stored; the optional
207
+ # `value:` kwarg (Ruby-side override) and `description:`
208
+ # are omitted from the published table.
210
209
  def collect_values(body)
211
210
  return [] if body.nil?
212
211
 
@@ -25,10 +25,10 @@ module Rigor
25
25
  # resolver methods themselves; rigor's value here is producing a
26
26
  # static type table downstream consumers can cross-reference.
27
27
  #
28
- # ## What downstream consumers DO with `:graphql_type_table`
28
+ # ## What downstream consumers DO with the published facts
29
29
  #
30
- # The fact is the substrate for two future capabilities (both
31
- # demand-driven, NOT in slice 1):
30
+ # The tables are the substrate for two future capabilities
31
+ # (demand-driven, not yet implemented):
32
32
  #
33
33
  # - Resolver-method check: for each `field :name, Type` whose
34
34
  # `name` is also defined as a Ruby method on the class, verify
@@ -37,33 +37,25 @@ module Rigor
37
37
  # plugin could type `Schema.execute(query).to_h` against the
38
38
  # queried fields.
39
39
  #
40
- # ## Floor / ceiling (slice 1)
40
+ # ## What's recognised
41
41
  #
42
- # Slice 1 ships the **floor**:
42
+ # - `class T < GraphQL::Schema::Object` subclasses (including
43
+ # nested namespaces); `field :name, Type, null: ...`
44
+ # declarations with constant-reference or list-array types
45
+ # and GraphQL→Ruby scalar mapping.
46
+ # - `class T < GraphQL::Schema::Enum`; `value "ACTIVE"` calls.
47
+ # - `class T < GraphQL::Schema::InputObject` /
48
+ # `GraphQL::Schema::Mutation`; `argument :name, Type,
49
+ # required: ...` declarations.
50
+ # - No user-facing diagnostics yet.
43
51
  #
44
- # - Recognises `class T < GraphQL::Schema::Object` subclasses
45
- # (including nested namespaces: `class Types::User < ...`,
46
- # `module Types; class User < ...; end; end`).
47
- # - Recognises the `field :name, Type, **opts` declaration with:
48
- # - `Type` as a `ConstantReadNode` / `ConstantPathNode` (`String`
49
- # / `Integer` / `Boolean` / `Float` / `ID`, or a user-defined
50
- # `Types::OtherObject`).
51
- # - `null: true` / `null: false` keyword extracts nullability.
52
- # - Maps the canonical GraphQL scalar names to underlying Ruby
53
- # classes (`String` → `String`, `Integer` → `Integer`,
54
- # `Boolean` → `TrueClass`, `Float` → `Float`, `ID` → `String`).
55
- # - Publishes the table; no user-facing diagnostics yet.
52
+ # ## Deferred (demand-driven)
56
53
  #
57
- # The **ceiling** (future slices, demand-driven):
58
- #
59
- # - **`GraphQL::Schema::Enum`** with `value "ACTIVE"` calls.
60
- # - **`GraphQL::Schema::Mutation`** + **`GraphQL::Schema::InputObject`**.
61
- # - **List / Non-Null wrappers** (`[String]`, `String.array`).
62
54
  # - **`resolver:` / `mutation:` reroute** recognition.
63
55
  # - **String type expressions** (`field :foo, "User"`) — defeats
64
56
  # static resolution by design (graphql-ruby's `BuildType.parse_type`
65
57
  # constantizes at runtime); a future slice could surface these
66
- # as `graphql.string-type` `:info` diagnostics that point the
58
+ # as `graphql.string-type` `:info` diagnostics pointing the
67
59
  # user at the constant-reference form for static typing.
68
60
  class Graphql < Rigor::Plugin::Base
69
61
  manifest(
@@ -71,9 +71,9 @@ module Rigor
71
71
  # - `is_a?(Result::Ok)` / `Some` / `None` exhaustive
72
72
  # narrowing — core control-flow analysis over a sealed
73
73
  # hierarchy, not a plugin surface.
74
- # - The `variants do variant Const, Type end` Enum DSL needs
75
- # an ADR-16 nested-class emission tier (ADR-36). Today's
76
- # contract has no `const_set`-emitting macro substrate.
74
+ # - The `variants do variant Const, Type end` Enum DSL is handled
75
+ # via ADR-36 `nested_class_templates:` in this plugin's manifest
76
+ # (Slice A see `nested_class_templates:` block below).
77
77
  class Mangrove < Rigor::Plugin::Base
78
78
  manifest(
79
79
  id: "mangrove",
@@ -30,10 +30,10 @@ module Rigor
30
30
  #
31
31
  # ## Configuration
32
32
  #
33
- # No knobs in v0.1.0. Activate via `plugins: ["rigor-minitest"]`
34
- # in `.rigor.yml`.
33
+ # No configuration knobs. Activate via
34
+ # `plugins: ["rigor-minitest"]` in `.rigor.yml`.
35
35
  #
36
- # ## Limitations (v0.1.0)
36
+ # ## Limitations
37
37
  #
38
38
  # - **No `assert_raises(T) { ... }`** — that's a block-shape
39
39
  # matcher and Rigor's narrowing model is for
@@ -169,7 +169,7 @@ module Rigor
169
169
 
170
170
  # Extracts the literal-string first argument when
171
171
  # present. Returns nil for variable / expression keys —
172
- # those are out of scope for v0.1.0.
172
+ # only literal keys are statically validated.
173
173
  def literal_key_for(call_node)
174
174
  args = call_node.arguments&.arguments || []
175
175
  return nil if args.empty?
@@ -224,9 +224,7 @@ module Rigor
224
224
 
225
225
  def collect_assoc_keys(hash_node)
226
226
  # Both `Prism::HashNode` and `Prism::KeywordHashNode`
227
- # expose `#elements`; the conditional was an
228
- # accidental no-op carried over from an earlier
229
- # draft.
227
+ # expose `#elements`, so a single path handles both.
230
228
  hash_node.elements.filter_map do |element|
231
229
  next nil unless element.is_a?(Prism::AssocNode)
232
230
 
@@ -57,7 +57,7 @@ module Rigor
57
57
  parsed.each do |locale, tree|
58
58
  locale = locale.to_s
59
59
  locales << locale
60
- flatten_tree(tree, []).each do |dotted_key, value|
60
+ each_flattened(tree, []) do |dotted_key, value|
61
61
  placeholders = (per_key[dotted_key] ||= {})
62
62
  placeholders[locale] = extract_placeholders(value)
63
63
  kinds = (per_key_kinds[dotted_key] ||= {})
@@ -101,24 +101,40 @@ module Rigor
101
101
  end
102
102
 
103
103
  # Recursively walks the per-locale subtree, yielding
104
- # `[dotted_key, leaf_value]` pairs. Hash leaves are
104
+ # `[dotted_key, leaf_value]` for each leaf. Hash leaves are
105
105
  # *not* recorded as entries themselves — only their
106
- # descendants — but every leaf scalar / array IS
107
- # recorded.
108
- def flatten_tree(node, breadcrumbs)
109
- case node
110
- when Hash
111
- node.flat_map do |k, v|
112
- flatten_tree(v, breadcrumbs + [k.to_s])
106
+ # descendants — but every leaf scalar / array IS recorded.
107
+ #
108
+ # `breadcrumbs` is a single mutable stack reused across the
109
+ # whole walk (push before recursing, pop after): for the
110
+ # 530-file / 14 MB Mastodon locale corpus the old
111
+ # `flat_map { flatten_tree(v, breadcrumbs + [k]) }` shape
112
+ # allocated a fresh breadcrumb Array at every node plus an
113
+ # intermediate result Array at every level — millions of
114
+ # short-lived objects and the run's top allocation site. The
115
+ # dotted key is still materialised once per leaf (it has to
116
+ # be); everything else is now allocation-free traversal.
117
+ def each_flattened(node, breadcrumbs, &)
118
+ if node.is_a?(Hash)
119
+ node.each do |k, v|
120
+ breadcrumbs.push(k.to_s)
121
+ each_flattened(v, breadcrumbs, &)
122
+ breadcrumbs.pop
113
123
  end
114
124
  else
115
- [[breadcrumbs.join("."), node]]
125
+ yield breadcrumbs.join("."), node
116
126
  end
117
127
  end
118
128
 
119
129
  def extract_placeholders(value)
120
130
  case value
121
- when String then value.scan(PLACEHOLDER_RE).flatten.to_set
131
+ when String
132
+ # Most locale leaves carry no `%{var}`; skip the scan +
133
+ # flatten + to_set allocation trio for them. A string with
134
+ # no `%{` yields an empty placeholder set either way.
135
+ return Set.new unless value.include?("%{")
136
+
137
+ value.scan(PLACEHOLDER_RE).flatten.to_set
122
138
  when Array then value.map { |v| extract_placeholders(v) }.reduce(Set.new) { |a, s| a | s }
123
139
  else Set.new
124
140
  end
@@ -40,7 +40,7 @@ module Rigor
40
40
  # arguments. Missing placeholders are errors; extra
41
41
  # arguments are warnings.
42
42
  #
43
- # ## Limitations (v0.1.0)
43
+ # ## Limitations
44
44
  #
45
45
  # - Only literal-string keys are validated. `t(key)` with
46
46
  # a variable receiver is silently passed through.
@@ -38,11 +38,9 @@ module Rigor
38
38
  # (`user_facebook_omniauth_authorize_path`) and Devise
39
39
  # declares them from the configured providers, which
40
40
  # live in an initializer this static parser does not
41
- # read. We register the SHAPE-FAMILY suffix matchers
42
- # `_omniauth_authorize_path` / `_omniauth_callback_path`
43
- # via a separate hook (see `OMNIAUTH_HELPER_PATTERNS`);
44
- # these are NOT in the table but consulted by the
45
- # `Analyzer.allowed_dynamic_pattern?` check.
41
+ # read. The suffix patterns are registered in
42
+ # `OMNIAUTH_SUFFIXES` and consulted by
43
+ # `HelperTable#omniauth_match?`.
46
44
  module DeviseRoutes
47
45
  # The standard Devise controllers and the helper
48
46
  # actions each generates. Keys are the controller
@@ -190,7 +188,7 @@ module Rigor
190
188
 
191
189
  # Returns the set of OmniAuth pattern suffixes the
192
190
  # analyzer accepts for a given resource. The analyzer
193
- # consults this set (via `HelperTable#allows_omniauth?`)
191
+ # consults this set (via `HelperTable#omniauth_match?`)
194
192
  # when a `*_path` / `*_url` call's name does not match
195
193
  # any registered entry and its prefix matches a Devise
196
194
  # resource.
@@ -1171,9 +1171,10 @@ module Rigor
1171
1171
  end
1172
1172
 
1173
1173
  def in_singular_resource?(*)
1174
- # Slice 1 doesn't model the singular-resource frame
1175
- # separately; placeholder so member / collection
1176
- # blocks at least descend.
1174
+ # Stub: always returns true so member / collection
1175
+ # blocks descend. The singular-resource frame
1176
+ # (`push_singular_resource`) is modelled in Context;
1177
+ # a future caller could use it to tighten this check.
1177
1178
  true
1178
1179
  end
1179
1180
 
@@ -1181,21 +1182,14 @@ module Rigor
1181
1182
  # `plural: true` for `resources :users`, `false` for
1182
1183
  # `resource :profile`.
1183
1184
  def register_resourceful_helpers(name, actions, base_arity, context, plural:)
1184
- # Singular resources (`resource :foo`) use the
1185
- # given name AS-IS for both path and helper
1186
- # singularising would mangle a deliberately-plural
1187
- # singular-DSL name like Mastodon's
1188
- # `resource :relationships, only: [:show, :update]`
1189
- # (Rails generates `relationships_path`, not
1190
- # `relationship_path`). Plural resources still
1191
- # singularise for the show / new / edit helpers.
1192
- # Plural resources singularise for show / new / edit
1193
- # helpers (`resources :users` → `user_path(id)`);
1194
- # singular resources use the name AS-IS even when it
1195
- # looks plural (Mastodon's `resource :relationships,
1196
- # only: [:show, :update]` → `relationships_path`).
1197
- # The path segment uses `name` in both shapes — Rails
1198
- # never singularises the URL.
1185
+ # Plural resources (`resources :users`) singularise
1186
+ # for show / new / edit helpers `user_path(id)`.
1187
+ # Singular resources (`resource :foo`) use the name
1188
+ # AS-IS singularising would mangle deliberately-
1189
+ # plural names like Mastodon's `resource
1190
+ # :relationships` `relationships_path`, not
1191
+ # `relationship_path`. The URL path uses `name`
1192
+ # in both shapes (Rails never singularises the URL).
1199
1193
  singular = plural ? singularize_word(name.to_s) : name.to_s
1200
1194
  path_base = "#{context.path_prefix}/#{name}"
1201
1195
 
@@ -26,9 +26,9 @@ module Rigor
26
26
  # - One level of nested `resources`
27
27
  #
28
28
  # The plugin publishes its parsed `:helper_table` through
29
- # the ADR-9 cross-plugin fact store so future
30
- # `rigor-actionpack` Phase 4 can consume it for
31
- # route-helper validation in controller code.
29
+ # the ADR-9 cross-plugin fact store; `rigor-actionpack`
30
+ # Phase 4 consumes it for route-helper validation in
31
+ # controller code.
32
32
  #
33
33
  # ## Configuration
34
34
  #
@@ -147,8 +147,8 @@ module Rigor
147
147
  end
148
148
 
149
149
  # Publishes the parsed table to the cross-plugin fact
150
- # store so future Tier 2 plugins (rigor-actionpack
151
- # Phase 4) can read it via `services.fact_store.read`.
150
+ # store; `rigor-actionpack` Phase 4 reads it via
151
+ # `services.fact_store.read`.
152
152
  def prepare(services)
153
153
  table = helper_table_or_nil
154
154
  return if table.nil?
@@ -21,10 +21,9 @@ module Rigor
21
21
  # { ... }` and `subject(:name) { ... }` declarations.
22
22
  # `:subject` is the key for the implicit `subject { ... }`.
23
23
  #
24
- # Pillar 2 Slice 2 — used by the plugin's let-binding
25
- # `dynamic_return` rule to bind `let`-named
26
- # method-shape calls inside `it` bodies to the let
27
- # block's inferred type.
24
+ # Used by the plugin's let-binding `dynamic_return`
25
+ # rule to bind `let`-named method-shape calls inside
26
+ # `it` bodies to the let block's inferred type.
28
27
  class LetScopeIndex
29
28
  Record = Struct.new(:outer_range, :describe_const, :lets, keyword_init: true) do
30
29
  def contains?(line) = outer_range.cover?(line)
@@ -6,7 +6,7 @@ require "rigor/type"
6
6
  module Rigor
7
7
  module Plugin
8
8
  class Rspec < Rigor::Plugin::Base
9
- # Pillar 2 Slice 2 — resolves the runtime type of a
9
+ # Resolves the runtime type of a
10
10
  # `let(:name) { body }` or `subject(:name) { body }`
11
11
  # block by pattern-matching its body's tail expression.
12
12
  #
@@ -8,7 +8,7 @@ require "rigor/flow_contribution/fact"
8
8
  module Rigor
9
9
  module Plugin
10
10
  class Rspec < Rigor::Plugin::Base
11
- # Pillar 2 Slice 1 — recognises `expect(x).to MATCHER`
11
+ # Recognises `expect(x).to MATCHER`
12
12
  # patterns at per-call recognition time and emits
13
13
  # `post_return_facts` that narrow the named local on the
14
14
  # post-call edge.
@@ -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