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
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../trinary"
4
+ require_relative "../value_semantics"
5
+ require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
7
+
8
+ module Rigor
9
+ module Type
10
+ # The class object produced by `Struct.new(:x, :y)` (ADR-48 Struct
11
+ # follow-up). The mutable sibling of {DataClass}: it models the *class*
12
+ # (the value bound to `Point` in `Point = Struct.new(:x, :y)`, or the
13
+ # anonymous superclass in `class Point < Struct.new(:x, :y)`), carrying
14
+ # the ordered member-name list so `Point.new(...)` can materialise a
15
+ # {StructInstance}.
16
+ #
17
+ # `keyword_init` records the `Struct.new(..., keyword_init: true)` flag
18
+ # so `.new` only materialises a precise instance for the matching call
19
+ # form — a positional `.new(1, 2)` on a `keyword_init: true` struct, or
20
+ # a keyword `.new(x: 1)` on a positional struct, is a different runtime
21
+ # shape and must degrade rather than fold a wrong member map.
22
+ #
23
+ # `class_name` carries the binding name when known (the named-subclass
24
+ # form) and is `nil` for the anonymous result of a bare `Struct.new(...)`
25
+ # before it is assigned to a constant.
26
+ #
27
+ # Equality and hashing are structural over the member list, the class
28
+ # name, and the keyword-init flag.
29
+ #
30
+ # See docs/adr/48-data-struct-value-folding.md § "Struct follow-up".
31
+ class StructClass
32
+ attr_reader :members, :class_name, :keyword_init
33
+
34
+ # @param members [Array<Symbol>] ordered member names.
35
+ # @param class_name [String, nil] the bound class name, or nil for
36
+ # the anonymous `Struct.new(...)` result.
37
+ # @param keyword_init [Boolean] the `keyword_init:` flag.
38
+ def initialize(members, class_name = nil, keyword_init: false)
39
+ unless members.is_a?(Array) && members.all?(Symbol)
40
+ raise ArgumentError, "members must be an Array of Symbols, got #{members.inspect}"
41
+ end
42
+ unless class_name.nil? || (class_name.is_a?(String) && !class_name.empty?)
43
+ raise ArgumentError, "class_name must be a non-empty String or nil, got #{class_name.inspect}"
44
+ end
45
+
46
+ @members = members.dup.freeze
47
+ @class_name = class_name&.freeze
48
+ @keyword_init = keyword_init ? true : false
49
+ freeze
50
+ end
51
+
52
+ def describe(_verbosity = :short)
53
+ return "singleton(#{class_name})" if class_name
54
+
55
+ "Struct.new(#{members.map(&:inspect).join(', ')})"
56
+ end
57
+
58
+ def erase_to_rbs
59
+ "singleton(#{class_name || 'Struct'})"
60
+ end
61
+
62
+ include Rigor::Type::PlainLattice
63
+
64
+ include Rigor::Type::AcceptanceRouter
65
+
66
+ include Rigor::ValueSemantics
67
+
68
+ value_fields :members, :class_name, :keyword_init
69
+
70
+ def inspect
71
+ "#<Rigor::Type::StructClass #{describe(:short)}>"
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../trinary"
4
+ require_relative "../value_semantics"
5
+ require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
7
+
8
+ module Rigor
9
+ module Type
10
+ # A `Struct.new` value instance (ADR-48 Struct follow-up) —
11
+ # `Point.new(1, 2)`. The mutable sibling of {DataInstance}: a closed,
12
+ # total, class-tagged member map (member name -> value type),
13
+ # HashShape-shaped but nominal.
14
+ #
15
+ # Unlike {DataInstance}, a `Struct` instance is **mutable** — `s.x = v`,
16
+ # `s[:x] = v`, and escape can invalidate the member map. The folding
17
+ # tier therefore only projects member reads off a **fresh** instance
18
+ # (the transient receiver of a `.new(...).x` / `.with(...).x` chain,
19
+ # which provably cannot have been mutated between materialisation and
20
+ # the read); a read off a *stored* binding degrades to `Dynamic[top]`
21
+ # rather than fold a possibly-stale member value. Promoting the
22
+ # fold to mutation-free bound locals is the deferred slice 3 (see ADR).
23
+ #
24
+ # That mutability-gating lives in the dispatch tier (`StructFolding`),
25
+ # not the carrier: the carrier itself just records the member map. Like
26
+ # {DataInstance}, non-folded methods project to the `Struct` nominal (or
27
+ # the tagged class) through {RbsDispatch}'s `receiver_descriptor`, so
28
+ # non-member calls resolve without mis-firing undefined-method.
29
+ #
30
+ # Equality and hashing are structural over the (member -> type) map and
31
+ # the class name.
32
+ #
33
+ # See docs/adr/48-data-struct-value-folding.md § "Struct follow-up".
34
+ class StructInstance
35
+ attr_reader :members, :class_name
36
+
37
+ # @param members [Hash{Symbol => Rigor::Type}] ordered member -> type
38
+ # map. Every declared member is present (Struct instances are total).
39
+ # @param class_name [String, nil] the tagging class name, or nil for
40
+ # an instance of an anonymous `Struct.new(...)` class.
41
+ def initialize(members, class_name = nil)
42
+ unless members.is_a?(Hash) && members.each_key.all?(Symbol)
43
+ raise ArgumentError, "members must be a Hash with Symbol keys, got #{members.inspect}"
44
+ end
45
+ unless class_name.nil? || (class_name.is_a?(String) && !class_name.empty?)
46
+ raise ArgumentError, "class_name must be a non-empty String or nil, got #{class_name.inspect}"
47
+ end
48
+
49
+ @members = members.dup.freeze
50
+ @class_name = class_name&.freeze
51
+ freeze
52
+ end
53
+
54
+ # @return [Array<Symbol>] ordered member names.
55
+ def member_names
56
+ members.keys
57
+ end
58
+
59
+ # @return [Rigor::Type, nil] the member's value type, or nil when the
60
+ # name is not a declared member.
61
+ def member_type(name)
62
+ members[name]
63
+ end
64
+
65
+ def describe(verbosity = :short)
66
+ rendered = members.map { |name, type| "#{name}: #{type.describe(verbosity)}" }
67
+ "#{class_name || 'Struct'}(#{rendered.join(', ')})"
68
+ end
69
+
70
+ # Erases to the tagging class nominal (conservative: the structural
71
+ # members are not RBS-expressible as a class instance). The
72
+ # anonymous case erases to the `Struct` supertype.
73
+ def erase_to_rbs
74
+ name = class_name
75
+ return "Struct" if name.nil?
76
+
77
+ name
78
+ end
79
+
80
+ include Rigor::Type::PlainLattice
81
+
82
+ include Rigor::Type::AcceptanceRouter
83
+
84
+ include Rigor::ValueSemantics
85
+
86
+ value_fields :members, :class_name
87
+
88
+ def inspect
89
+ "#<Rigor::Type::StructInstance #{describe(:short)}>"
90
+ end
91
+ end
92
+ end
93
+ end
@@ -3,6 +3,7 @@
3
3
  require_relative "../trinary"
4
4
  require_relative "../value_semantics"
5
5
  require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
6
7
 
7
8
  module Rigor
8
9
  module Type
@@ -17,10 +18,9 @@ module Rigor
17
18
  #
18
19
  # Slice 5 phase 1 introduces the carrier and surfaces it from the
19
20
  # `ArrayNode` literal handler when every element is a non-splat
20
- # value. Tuple-aware refinements for `tuple[0]`, `tuple.first`, and
21
- # destructuring assignment are deferred to Slice 5 phase 2; they
22
- # will run as a higher-priority dispatch tier above
23
- # {Rigor::Inference::MethodDispatcher::RbsDispatch}.
21
+ # value. Tuple-aware refinements (`tuple[0]`, `tuple.first`,
22
+ # destructuring) are implemented in `ShapeDispatch`, which runs
23
+ # above {Rigor::Inference::MethodDispatcher::RbsDispatch}.
24
24
  #
25
25
  # Equality and hashing are structural across an ordered, frozen
26
26
  # element list. The empty Tuple `Tuple[]` is permitted; the array
@@ -53,17 +53,7 @@ module Rigor
53
53
  "[#{elements.map(&:erase_to_rbs).join(', ')}]"
54
54
  end
55
55
 
56
- def top
57
- Trinary.no
58
- end
59
-
60
- def bot
61
- Trinary.no
62
- end
63
-
64
- def dynamic
65
- Trinary.no
66
- end
56
+ include Rigor::Type::PlainLattice
67
57
 
68
58
  include Rigor::Type::AcceptanceRouter
69
59
 
data/lib/rigor/type.rb CHANGED
@@ -21,6 +21,8 @@ require_relative "type/tuple"
21
21
  require_relative "type/hash_shape"
22
22
  require_relative "type/data_class"
23
23
  require_relative "type/data_instance"
24
+ require_relative "type/struct_class"
25
+ require_relative "type/struct_instance"
24
26
  require_relative "type/union"
25
27
  require_relative "type/difference"
26
28
  require_relative "type/refined"
data/lib/rigor/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rigor
4
- VERSION = "0.1.19"
4
+ VERSION = "0.2.1"
5
5
  end
@@ -26,7 +26,7 @@ module Rigor
26
26
  # `stream_for` call) so the analyzer knows it can't
27
27
  # be sure of every stream name.
28
28
  #
29
- # Limitations (intentional for v0.1.0):
29
+ # Intentional limitations:
30
30
  #
31
31
  # - Direct-superclass match only.
32
32
  # - Public-vs-private is not tracked; the framework
@@ -69,9 +69,9 @@ module Rigor
69
69
  # True when at least one discovered channel uses a
70
70
  # dynamic stream registration. The analyzer treats
71
71
  # this as "we can't be sure any literal name is
72
- # missing" and downgrades unknown-stream from
73
- # `:warning` to `:info` (or drops it entirely;
74
- # current behaviour: skip warnings).
72
+ # missing" and skips the `unknown-stream` warning
73
+ # entirely absence of a literal match doesn't prove
74
+ # the name is wrong.
75
75
  def any_dynamic_streams?
76
76
  @entries.any?(&:dynamic_streams)
77
77
  end
@@ -41,7 +41,7 @@ module Rigor
41
41
  # `stream_for record`) — the absence of a literal
42
42
  # match doesn't prove absence.
43
43
  #
44
- # ## Limitations (v0.1.0)
44
+ # ## Limitations
45
45
  #
46
46
  # - **Direct-superclass match only.** Indirect
47
47
  # inheritance (`AdminChannel < BaseChannel <
@@ -51,8 +51,8 @@ module Rigor
51
51
  # ActionCable actions are invoked from JS via
52
52
  # `subscription.perform("action_name", data)`; we
53
53
  # don't analyse JS so the action-method index is
54
- # currently informational only (future cross-plugin
55
- # handoff to a hypothetical JS-side analyzer).
54
+ # informational only (deferred: cross-plugin handoff
55
+ # to a JS-side analyzer).
56
56
  # - **`broadcast_to` arity isn't checked.** The method
57
57
  # takes any record + any data hash; there's no
58
58
  # useful arity envelope.
@@ -301,19 +301,11 @@ module Rigor
301
301
  [entry.method_name, entry]
302
302
  end
303
303
 
304
- # Merge in actions from include'd modules. The
305
- # discoverer pre-collected every module's defs as
306
- # `module_actions` keyed by fully-qualified module
307
- # name. We resolve each include against that map —
308
- # tries the full include name first, then walks down
309
- # the class's lexical chain looking for a nested
310
- # match (e.g. `Emails::Issues` inside `class Notify`
311
- # at top-level resolves to top-level `Emails::Issues`).
312
- # Includes we cannot resolve are silently skipped;
313
- # the per-mailer `unresolved_includes?` predicate
314
- # below (consumed by the analyzer) downgrades
315
- # `unknown-action` to silence when any include is
316
- # unresolved.
304
+ # Merge actions from include'd modules (pre-collected
305
+ # in `module_actions` keyed by fully-qualified name).
306
+ # Unresolvable includes are tracked; `unresolved_includes?`
307
+ # (consumed by the analyzer) downgrades `unknown-action`
308
+ # to silence when any include remains unresolved.
317
309
  unresolved_includes = []
318
310
  includes.each do |include_name|
319
311
  inc_actions = module_actions[include_name]
@@ -33,9 +33,8 @@ module Rigor
33
33
  # Phase 2 — filter-chain DSL methods. Each takes a
34
34
  # variadic list of filter names (Symbols / Strings) plus
35
35
  # optional `only:` / `except:` / `if:` / `unless:`
36
- # modifiers. The validation key is the filter NAMES; the
37
- # modifiers are accepted but their action-name argument
38
- # is not yet validated (Phase 2.5).
36
+ # modifiers. Only the filter NAMES are validated; the
37
+ # `only:`/`except:` action-name arguments are not (deferred).
39
38
  FILTER_DSL_METHODS = %i[
40
39
  before_action after_action around_action
41
40
  skip_before_action skip_after_action skip_around_action
@@ -43,18 +42,14 @@ module Rigor
43
42
  ].freeze
44
43
 
45
44
  # Phase 3 — render-target template extensions checked in
46
- # priority order. The first six cover the templating
47
- # engines used by the projects this plugin is regularly
48
- # exercised against: ERB (Rails default — `.html.erb`,
49
- # `.text.erb`), HAML (Mastodon, Solidus admin
50
- # `.html.haml`), Slim, and JSON (`.json.jbuilder` plus a
51
- # raw `.json.erb` for hand-rolled API responses). When a
52
- # template exists under any of these extensions, the
53
- # missing-template diagnostic stays silent.
54
- # Configurable extension list is queued — see the
55
- # `external-author plugin SKILL` track (v0.2.0). For now
56
- # this set is wide enough to cover the surveyed real-world
57
- # projects without leaking FPs.
45
+ # priority order. Covers the engines used by surveyed
46
+ # projects: ERB (Rails default `.html.erb`, `.text.erb`),
47
+ # HAML (Mastodon, Solidus admin — `.html.haml`), Slim, and
48
+ # JSON (`.json.jbuilder` plus `.json.erb` for hand-rolled API
49
+ # responses). When a template exists under any of these
50
+ # extensions, the missing-template diagnostic stays silent.
51
+ # A configurable extension list is deferred; this set is wide
52
+ # enough to cover surveyed real-world projects without FPs.
58
53
  RENDER_TEMPLATE_EXTENSIONS = %w[
59
54
  .html.erb
60
55
  .text.erb
@@ -167,8 +162,7 @@ module Rigor
167
162
  # list (looked up via the model_index fact published by
168
163
  # `rigor-activerecord`). Calls whose `:require` argument is a
169
164
  # non-literal Symbol are passed through; namespaced models
170
- # (`params.require(:admin_user)` → `Admin::User`) are deferred to a
171
- # Phase 1.5 follow-up.
165
+ # (`params.require(:admin_user)` → `Admin::User`) are deferred.
172
166
  #
173
167
  # @param call_node [Prism::Node]
174
168
  # @param model_index [Hash{String => Hash}]
@@ -68,17 +68,14 @@ module Rigor
68
68
  class Actionpack < Rigor::Plugin::Base
69
69
  manifest(
70
70
  id: "actionpack",
71
- # Bumped 2026-06-02 — ADR-37 node_rule migration. The four
72
- # phases (helper / filter / render / strong-params) now run
73
- # per-call over the engine-owned walk instead of the
74
- # hand-rolled `diagnostics_for_file` traversal; the enclosing
75
- # controller is read from the node-rule `NodeContext` ancestors.
76
- # Nested-module qualification is preserved — a
77
- # `module Admin; class DomainBlocksController; end` file still
71
+ # ADR-37: the four phases (helper / filter / render /
72
+ # strong-params) run per-call over the engine-owned walk;
73
+ # the enclosing controller is read from the node-rule
74
+ # `NodeContext` ancestors. Nested-module qualification is
75
+ # preserved `module Admin; class DomainBlocksController`
78
76
  # resolves as `Admin::DomainBlocksController` (matching the
79
- # `ControllerDiscoverer`), so render paths
80
- # (`admin/domain_blocks/new`) and filter-chain validation on
81
- # nested controllers are unchanged.
77
+ # `ControllerDiscoverer`), so render paths and filter-chain
78
+ # validation on nested controllers are correct.
82
79
  version: "0.8.0",
83
80
  description: "Validates Action Pack route-helper calls and filter chains inside controllers.",
84
81
  config_schema: {
@@ -12,8 +12,9 @@ module Rigor
12
12
  # (`Float::INFINITY` for the upper bound when `*args`
13
13
  # is present). `keyword_required` lists any required
14
14
  # keyword arguments — Active Job supports keyword args
15
- # but they're rare in user code, so the analyzer only
16
- # validates positional arity for v0.1.0.
15
+ # but they're rare in user code, so the analyzer
16
+ # validates positional arity only (keyword arity
17
+ # validation is deferred).
17
18
  class JobIndex
18
19
  Entry = Data.define(:class_name, :min_arity, :max_arity, :keyword_required) do
19
20
  # Flexible-friendly textual form of the arity for
@@ -227,8 +227,8 @@ module Rigor
227
227
  # Recognised single-instance and collection association
228
228
  # DSL methods. The kind drives the eventual return-type
229
229
  # contribution: singular associations narrow to
230
- # `Nominal[Target] | nil`, plural ones currently degrade
231
- # to the RBS envelope (relation types are a future track).
230
+ # `Nominal[Target] | nil`, plural ones narrow to
231
+ # `ActiveRecord::Relation[Target]`.
232
232
  #
233
233
  # `composed_of` value-object aggregations and
234
234
  # `delegated_type` roles are folded in here too — both
@@ -447,8 +447,8 @@ module Rigor
447
447
 
448
448
  # `scope :active, -> { ... }`. Records the scope name
449
449
  # only (the body is intentionally NOT introspected —
450
- # scopes return ActiveRecord::Relation, which Rigor
451
- # doesn't carry a precise type for yet).
450
+ # the caller contributes `ActiveRecord::Relation[Model]`
451
+ # based on the name alone via `class_scope_return_type`).
452
452
  def lookup_scopes(body)
453
453
  return [] if body.nil?
454
454
 
@@ -75,10 +75,9 @@ module Rigor
75
75
  "model_base_classes" => { kind: :array, default: %w[ApplicationRecord ActiveRecord::Base] }
76
76
  },
77
77
  produces: [:model_index],
78
- # ADR-25 — the bundled `ActiveRecord::Relation` RBS, the
79
- # type `flow_contribution_for`'s relation-typed call sites
80
- # (`has_many` accessors, `Model.where`, scopes) dispatch
81
- # against.
78
+ # ADR-25 — the bundled `ActiveRecord::Relation` RBS that
79
+ # relation-typed call sites (`has_many` accessors,
80
+ # `Model.where`, scopes) dispatch against.
82
81
  signature_paths: ["sig"],
83
82
  # ADR-26 — `ActiveRecord::Relation` is an "open" receiver:
84
83
  # it delegates an unbounded set of user-defined scopes /
@@ -89,7 +88,7 @@ module Rigor
89
88
  )
90
89
 
91
90
  # The class the bundled `sig/active_record/relation.rbs`
92
- # describes; `flow_contribution_for` contributes
91
+ # describes; `dynamic_return` contributes
93
92
  # `ActiveRecord::Relation[Model]` for relation-returning
94
93
  # call sites (`has_many` accessors, `Model.where`, scopes).
95
94
  RELATION_CLASS_NAME = "ActiveRecord::Relation"
@@ -261,9 +260,8 @@ module Rigor
261
260
  names
262
261
  end
263
262
 
264
- # The migrated body of the legacy `flow_contribution_for` —
265
- # same resolution order, returning the bare type the
266
- # `dynamic_return` contract expects.
263
+ # Resolution body for `dynamic_return` — same four-path
264
+ # order, returning the bare type the contract expects.
267
265
  def contribution_return_type(call_node, scope)
268
266
  return nil unless call_node.is_a?(Prism::CallNode)
269
267
 
@@ -11,13 +11,11 @@ module Rigor
11
11
  # plugin recognised so users can verify the model →
12
12
  # attachment mapping the plugin sees.
13
13
  #
14
- # No `:error` diagnostics in this slice — the
15
- # `dynamic_return` return-type narrowing carries
16
- # the type-checking value; surfacing unknown attachment
17
- # names as errors requires a coupled receiver-class
18
- # narrowing pass that the integration spec doesn't yet
19
- # rely on. A future slice can add `unknown-attachment`
20
- # similar to `rigor-activerecord`'s `unknown-column`.
14
+ # No `:error` diagnostics here — the `dynamic_return`
15
+ # return-type narrowing carries the type-checking value.
16
+ # An `unknown-attachment` rule (similar to
17
+ # `rigor-activerecord`'s `unknown-column`) is deferred:
18
+ # it requires a coupled receiver-class narrowing pass.
21
19
  class Analyzer
22
20
  attr_reader :diagnostics
23
21
 
@@ -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)