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
@@ -46,6 +46,11 @@ module Rigor
46
46
  def initialize(path_mapper:, overwrite: false)
47
47
  @path_mapper = path_mapper
48
48
  @overwrite = overwrite
49
+ # Run-level (cross-file) namespace-kind view, populated
50
+ # per `#write_all` from every candidate's per-file map.
51
+ # Empty until then so the single-target `#write` path
52
+ # falls back to per-candidate kinds only.
53
+ @global_namespace_kinds = {}
49
54
  end
50
55
 
51
56
  # Process the full candidate list by resolving each
@@ -65,6 +70,7 @@ module Rigor
65
70
  emittable = candidates.select { |c| EMITTABLE.include?(c.classification) }
66
71
  return [] if emittable.empty?
67
72
 
73
+ @global_namespace_kinds = build_namespace_kinds(candidates)
68
74
  emittable.group_by { |c| @path_mapper.target_for(c.path, class_name: c.class_name) }
69
75
  .map { |target, group| write_target(target, group) }
70
76
  end
@@ -161,13 +167,45 @@ module Rigor
161
167
  end
162
168
 
163
169
  def merged_namespace_kinds(candidates)
164
- merged = {}
170
+ merged = @global_namespace_kinds.dup
165
171
  candidates.each do |c|
166
- (c.namespace_kinds || {}).each { |k, v| merged[k] = v }
172
+ (c.namespace_kinds || {}).each { |k, v| apply_namespace_kind(merged, k, v) }
167
173
  end
168
174
  merged
169
175
  end
170
176
 
177
+ # Folds every candidate's per-file namespace-kind map
178
+ # into one run-level view so a `class Foo` recorded
179
+ # while scanning `foo.rb` governs the wrapper keyword
180
+ # emitted for `Foo` in a *sibling* file's target — e.g.
181
+ # `foo/bar.rb` declaring `class Foo::Bar`, whose compact
182
+ # constant path never names `Foo`, so the walker records
183
+ # no kind for it. Without this view that sibling target
184
+ # wraps the nested class in `module Foo` while `foo.rbs`
185
+ # declares `class Foo`; loading both raises
186
+ # `RBS::DuplicatedDeclarationError`, aborting the whole
187
+ # RBS env build.
188
+ def build_namespace_kinds(candidates)
189
+ candidates.each_with_object({}) do |candidate, acc|
190
+ (candidate.namespace_kinds || {}).each { |name, kind| apply_namespace_kind(acc, name, kind) }
191
+ end
192
+ end
193
+
194
+ # A `class` declaration is authoritative and MUST win
195
+ # over the `:module` wrapper default: a compact
196
+ # `class Foo::Bar` never names `Foo`, so the only signal
197
+ # for `Foo`'s kind is an actual `class Foo` (or
198
+ # `Const = Data.define(...)` shell) seen elsewhere. This
199
+ # guarantees the generated tree never mixes `class` /
200
+ # `module` for the same constant.
201
+ def apply_namespace_kind(map, key, kind)
202
+ if kind == :class
203
+ map[key] = :class
204
+ else
205
+ map[key] ||= :module
206
+ end
207
+ end
208
+
171
209
  # Tree node: { name:, children: Hash{String => node},
172
210
  # methods: Array<MethodCandidate>, shell: Boolean }.
173
211
  # `shell` flags nodes that came in via `class_shells`
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ # Classifies each configured `signature_paths:` entry by what it
5
+ # actually contributes to the RBS environment, so a caller can warn
6
+ # when a configured path resolves to nothing.
7
+ #
8
+ # The failure this guards against is silent. {Environment::RbsLoader}
9
+ # `add`s a `signature_paths:` entry only when `path.directory?`, and
10
+ # only the `.rbs` files under it carry signatures — so a typo'd or
11
+ # moved path (or a directory holding no `.rbs`) loads zero signatures
12
+ # with no trace on stderr or in the run summary. The downstream symptom
13
+ # is the most authoritative diagnostics: every call into the extensions
14
+ # the missing RBS was meant to describe fires `call.undefined-method` at
15
+ # `evidence_tier: high`. A one-character path typo can manufacture
16
+ # hundreds of plausible-looking false positives; surfacing the empty
17
+ # entry makes the real cause visible.
18
+ #
19
+ # The audit deliberately mirrors the loader's own acceptance test
20
+ # (`path.directory?` + a recursive `**/*.rbs` glob) so a `:ok` verdict
21
+ # means the loader did load from it and a warning means it did not.
22
+ module SignaturePathAudit
23
+ # One configured `signature_paths:` entry's resolution status.
24
+ #
25
+ # `status` is one of:
26
+ # - `:ok` — a directory containing at least one `.rbs`.
27
+ # - `:missing` — the path does not exist.
28
+ # - `:not_directory` — the path exists but is not a directory (the
29
+ # loader only `add`s directories, so a `.rbs` file passed directly
30
+ # is silently ignored).
31
+ # - `:empty` — a directory with no `.rbs` file (recursive).
32
+ Entry = Data.define(:path, :status, :rbs_file_count) do
33
+ def ok?
34
+ status == :ok
35
+ end
36
+
37
+ def warning?
38
+ !ok?
39
+ end
40
+
41
+ # One-line, human-facing reason. The wording matches the loader's
42
+ # actual behaviour ("loaded nothing from it") rather than the
43
+ # filesystem error, so the message points at the consequence.
44
+ def message
45
+ case status
46
+ when :missing
47
+ "signature_paths: #{path.inspect} does not exist (no signatures loaded from it)"
48
+ when :not_directory
49
+ "signature_paths: #{path.inspect} is not a directory (no signatures loaded from it)"
50
+ when :empty
51
+ "signature_paths: #{path.inspect} matched 0 signature files"
52
+ else
53
+ "signature_paths: #{path.inspect} loaded #{rbs_file_count} signature file(s)"
54
+ end
55
+ end
56
+
57
+ def to_h
58
+ { "path" => path, "status" => status.to_s, "rbs_file_count" => rbs_file_count, "message" => message }
59
+ end
60
+ end
61
+
62
+ # Audits each configured entry. `signature_paths` is the
63
+ # {Configuration#signature_paths} array (absolute paths, already
64
+ # resolved against the config file's directory). Pass `nil` — the
65
+ # unset default, where Rigor auto-detects `<root>/sig` — to get an
66
+ # empty result: an absent auto-detected `sig/` is a normal setup, not
67
+ # a misconfiguration, so it is never audited.
68
+ #
69
+ # @param signature_paths [Array<String, Pathname>, nil]
70
+ # @return [Array<Entry>]
71
+ def self.audit(signature_paths)
72
+ Array(signature_paths).map { |path| classify(path.to_s) }
73
+ end
74
+
75
+ # The subset of {audit} that resolved to nothing — the entries worth
76
+ # warning about.
77
+ #
78
+ # @param signature_paths [Array<String, Pathname>, nil]
79
+ # @return [Array<Entry>]
80
+ def self.warnings(signature_paths)
81
+ audit(signature_paths).select(&:warning?)
82
+ end
83
+
84
+ def self.classify(path)
85
+ return Entry.new(path: path, status: :missing, rbs_file_count: 0) unless File.exist?(path)
86
+ return Entry.new(path: path, status: :not_directory, rbs_file_count: 0) unless File.directory?(path)
87
+
88
+ count = Dir.glob(File.join(path, "**", "*.rbs")).size
89
+ Entry.new(path: path, status: count.zero? ? :empty : :ok, rbs_file_count: count)
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Source
5
+ # Flattens a Prism constant-reference node (`ConstantReadNode` /
6
+ # `ConstantPathNode`) to its source-qualified `"A::B::C"` string.
7
+ #
8
+ # Two nil policies for the one edge case that distinguishes the call
9
+ # sites — a constant path rooted in a *dynamic* base (`expr::Bar`, where
10
+ # the left side is a runtime expression rather than a constant):
11
+ #
12
+ # * {.qualified_name} / {.render} are LENIENT — they drop the dynamic
13
+ # segment and render the trailing constant names (`expr::Bar` => "Bar").
14
+ # The scope indexer and statement evaluator feed only genuine
15
+ # class/module path nodes and want a best-effort name.
16
+ # * {.qualified_name_or_nil} is STRICT — a dynamic base anywhere in the
17
+ # chain yields `nil`, so a caller that statically names constants can
18
+ # treat the path as opaque rather than guessing.
19
+ #
20
+ # A leading `::` (absolute root, `::Foo`) renders as `"Foo"` under both
21
+ # policies. A node that is neither a `ConstantReadNode` nor a
22
+ # `ConstantPathNode` yields `nil` under both.
23
+ module ConstantPath
24
+ module_function
25
+
26
+ # Lenient dispatch over a constant-reference node.
27
+ def qualified_name(node)
28
+ case node
29
+ when Prism::ConstantReadNode then node.name.to_s
30
+ when Prism::ConstantPathNode then render(node)
31
+ end
32
+ end
33
+
34
+ # Lenient render of a `ConstantPathNode`; never nil for a path node.
35
+ def render(node)
36
+ prefix =
37
+ case node.parent
38
+ when Prism::ConstantReadNode then "#{node.parent.name}::"
39
+ when Prism::ConstantPathNode then "#{render(node.parent)}::"
40
+ else ""
41
+ end
42
+ "#{prefix}#{node.name}"
43
+ end
44
+
45
+ # Strict dispatch: a dynamic base anywhere in the path yields nil.
46
+ def qualified_name_or_nil(node)
47
+ case node
48
+ when Prism::ConstantReadNode
49
+ node.name.to_s
50
+ when Prism::ConstantPathNode
51
+ parent = node.parent
52
+ return node.name.to_s if parent.nil?
53
+
54
+ parent_name = qualified_name_or_nil(parent)
55
+ return nil if parent_name.nil?
56
+
57
+ "#{parent_name}::#{node.name}"
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
data/lib/rigor/source.rb CHANGED
@@ -14,3 +14,4 @@ end
14
14
  require_relative "source/node_locator"
15
15
  require_relative "source/node_walker"
16
16
  require_relative "source/literals"
17
+ require_relative "source/constant_path"
@@ -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
@@ -46,17 +47,7 @@ module Rigor
46
47
  "Method"
47
48
  end
48
49
 
49
- def top
50
- Trinary.no
51
- end
52
-
53
- def bot
54
- Trinary.no
55
- end
56
-
57
- def dynamic
58
- Trinary.no
59
- end
50
+ include Rigor::Type::PlainLattice
60
51
 
61
52
  include Rigor::Type::AcceptanceRouter
62
53
 
@@ -405,6 +405,21 @@ module Rigor
405
405
  DataInstance.new(members, class_name)
406
406
  end
407
407
 
408
+ # ADR-48 Struct follow-up — the class object produced by
409
+ # `Struct.new(:x, :y)`. `members` is the ordered Symbol member-name
410
+ # list; `keyword_init` records the `keyword_init:` flag; `class_name`
411
+ # tags the class when known (the named-subclass form).
412
+ def struct_class_of(members:, class_name: nil, keyword_init: false)
413
+ StructClass.new(members, class_name, keyword_init: keyword_init)
414
+ end
415
+
416
+ # ADR-48 Struct follow-up — a `Struct.new` value instance. `members`
417
+ # is the ordered member-name -> value-type map; `class_name` tags the
418
+ # instance's class when known.
419
+ def struct_instance_of(members:, class_name: nil)
420
+ StructInstance.new(members, class_name)
421
+ end
422
+
408
423
  # Normalized union. Flattens nested Unions, deduplicates structurally
409
424
  # equal members, drops Bot, and collapses 0/1-member results.
410
425
  def union(*types)
@@ -929,9 +944,7 @@ module Rigor
929
944
  end
930
945
  end
931
946
 
932
- # ADR-15 Phase 4b.x eager-allocate the singleton
933
- # `Dynamic[Top]` carrier on the main Ractor at load time.
934
- # The `untyped` reader above just returns this ivar.
947
+ # Eager-allocated at load time; see `untyped` method comment above.
935
948
  @untyped = Dynamic.new(Top.instance)
936
949
  end
937
950
  end
@@ -3,6 +3,7 @@
3
3
  require "date"
4
4
  require_relative "../trinary"
5
5
  require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
6
7
 
7
8
  module Rigor
8
9
  module Type
@@ -100,17 +101,7 @@ module Rigor
100
101
  end
101
102
  end
102
103
 
103
- def top
104
- Trinary.no
105
- end
106
-
107
- def bot
108
- Trinary.no
109
- end
110
-
111
- def dynamic
112
- Trinary.no
113
- end
104
+ include Rigor::Type::PlainLattice
114
105
 
115
106
  include Rigor::Type::AcceptanceRouter
116
107
 
@@ -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
@@ -54,17 +55,7 @@ module Rigor
54
55
  "singleton(#{class_name || 'Data'})"
55
56
  end
56
57
 
57
- def top
58
- Trinary.no
59
- end
60
-
61
- def bot
62
- Trinary.no
63
- end
64
-
65
- def dynamic
66
- Trinary.no
67
- end
58
+ include Rigor::Type::PlainLattice
68
59
 
69
60
  include Rigor::Type::AcceptanceRouter
70
61
 
@@ -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
@@ -74,17 +75,7 @@ module Rigor
74
75
  name
75
76
  end
76
77
 
77
- def top
78
- Trinary.no
79
- end
80
-
81
- def bot
82
- Trinary.no
83
- end
84
-
85
- def dynamic
86
- Trinary.no
87
- end
78
+ include Rigor::Type::PlainLattice
88
79
 
89
80
  include Rigor::Type::AcceptanceRouter
90
81
 
@@ -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
@@ -96,17 +97,7 @@ module Rigor
96
97
  read_only_keys.include?(key)
97
98
  end
98
99
 
99
- def top
100
- Trinary.no
101
- end
102
-
103
- def bot
104
- Trinary.no
105
- end
106
-
107
- def dynamic
108
- Trinary.no
109
- end
100
+ include Rigor::Type::PlainLattice
110
101
 
111
102
  include Rigor::Type::AcceptanceRouter
112
103
 
@@ -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
@@ -103,17 +104,7 @@ module Rigor
103
104
  "Integer"
104
105
  end
105
106
 
106
- def top
107
- Trinary.no
108
- end
109
-
110
- def bot
111
- Trinary.no
112
- end
113
-
114
- def dynamic
115
- Trinary.no
116
- end
107
+ include Rigor::Type::PlainLattice
117
108
 
118
109
  include Rigor::Type::AcceptanceRouter
119
110
 
@@ -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
@@ -62,17 +63,7 @@ module Rigor
62
63
  members.first.erase_to_rbs
63
64
  end
64
65
 
65
- def top
66
- Trinary.no
67
- end
68
-
69
- def bot
70
- Trinary.no
71
- end
72
-
73
- def dynamic
74
- Trinary.no
75
- end
66
+ include Rigor::Type::PlainLattice
76
67
 
77
68
  include Rigor::Type::AcceptanceRouter
78
69
 
@@ -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
@@ -52,17 +53,7 @@ module Rigor
52
53
  "#{class_name}[#{rendered}]"
53
54
  end
54
55
 
55
- def top
56
- Trinary.no
57
- end
58
-
59
- def bot
60
- Trinary.no
61
- end
62
-
63
- def dynamic
64
- Trinary.no
65
- end
56
+ include Rigor::Type::PlainLattice
66
57
 
67
58
  include Rigor::Type::AcceptanceRouter
68
59
 
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../trinary"
4
+
5
+ module Rigor
6
+ module Type
7
+ # Supplies the lattice-membership trio for the "plain" carriers — the
8
+ # concrete value types that are neither a lattice extreme (`Top` /
9
+ # `Bot` / `Dynamic`) nor a wrapper that computes membership from an
10
+ # inner type.
11
+ #
12
+ # Every such carrier answers `top` / `bot` / `dynamic` with the same
13
+ # `Trinary.no` ("this value is not that lattice point"), so the trio
14
+ # lived as a byte-identical copy in a dozen carriers. The extremes
15
+ # override the relevant member (`Top#top` / `Bot#bot` /
16
+ # `Dynamic#dynamic` answer `Trinary.yes`) and the delegators (`App`,
17
+ # `Difference`, `Refined`, `Union`) compute `dynamic` from their inner
18
+ # type(s); none of those include this module.
19
+ #
20
+ # Mirrors the existing {AcceptanceRouter} / `ValueSemantics` mixins —
21
+ # narrow trait sharing, never carrier inheritance (which the type-object
22
+ # contract forbids).
23
+ module PlainLattice
24
+ def top
25
+ Trinary.no
26
+ end
27
+
28
+ def bot
29
+ Trinary.no
30
+ end
31
+
32
+ def dynamic
33
+ Trinary.no
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "prism"
4
+
3
5
  require_relative "../trinary"
4
6
  require_relative "../value_semantics"
5
7
  require_relative "acceptance_router"
@@ -104,18 +106,31 @@ module Rigor
104
106
  # so callers can pass any `Constant#value` without a
105
107
  # type-prefilter.
106
108
  #
107
- # Plugin-contributed predicates land here once ADR-2 is
108
- # in flight; today the table is closed over the v0.0.4
109
- # built-in catalogue.
109
+ # Plugin-contributed predicates are not yet wired; today
110
+ # the table covers the built-in catalogue.
110
111
  #
111
112
  # Recogniser policy:
112
113
  #
113
- # - `:numeric` is deliberately conservative only decimal
114
- # integer and plain-decimal-fraction strings are
115
- # recognised, mirroring `imported-built-in-types.md`'s
116
- # "Rigor's numeric-string predicate" wording. Looser
117
- # forms (scientific, hex, rational) MAY join later
118
- # without breaking the registry contract.
114
+ # - `:numeric` recognises a string that is a *single Ruby
115
+ # numeric literal* exactly the syntax that, written in
116
+ # Ruby source, evaluates to an `Integer` / `Float` /
117
+ # `Rational` / `Complex`. The recogniser delegates to the
118
+ # real Ruby parser ({Refined.ruby_numeric_literal?} via
119
+ # Prism), so it tracks Ruby's grammar precisely: decimal /
120
+ # `0x` hex / `0o` (or leading-zero) octal / `0b` binary /
121
+ # `0d` decimal integers, underscore digit separators
122
+ # (`1_000`), decimal fractions and scientific floats
123
+ # (`1.5`, `1E-5`), and the `r` rational / `i` imaginary
124
+ # suffixes (`1r`, `2i`, `0xffr`). A single leading sign is
125
+ # folded into the literal (`-1`, `+1.5`), but a doubled
126
+ # sign (`--1`, `++1`) parses as a unary-operator chain — a
127
+ # `CallNode`, not a literal — and is rejected, as are
128
+ # multi-dot junk (`1.2.3`), partial literals (`0x`, `1_`),
129
+ # whitespace-padded strings, and — crucially — non-ASCII
130
+ # "digits" (full-width `1`, superscript `²`, other Unicode
131
+ # number characters): Ruby's lexer only accepts `[0-9]` in
132
+ # a numeric literal, so those are `CallNode`s too. The
133
+ # stricter base-N predicates below remain proper subsets.
119
134
  # - `:decimal_int` is "what `Integer(s, 10)` would parse
120
135
  # without remainder" — one or more decimal digits,
121
136
  # optional leading sign, no whitespace, no fractional
@@ -127,20 +142,64 @@ module Rigor
127
142
  # not octal-int-string. This matches the typical user
128
143
  # intent — a refinement marks a string that "looks like
129
144
  # octal", not "happens to be base-8 valid".
130
- NUMERIC_STRING_PATTERN = /\A-?\d+(?:\.\d+)?\z/
131
145
  DECIMAL_INT_STRING_PATTERN = /\A-?\d+\z/
132
146
  OCTAL_INT_STRING_PATTERN = /\A-?(?:0[oO][0-7]+|0[0-7]+)\z/
133
147
  HEX_INT_STRING_PATTERN = /\A-?0[xX][0-9a-fA-F]+\z/
134
- private_constant :NUMERIC_STRING_PATTERN, :DECIMAL_INT_STRING_PATTERN,
148
+ private_constant :DECIMAL_INT_STRING_PATTERN,
135
149
  :OCTAL_INT_STRING_PATTERN, :HEX_INT_STRING_PATTERN
136
150
 
151
+ # Prism node classes that represent a numeric literal. A
152
+ # string is a numeric-string exactly when the parser reduces
153
+ # the whole input to a single one of these (the leading sign
154
+ # is already folded into the literal by the parser).
155
+ NUMERIC_LITERAL_NODES = [
156
+ Prism::IntegerNode,
157
+ Prism::FloatNode,
158
+ Prism::RationalNode,
159
+ Prism::ImaginaryNode
160
+ ].freeze
161
+ private_constant :NUMERIC_LITERAL_NODES
162
+
163
+ # Cheap pre-filter applied before invoking the parser: every
164
+ # Ruby numeric literal starts with an ASCII digit, optionally
165
+ # preceded by exactly one sign. Strings that fail this never
166
+ # reach Prism (the common non-numeric case stays allocation-
167
+ # and parse-free).
168
+ NUMERIC_LITERAL_PREFIX = /\A[+-]?\d/
169
+ private_constant :NUMERIC_LITERAL_PREFIX
170
+
171
+ # @param value [Object] typically a `Constant#value`
172
+ # @return [Boolean] true when `value` is a String that is a
173
+ # single, complete Ruby numeric literal. Total over
174
+ # arbitrary input — never raises (Prism reports malformed
175
+ # input through `errors`, it does not throw).
176
+ def self.ruby_numeric_literal?(value)
177
+ return false unless value.is_a?(String)
178
+ return false if value.empty?
179
+ # A numeric literal carries no whitespace; reject any
180
+ # leading / trailing / interior space so the *whole* string
181
+ # must be the literal (Prism would otherwise accept a
182
+ # trailing-space `"1 "`).
183
+ return false if value.match?(/\s/)
184
+ return false unless value.match?(NUMERIC_LITERAL_PREFIX)
185
+
186
+ result = Prism.parse(value)
187
+ return false unless result.errors.empty?
188
+
189
+ body = result.value.statements&.body
190
+ return false unless body && body.size == 1
191
+
192
+ node = body.first
193
+ NUMERIC_LITERAL_NODES.any? { |klass| node.is_a?(klass) }
194
+ end
195
+
137
196
  PREDICATES = {
138
197
  lowercase: ->(v) { v.is_a?(String) && v == v.downcase },
139
198
  not_lowercase: ->(v) { v.is_a?(String) && v != v.downcase },
140
199
  uppercase: ->(v) { v.is_a?(String) && v == v.upcase },
141
200
  not_uppercase: ->(v) { v.is_a?(String) && v != v.upcase },
142
- numeric: ->(v) { v.is_a?(String) && NUMERIC_STRING_PATTERN.match?(v) },
143
- not_numeric: ->(v) { v.is_a?(String) && !NUMERIC_STRING_PATTERN.match?(v) },
201
+ numeric: ->(v) { ruby_numeric_literal?(v) },
202
+ not_numeric: ->(v) { v.is_a?(String) && !ruby_numeric_literal?(v) },
144
203
  decimal_int: ->(v) { v.is_a?(String) && DECIMAL_INT_STRING_PATTERN.match?(v) },
145
204
  octal_int: ->(v) { v.is_a?(String) && OCTAL_INT_STRING_PATTERN.match?(v) },
146
205
  hex_int: ->(v) { v.is_a?(String) && HEX_INT_STRING_PATTERN.match?(v) },
@@ -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
@@ -34,17 +35,7 @@ module Rigor
34
35
  "singleton(#{class_name})"
35
36
  end
36
37
 
37
- def top
38
- Trinary.no
39
- end
40
-
41
- def bot
42
- Trinary.no
43
- end
44
-
45
- def dynamic
46
- Trinary.no
47
- end
38
+ include Rigor::Type::PlainLattice
48
39
 
49
40
  include Rigor::Type::AcceptanceRouter
50
41