rigortype 0.1.19 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. checksums.yaml +4 -4
  2. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -23
  3. data/lib/rigor/analysis/check_rules/rule_walk.rb +3 -21
  4. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
  5. data/lib/rigor/analysis/check_rules.rb +492 -71
  6. data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
  7. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
  8. data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
  9. data/lib/rigor/analysis/fact_store.rb +5 -4
  10. data/lib/rigor/analysis/rule_catalog.rb +153 -6
  11. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +17 -17
  12. data/lib/rigor/analysis/runner/project_pre_passes.rb +9 -8
  13. data/lib/rigor/analysis/runner.rb +17 -6
  14. data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
  15. data/lib/rigor/analysis/worker_session.rb +10 -14
  16. data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
  17. data/lib/rigor/cache/store.rb +5 -3
  18. data/lib/rigor/cli/annotate_command.rb +28 -7
  19. data/lib/rigor/cli/baseline_command.rb +4 -3
  20. data/lib/rigor/cli/check_command.rb +115 -16
  21. data/lib/rigor/cli/coverage_command.rb +148 -16
  22. data/lib/rigor/cli/coverage_scan.rb +57 -0
  23. data/lib/rigor/cli/explain_command.rb +2 -0
  24. data/lib/rigor/cli/lsp_command.rb +3 -7
  25. data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
  26. data/lib/rigor/cli/mutation_protection_report.rb +73 -0
  27. data/lib/rigor/cli/options.rb +9 -0
  28. data/lib/rigor/cli/plugins_command.rb +2 -1
  29. data/lib/rigor/cli/protection_renderer.rb +63 -0
  30. data/lib/rigor/cli/protection_report.rb +68 -0
  31. data/lib/rigor/cli/sig_gen_command.rb +2 -1
  32. data/lib/rigor/cli/trace_command.rb +2 -1
  33. data/lib/rigor/cli/triage_command.rb +2 -1
  34. data/lib/rigor/cli/type_of_command.rb +1 -1
  35. data/lib/rigor/cli/type_scan_command.rb +2 -1
  36. data/lib/rigor/cli.rb +3 -2
  37. data/lib/rigor/configuration/dependencies.rb +2 -4
  38. data/lib/rigor/configuration.rb +45 -7
  39. data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
  40. data/lib/rigor/environment/class_registry.rb +4 -3
  41. data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
  42. data/lib/rigor/environment/lockfile_resolver.rb +1 -1
  43. data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
  44. data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
  45. data/lib/rigor/environment/rbs_loader.rb +49 -5
  46. data/lib/rigor/environment.rb +17 -7
  47. data/lib/rigor/flow_contribution/fact.rb +1 -1
  48. data/lib/rigor/flow_contribution.rb +3 -5
  49. data/lib/rigor/inference/acceptance.rb +17 -9
  50. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  51. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
  52. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
  53. data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
  54. data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
  55. data/lib/rigor/inference/expression_typer.rb +20 -28
  56. data/lib/rigor/inference/hkt_body.rb +8 -11
  57. data/lib/rigor/inference/hkt_body_parser.rb +10 -12
  58. data/lib/rigor/inference/hkt_registry.rb +10 -11
  59. data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
  60. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +156 -21
  61. data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
  62. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
  63. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
  64. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
  65. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
  66. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
  67. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
  68. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +90 -15
  69. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
  70. data/lib/rigor/inference/method_dispatcher.rb +40 -48
  71. data/lib/rigor/inference/mutation_widening.rb +5 -11
  72. data/lib/rigor/inference/narrowing.rb +14 -16
  73. data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
  74. data/lib/rigor/inference/project_patched_methods.rb +4 -7
  75. data/lib/rigor/inference/project_patched_scanner.rb +2 -13
  76. data/lib/rigor/inference/protection_scanner.rb +86 -0
  77. data/lib/rigor/inference/scope_indexer.rb +129 -55
  78. data/lib/rigor/inference/statement_evaluator.rb +244 -114
  79. data/lib/rigor/inference/struct_fold_safety.rb +181 -0
  80. data/lib/rigor/inference/synthetic_method.rb +7 -7
  81. data/lib/rigor/language_server/completion_provider.rb +6 -12
  82. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
  83. data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
  84. data/lib/rigor/language_server/hover_provider.rb +2 -3
  85. data/lib/rigor/language_server/hover_renderer.rb +2 -11
  86. data/lib/rigor/language_server/server.rb +9 -17
  87. data/lib/rigor/language_server.rb +4 -5
  88. data/lib/rigor/plugin/base.rb +10 -8
  89. data/lib/rigor/plugin/macro/block_as_method.rb +3 -4
  90. data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
  91. data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
  92. data/lib/rigor/plugin/macro.rb +4 -5
  93. data/lib/rigor/plugin/manifest.rb +45 -66
  94. data/lib/rigor/plugin/registry.rb +6 -7
  95. data/lib/rigor/plugin/type_node_resolver.rb +6 -8
  96. data/lib/rigor/protection/mutation_scanner.rb +120 -0
  97. data/lib/rigor/protection/mutator.rb +246 -0
  98. data/lib/rigor/rbs_extended.rb +24 -36
  99. data/lib/rigor/reflection.rb +4 -7
  100. data/lib/rigor/scope/discovery_index.rb +14 -2
  101. data/lib/rigor/scope.rb +54 -11
  102. data/lib/rigor/sig_gen/observed_call.rb +3 -3
  103. data/lib/rigor/sig_gen/writer.rb +40 -2
  104. data/lib/rigor/source/constant_path.rb +62 -0
  105. data/lib/rigor/source.rb +1 -0
  106. data/lib/rigor/type/bound_method.rb +2 -11
  107. data/lib/rigor/type/combinator.rb +16 -3
  108. data/lib/rigor/type/constant.rb +2 -11
  109. data/lib/rigor/type/data_class.rb +2 -11
  110. data/lib/rigor/type/data_instance.rb +2 -11
  111. data/lib/rigor/type/hash_shape.rb +2 -11
  112. data/lib/rigor/type/integer_range.rb +2 -11
  113. data/lib/rigor/type/intersection.rb +2 -11
  114. data/lib/rigor/type/nominal.rb +2 -11
  115. data/lib/rigor/type/plain_lattice.rb +37 -0
  116. data/lib/rigor/type/refined.rb +72 -13
  117. data/lib/rigor/type/singleton.rb +2 -11
  118. data/lib/rigor/type/struct_class.rb +75 -0
  119. data/lib/rigor/type/struct_instance.rb +93 -0
  120. data/lib/rigor/type/tuple.rb +5 -15
  121. data/lib/rigor/type.rb +2 -0
  122. data/lib/rigor/version.rb +1 -1
  123. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  124. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
  125. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +3 -3
  126. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
  127. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
  128. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +7 -10
  129. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
  130. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
  131. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +6 -8
  132. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
  133. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -2
  134. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
  135. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
  136. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
  137. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
  138. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
  139. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
  140. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
  141. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +7 -9
  142. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
  143. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
  144. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +3 -3
  145. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
  146. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
  147. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
  148. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +1 -1
  149. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
  150. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
  151. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +5 -5
  152. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
  153. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
  154. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  155. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +19 -14
  156. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
  157. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
  158. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
  159. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
  160. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
  161. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
  162. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
  163. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +28 -41
  164. data/sig/rigor/scope.rbs +9 -1
  165. data/sig/rigor/type.rbs +36 -1
  166. metadata +19 -1
@@ -132,8 +132,7 @@ module Rigor
132
132
  # ADR-60 WD3 record-and-validate: the producer's in-block
133
133
  # `AttachmentDiscoverer` reads are captured into the
134
134
  # dependency descriptor after the block runs, and the
135
- # producer's `watch:` covers model-file additions — so no
136
- # priming walk is needed (it used to run the discover twice).
135
+ # producer's `watch:` covers model-file additions.
137
136
  @attachment_index = cache_for(:attachment_index, params: {}).call
138
137
  rescue Plugin::AccessDeniedError => e
139
138
  @load_errors << "rigor-activestorage: #{e.message}"
@@ -44,19 +44,17 @@ module Rigor
44
44
  # `send_reset_password_instructions`, etc. resolve through the
45
45
  # synthetic-method tier without `call.undefined-method`.
46
46
  #
47
- # ## Floor / ceiling per ADR-16 WD13
47
+ # ## Precision tier
48
48
  #
49
- # Slice 3 ships at the **floor**: synthesised method names
50
- # emit and the dispatcher's `try_synthetic_method` tier
51
- # returns `Type::Combinator.untyped` (Dynamic[T]) for every
52
- # match. Per the slice-3 design judgment (1) the precision
53
- # promotion looking up the module's authored RBS return
54
- # type at dispatch time is **slice-6 ceiling work** and is
55
- # NOT a delivery commitment of slice 3c. The `origin_module:`
56
- # provenance field is recorded so the ceiling slice can
57
- # promote without rescanning.
49
+ # The scanner records `origin_module:` in each synthetic
50
+ # method's provenance. The dispatcher's slice-6a TierB path
51
+ # (`promote_via_origin_module`) redispatches on
52
+ # `Nominal[origin_module]` via `RbsDispatch`, so Devise's
53
+ # authored RBS return types win: `valid_password?` returns
54
+ # `bool`, not `Dynamic[T]`. Unknown return types degrade
55
+ # gracefully to `Dynamic[T]`.
58
56
  #
59
- # ## Scope (slice 3c minimum)
57
+ # ## Scope
60
58
  #
61
59
  # - Recognises model-side `devise :a, :b` on any AR::Base
62
60
  # subclass; trait symbol set mirrors `lib/devise/modules.rb`.
@@ -35,16 +35,15 @@ module Rigor
35
35
  # other files then dispatch through the synthetic record rather
36
36
  # than falling through to `call.undefined-method`.
37
37
  #
38
- # ## Floor / ceiling per ADR-16 WD13
38
+ # ## Precision model (ADR-16 WD13 + ADR-18)
39
39
  #
40
- # Slice 2 ships at the **floor**: the synthetic reader's return
41
- # type degrades to `Dynamic[T]`. The manifest's `returns: "Object"`
42
- # is recorded but not resolved precise return-type promotion
43
- # (so `attribute :city, Types::String` makes `address.city`
44
- # return `String`) is the **ceiling**, deferred to slice 6
45
- # (ADR-13 `Plugin::TypeNodeResolver` chain). The plugin's manifest
46
- # value of `returns:` would today be the upstream gem's reader
47
- # return shape; slice 6 unlocks precision without re-authoring.
40
+ # The synthetic reader's return type is resolved via ADR-18's
41
+ # `returns_from_arg:` fact lookup: the call-site's second argument
42
+ # (`Types::String` etc.) is looked up through the `:dry_type_aliases`
43
+ # fact published by `rigor-dry-types`, yielding `Nominal[String]`
44
+ # for common cases. When the lookup misses (e.g. inline method-chain
45
+ # argument whose chain-head isn't currently extracted), the row
46
+ # falls back to `Dynamic[Top]` silently.
48
47
  #
49
48
  # ## Scope (slice 2c minimum)
50
49
  #
@@ -22,9 +22,8 @@ module Rigor
22
22
  # Other dry-rb adapter plugins consume this fact:
23
23
  #
24
24
  # - `rigor-dry-struct` reads it so `attribute :city, Types::String`
25
- # can promote `address.city` from `Dynamic[T]` to `Nominal[String]`
26
- # (gated on the slice-6 precision-promotion work + ADR-13
27
- # resolver chain).
25
+ # promotes `address.city` from `Dynamic[Top]` to `Nominal[String]`
26
+ # via ADR-18's `returns_from_arg:` fact lookup.
28
27
  # - `rigor-dry-validation` / `rigor-dry-schema` read it for
29
28
  # per-key type recognition in `schema { … }` / `params { … }`
30
29
  # blocks (separate plugin slice).
@@ -43,17 +42,19 @@ module Rigor
43
42
  # "<UnderlyingClass>" }` so consumers can match on the
44
43
  # qualified constant name they see in source.
45
44
  #
46
- # The **ceiling** (slice 2+):
45
+ # Implemented beyond the floor:
47
46
  #
48
- # - Recognises nested namespaces (`Types::Coercible::Integer`,
47
+ # - Nested-namespace aliases (`Types::Coercible::Integer`,
49
48
  # `Types::Strict::Symbol`, `Types::Params::Bool`,
50
- # `Types::JSON::Date`) — each is a separate dry-types
51
- # "category" with its own coercion semantics.
52
- # - Recognises user-authored compositions
53
- # (`Types::String.constrained(min_size: 1)`,
54
- # `Email = Types::String.constrained(format: …)`) so the
55
- # alias surface extends beyond the canonical names.
56
- # - Emits `dry-types.unknown-alias` / `dry-types.alias-shadow`
49
+ # `Types::JSON::Date`) — the four coercion categories each
50
+ # map to the same underlying class as their canonical shortcut.
51
+ # - User-authored compositions (`Email = Types::String.constrained(...)`,
52
+ # transitive resolution through composition chains) — the alias
53
+ # surface extends beyond the 15 canonical names.
54
+ #
55
+ # Deferred:
56
+ #
57
+ # - `dry-types.unknown-alias` / `dry-types.alias-shadow`
57
58
  # diagnostics when downstream code references a name that
58
59
  # wasn't published.
59
60
  #
@@ -21,10 +21,9 @@ module Rigor
21
21
  # cross-plugin fact.
22
22
  # - Ships an RBS overlay (`sig/dry_validation.rbs`) typing
23
23
  # `Dry::Validation::Contract#call` (returns Result) and
24
- # `Dry::Validation::Result#{success?, failure?, to_h}`. Users
25
- # add the path to their `.rigor.yml`'s `signature_paths:` so
26
- # `contract.call(input).to_h` infers cleanly. See the README
27
- # for the wiring step.
24
+ # `Dry::Validation::Result#{success?, failure?, to_h}`.
25
+ # The manifest's `signature_paths: ["sig"]` auto-contributes
26
+ # the overlay (ADR-25) no project-side wiring needed.
28
27
  #
29
28
  # Slice 2 (deferred, per design note):
30
29
  #
@@ -86,14 +86,14 @@ module Rigor
86
86
  # a `Prism::SymbolNode` is treated as a literal
87
87
  # attribute reference.
88
88
  #
89
- # Phase 1 (c) — when `model_index` (the cross-plugin
90
- # `:model_index` fact published by rigor-activerecord)
91
- # is present, the effective accepted key set is the
92
- # UNION of the factory's declared attributes plus the
93
- # corresponding model's columns. FactoryBot's runtime
94
- # accepts any AR attribute regardless of whether the
95
- # factory declared it, so the cross-check broadens the
96
- # acceptance accordingly.
89
+ # When `model_index` (the cross-plugin `:model_index`
90
+ # fact published by rigor-activerecord) is present,
91
+ # the effective accepted key set is the UNION of the
92
+ # factory's declared attributes plus the corresponding
93
+ # model's columns. FactoryBot's runtime accepts any AR
94
+ # attribute regardless of whether the factory declared
95
+ # it, so the cross-check broadens the acceptance
96
+ # accordingly.
97
97
  def unknown_attribute_violations(call_node, entry, model_index)
98
98
  accepted_keys, suggestion_dictionary = effective_keys(entry, model_index)
99
99
  attr_spell_checker = DidYouMean::SpellChecker.new(dictionary: suggestion_dictionary)
@@ -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.