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.
- checksums.yaml +4 -4
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -23
- data/lib/rigor/analysis/check_rules/rule_walk.rb +3 -21
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
- data/lib/rigor/analysis/check_rules.rb +492 -71
- data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
- data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
- data/lib/rigor/analysis/fact_store.rb +5 -4
- data/lib/rigor/analysis/rule_catalog.rb +153 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +17 -17
- data/lib/rigor/analysis/runner/project_pre_passes.rb +9 -8
- data/lib/rigor/analysis/runner.rb +17 -6
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
- data/lib/rigor/analysis/worker_session.rb +10 -14
- data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
- data/lib/rigor/cache/store.rb +5 -3
- data/lib/rigor/cli/annotate_command.rb +28 -7
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/check_command.rb +115 -16
- data/lib/rigor/cli/coverage_command.rb +148 -16
- data/lib/rigor/cli/coverage_scan.rb +57 -0
- data/lib/rigor/cli/explain_command.rb +2 -0
- data/lib/rigor/cli/lsp_command.rb +3 -7
- data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
- data/lib/rigor/cli/mutation_protection_report.rb +73 -0
- data/lib/rigor/cli/options.rb +9 -0
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/protection_renderer.rb +63 -0
- data/lib/rigor/cli/protection_report.rb +68 -0
- data/lib/rigor/cli/sig_gen_command.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +2 -1
- data/lib/rigor/cli/triage_command.rb +2 -1
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_command.rb +2 -1
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/configuration/dependencies.rb +2 -4
- data/lib/rigor/configuration.rb +45 -7
- data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
- data/lib/rigor/environment/class_registry.rb +4 -3
- data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
- data/lib/rigor/environment/lockfile_resolver.rb +1 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
- data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
- data/lib/rigor/environment/rbs_loader.rb +49 -5
- data/lib/rigor/environment.rb +17 -7
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution.rb +3 -5
- data/lib/rigor/inference/acceptance.rb +17 -9
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
- data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
- data/lib/rigor/inference/expression_typer.rb +20 -28
- data/lib/rigor/inference/hkt_body.rb +8 -11
- data/lib/rigor/inference/hkt_body_parser.rb +10 -12
- data/lib/rigor/inference/hkt_registry.rb +10 -11
- data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +156 -21
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +90 -15
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
- data/lib/rigor/inference/method_dispatcher.rb +40 -48
- data/lib/rigor/inference/mutation_widening.rb +5 -11
- data/lib/rigor/inference/narrowing.rb +14 -16
- data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
- data/lib/rigor/inference/project_patched_methods.rb +4 -7
- data/lib/rigor/inference/project_patched_scanner.rb +2 -13
- data/lib/rigor/inference/protection_scanner.rb +86 -0
- data/lib/rigor/inference/scope_indexer.rb +129 -55
- data/lib/rigor/inference/statement_evaluator.rb +244 -114
- data/lib/rigor/inference/struct_fold_safety.rb +181 -0
- data/lib/rigor/inference/synthetic_method.rb +7 -7
- data/lib/rigor/language_server/completion_provider.rb +6 -12
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
- data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
- data/lib/rigor/language_server/hover_provider.rb +2 -3
- data/lib/rigor/language_server/hover_renderer.rb +2 -11
- data/lib/rigor/language_server/server.rb +9 -17
- data/lib/rigor/language_server.rb +4 -5
- data/lib/rigor/plugin/base.rb +10 -8
- data/lib/rigor/plugin/macro/block_as_method.rb +3 -4
- data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
- data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
- data/lib/rigor/plugin/macro.rb +4 -5
- data/lib/rigor/plugin/manifest.rb +45 -66
- data/lib/rigor/plugin/registry.rb +6 -7
- data/lib/rigor/plugin/type_node_resolver.rb +6 -8
- data/lib/rigor/protection/mutation_scanner.rb +120 -0
- data/lib/rigor/protection/mutator.rb +246 -0
- data/lib/rigor/rbs_extended.rb +24 -36
- data/lib/rigor/reflection.rb +4 -7
- data/lib/rigor/scope/discovery_index.rb +14 -2
- data/lib/rigor/scope.rb +54 -11
- data/lib/rigor/sig_gen/observed_call.rb +3 -3
- data/lib/rigor/sig_gen/writer.rb +40 -2
- data/lib/rigor/source/constant_path.rb +62 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/bound_method.rb +2 -11
- data/lib/rigor/type/combinator.rb +16 -3
- data/lib/rigor/type/constant.rb +2 -11
- data/lib/rigor/type/data_class.rb +2 -11
- data/lib/rigor/type/data_instance.rb +2 -11
- data/lib/rigor/type/hash_shape.rb +2 -11
- data/lib/rigor/type/integer_range.rb +2 -11
- data/lib/rigor/type/intersection.rb +2 -11
- data/lib/rigor/type/nominal.rb +2 -11
- data/lib/rigor/type/plain_lattice.rb +37 -0
- data/lib/rigor/type/refined.rb +72 -13
- data/lib/rigor/type/singleton.rb +2 -11
- data/lib/rigor/type/struct_class.rb +75 -0
- data/lib/rigor/type/struct_instance.rb +93 -0
- data/lib/rigor/type/tuple.rb +5 -15
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +3 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +7 -10
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +6 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -2
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +7 -9
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +3 -3
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +5 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +19 -14
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +28 -41
- data/sig/rigor/scope.rbs +9 -1
- data/sig/rigor/type.rbs +36 -1
- metadata +19 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../../type"
|
|
4
4
|
require_relative "singleton_folding"
|
|
5
|
+
require_relative "member_shape_projection"
|
|
5
6
|
|
|
6
7
|
module Rigor
|
|
7
8
|
module Inference
|
|
@@ -30,6 +31,10 @@ module Rigor
|
|
|
30
31
|
module DataFolding
|
|
31
32
|
module_function
|
|
32
33
|
|
|
34
|
+
# The `[]` / `to_h` / `deconstruct` / `members` / `with` projections
|
|
35
|
+
# and the reader-redefinition guard are shared with {StructFolding}.
|
|
36
|
+
extend MemberShapeProjection
|
|
37
|
+
|
|
33
38
|
# @return [Rigor::Type, nil] the folded result, or nil to defer.
|
|
34
39
|
def try_dispatch(context)
|
|
35
40
|
receiver = context.receiver
|
|
@@ -165,81 +170,12 @@ module Rigor
|
|
|
165
170
|
when :deconstruct then instance_deconstruct(instance)
|
|
166
171
|
when :deconstruct_keys then instance_deconstruct_keys(instance, args)
|
|
167
172
|
when :members then instance_members(instance)
|
|
168
|
-
when :with
|
|
173
|
+
when :with
|
|
174
|
+
instance_with(instance, args) do |members, class_name|
|
|
175
|
+
Type::Combinator.data_instance_of(members: members, class_name: class_name)
|
|
176
|
+
end
|
|
169
177
|
end
|
|
170
178
|
end
|
|
171
|
-
|
|
172
|
-
# A `Data.define` class body (the `class Point < Data.define(:x);
|
|
173
|
-
# def x; …; end; end` subclass body, or a `Const = Data.define(:x) do
|
|
174
|
-
# def x; …; end; end` block) can redefine a member's synthesised
|
|
175
|
-
# reader. When it does, `inst.x` runs that `def`, NOT the member, so
|
|
176
|
-
# folding the read to the member type would be unsound (a downstream
|
|
177
|
-
# FP). Both named forms register the override as a real `def` node
|
|
178
|
-
# under the class name, so an entry in the project def-node table is
|
|
179
|
-
# the discriminator (the synthesised reader has no def node). The
|
|
180
|
-
# value accessors `[]` / `to_h` / `deconstruct` bypass the reader and
|
|
181
|
-
# stay foldable, so this gate is on the bare member read only.
|
|
182
|
-
def reader_overridden?(instance, method_name, scope)
|
|
183
|
-
class_name = instance.class_name
|
|
184
|
-
return false if class_name.nil? || scope.nil?
|
|
185
|
-
|
|
186
|
-
!scope.user_def_for(class_name, method_name).nil?
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
def instance_index(instance, args)
|
|
190
|
-
return nil unless args.size == 1
|
|
191
|
-
|
|
192
|
-
arg = args.first
|
|
193
|
-
return nil unless arg.is_a?(Type::Constant)
|
|
194
|
-
|
|
195
|
-
key = arg.value
|
|
196
|
-
case key
|
|
197
|
-
when Symbol
|
|
198
|
-
instance.members[key]
|
|
199
|
-
when Integer
|
|
200
|
-
values = instance.members.values
|
|
201
|
-
idx = key.negative? ? key + values.size : key
|
|
202
|
-
values[idx] if idx && idx >= 0 && idx < values.size
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
def instance_to_h(instance)
|
|
207
|
-
Type::Combinator.hash_shape_of(instance.members.dup)
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
def instance_deconstruct(instance)
|
|
211
|
-
Type::Combinator.tuple_of(*instance.members.values)
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
# `deconstruct_keys(nil)` / `deconstruct_keys([:x])` both yield a
|
|
215
|
-
# subset of the member map; the conservative, always-correct answer
|
|
216
|
-
# is the full closed member shape.
|
|
217
|
-
def instance_deconstruct_keys(instance, args)
|
|
218
|
-
return nil unless args.size <= 1
|
|
219
|
-
|
|
220
|
-
Type::Combinator.hash_shape_of(instance.members.dup)
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
def instance_members(instance)
|
|
224
|
-
Type::Combinator.tuple_of(*instance.member_names.map { |name| Type::Combinator.constant_of(name) })
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
# `Data#with(x: 9)` returns a new frozen copy with the named members
|
|
228
|
-
# overridden. Only a closed keyword `HashShape` whose keys are a
|
|
229
|
-
# subset of the members folds; anything else defers (RBS resolves
|
|
230
|
-
# `with` to `self`, returning the unchanged instance type).
|
|
231
|
-
def instance_with(instance, args)
|
|
232
|
-
return instance if args.empty?
|
|
233
|
-
return nil unless args.size == 1
|
|
234
|
-
|
|
235
|
-
shape = args.first
|
|
236
|
-
return nil unless shape.is_a?(Type::HashShape) && shape.closed?
|
|
237
|
-
return nil unless shape.optional_keys.empty?
|
|
238
|
-
return nil unless shape.pairs.keys.all? { |key| instance.members.key?(key) }
|
|
239
|
-
|
|
240
|
-
merged = instance.members.merge(shape.pairs)
|
|
241
|
-
Type::Combinator.data_instance_of(members: merged, class_name: instance.class_name)
|
|
242
|
-
end
|
|
243
179
|
end
|
|
244
180
|
end
|
|
245
181
|
end
|
|
@@ -54,11 +54,10 @@ module Rigor
|
|
|
54
54
|
# *correctness-preservingly* proved" excludes Constants whose
|
|
55
55
|
# value is host-specific.
|
|
56
56
|
module FileFolding
|
|
57
|
-
# File class methods
|
|
58
|
-
#
|
|
59
|
-
# platform-sensitive
|
|
60
|
-
#
|
|
61
|
-
# opt-in flag for any of them to fire.
|
|
57
|
+
# File class methods the analyzer can fold when the opt-in
|
|
58
|
+
# flag is set. Currently identical to PLATFORM_DEPENDENT_METHODS
|
|
59
|
+
# — separated for a future non-platform-sensitive tier that
|
|
60
|
+
# can fold without the opt-in flag.
|
|
62
61
|
FILE_PURE_CLASS_METHODS = Set[
|
|
63
62
|
:basename,
|
|
64
63
|
:dirname,
|
|
@@ -72,8 +71,8 @@ module Rigor
|
|
|
72
71
|
# Methods whose result depends on host directory-separator
|
|
73
72
|
# semantics (`/` on POSIX, `/` AND `\` on Windows, drive
|
|
74
73
|
# letters, UNC paths). Folding these would bake the
|
|
75
|
-
# analyzer-host's platform into the inferred type. The opt-
|
|
76
|
-
#
|
|
74
|
+
# analyzer-host's platform into the inferred type. The opt-in
|
|
75
|
+
# flag below controls whether to do it anyway.
|
|
77
76
|
PLATFORM_DEPENDENT_METHODS = Set[
|
|
78
77
|
:basename, :dirname, :extname, :join, :split, :absolute_path?
|
|
79
78
|
].freeze
|
|
@@ -175,24 +175,18 @@ module Rigor
|
|
|
175
175
|
type.is_a?(Type::Constant) && type.value.is_a?(Symbol)
|
|
176
176
|
end
|
|
177
177
|
|
|
178
|
-
# Element-yielding Enumerable methods covered as a
|
|
179
|
-
#
|
|
180
|
-
#
|
|
181
|
-
#
|
|
182
|
-
#
|
|
183
|
-
#
|
|
184
|
-
# `Tuple[K, V]` pair rather than the projected
|
|
178
|
+
# Element-yielding Enumerable methods covered as a placeholder.
|
|
179
|
+
# RBS already binds the block parameter correctly for plain
|
|
180
|
+
# `Array[T]` / `Set[T]` / `Range[T]` receivers via generic
|
|
181
|
+
# substitution; this tier exists so Tuple- and HashShape-shaped
|
|
182
|
+
# receivers reach the block body with the precise per-position
|
|
183
|
+
# element union / `Tuple[K, V]` pair rather than the projected
|
|
185
184
|
# `Array[union]` / `Hash[K, V]` widening.
|
|
186
185
|
#
|
|
187
|
-
# NOTE (
|
|
188
|
-
#
|
|
189
|
-
#
|
|
190
|
-
#
|
|
191
|
-
# after PHPStan's extension API (ADR-2). The placeholders
|
|
192
|
-
# below stay until the plugin surface is in place; once it
|
|
193
|
-
# ships, this dispatcher loses these arms and the
|
|
194
|
-
# equivalent rules move into a built-in plugin loaded at
|
|
195
|
-
# boot.
|
|
186
|
+
# NOTE: `Plugin::NodeRuleWalk` (ADR-52 WD4) is now in place as
|
|
187
|
+
# the intended migration target for these Enumerable projections.
|
|
188
|
+
# The four methods (group_by, partition, each_slice, each_cons)
|
|
189
|
+
# remain here pending that migration.
|
|
196
190
|
def single_element_block_params(receiver)
|
|
197
191
|
element = element_type_of(receiver)
|
|
198
192
|
return nil if element.nil?
|
|
@@ -43,14 +43,21 @@ module Rigor
|
|
|
43
43
|
private_constant :NUMERIC_CONSTRUCTORS
|
|
44
44
|
|
|
45
45
|
# `Kernel#Integer(s)` predicate-aware refinement set
|
|
46
|
-
# (v0.1.1 Track 1 slice 2b).
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
|
|
46
|
+
# (v0.1.1 Track 1 slice 2b). `decimal-int-string` is the
|
|
47
|
+
# only string refinement whose every inhabitant `Integer(s)`
|
|
48
|
+
# parses without remainder, so the result is a plain
|
|
49
|
+
# `Integer` — but NOT `non-negative-int`: the predicate
|
|
50
|
+
# `/\A-?\d+\z/` admits a leading sign, so `"-7"` is a valid
|
|
51
|
+
# decimal-int-string and `Integer("-7") == -7 < 0`. The
|
|
52
|
+
# narrowing is total (every inhabitant parses) but not `>= 0`,
|
|
53
|
+
# so it lands on `universal_int`. `numeric-string` is
|
|
54
|
+
# deliberately NOT in this set at all: since it was widened to
|
|
55
|
+
# the full Ruby numeric-literal grammar (floats, hex, rational,
|
|
56
|
+
# imaginary, signs), `Integer(numeric_string)` would raise for
|
|
57
|
+
# a `"1.5"` / `"2i"` inhabitant — not even total — so it falls
|
|
58
|
+
# through to RBS `Integer`. The `Integer(s, base)` overload is
|
|
59
|
+
# left for a later slice.
|
|
60
|
+
INTEGER_REFINEMENT_PREDICATES = Set[:decimal_int].freeze
|
|
54
61
|
private_constant :INTEGER_REFINEMENT_PREDICATES
|
|
55
62
|
|
|
56
63
|
def try_dispatch(context)
|
|
@@ -70,7 +77,7 @@ module Rigor
|
|
|
70
77
|
# paths, tried in order:
|
|
71
78
|
#
|
|
72
79
|
# 1. A `Refined[String, predicate]` argument whose predicate
|
|
73
|
-
# is a
|
|
80
|
+
# is a total-parse carrier narrows to `universal_int`
|
|
74
81
|
# (see {try_integer_from_refinement}).
|
|
75
82
|
# 2. A `Constant` String or Numeric argument — optionally
|
|
76
83
|
# with a `Constant[Integer]` base — runs the actual
|
|
@@ -120,9 +127,14 @@ module Rigor
|
|
|
120
127
|
# `Kernel#Integer(s)` over a `Refined[String, predicate]`
|
|
121
128
|
# whose predicate is in {INTEGER_REFINEMENT_PREDICATES}.
|
|
122
129
|
# Mirrors the `String#to_i` projection in `ShapeDispatch`
|
|
123
|
-
# (v0.1.1 slice 2a) — the result is
|
|
124
|
-
# `non-negative-int
|
|
125
|
-
# so the
|
|
130
|
+
# (v0.1.1 slice 2a) — the result is `universal_int`, NOT
|
|
131
|
+
# `non-negative-int`: a decimal-int-string admits a leading
|
|
132
|
+
# sign (`"-7"`), so the parsed Integer can be negative. The
|
|
133
|
+
# carrier stays an `IntegerRange` (rather than declining to
|
|
134
|
+
# the RBS `Nominal[Integer]`) so downstream range narrowing
|
|
135
|
+
# still has a range to intersect. Returns nil for any other
|
|
136
|
+
# arg shape so the RBS tier handles the generic `Integer(arg)`
|
|
137
|
+
# case.
|
|
126
138
|
def try_integer_from_refinement(args)
|
|
127
139
|
return nil unless args.size == 1
|
|
128
140
|
|
|
@@ -133,7 +145,7 @@ module Rigor
|
|
|
133
145
|
return nil unless base.is_a?(Type::Nominal) && base.class_name == "String"
|
|
134
146
|
return nil unless INTEGER_REFINEMENT_PREDICATES.include?(arg.predicate_id)
|
|
135
147
|
|
|
136
|
-
Type::Combinator.
|
|
148
|
+
Type::Combinator.universal_int
|
|
137
149
|
end
|
|
138
150
|
|
|
139
151
|
def try_array(args)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module MethodDispatcher
|
|
8
|
+
# The member-shape projections shared by {DataFolding} and
|
|
9
|
+
# {StructFolding}. A `DataInstance` and a `StructInstance` expose the
|
|
10
|
+
# same surface — an ordered `members` map, `member_names`, and a
|
|
11
|
+
# `class_name` — so the value projections off that surface
|
|
12
|
+
# (`[]` / `to_h` / `deconstruct` / `deconstruct_keys` / `members` /
|
|
13
|
+
# `with`) and the reader-redefinition guard are identical between the
|
|
14
|
+
# two folders. Only `#with`'s carrier constructor differs
|
|
15
|
+
# (`data_instance_of` vs `struct_instance_of`), so it takes a block
|
|
16
|
+
# that builds the new instance from the merged member map.
|
|
17
|
+
#
|
|
18
|
+
# Both folders `extend` this module so the projections resolve as
|
|
19
|
+
# their own module functions (matching their `module_function` style).
|
|
20
|
+
module MemberShapeProjection
|
|
21
|
+
# A Data/Struct subclass body can redefine a member's synthesised
|
|
22
|
+
# reader (`def x`); when it does, `inst.x` runs that `def`, not the
|
|
23
|
+
# member, so folding the read would be unsound. A real `def` node
|
|
24
|
+
# under the class name is the discriminator (the synthesised reader
|
|
25
|
+
# has none), so an entry in the project def-node table gates the
|
|
26
|
+
# bare member read off.
|
|
27
|
+
def reader_overridden?(instance, method_name, scope)
|
|
28
|
+
class_name = instance.class_name
|
|
29
|
+
return false if class_name.nil? || scope.nil?
|
|
30
|
+
|
|
31
|
+
!scope.user_def_for(class_name, method_name).nil?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def instance_index(instance, args)
|
|
35
|
+
return nil unless args.size == 1
|
|
36
|
+
|
|
37
|
+
arg = args.first
|
|
38
|
+
return nil unless arg.is_a?(Type::Constant)
|
|
39
|
+
|
|
40
|
+
key = arg.value
|
|
41
|
+
case key
|
|
42
|
+
when Symbol
|
|
43
|
+
instance.members[key]
|
|
44
|
+
when Integer
|
|
45
|
+
values = instance.members.values
|
|
46
|
+
idx = key.negative? ? key + values.size : key
|
|
47
|
+
values[idx] if idx && idx >= 0 && idx < values.size
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def instance_to_h(instance)
|
|
52
|
+
Type::Combinator.hash_shape_of(instance.members.dup)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def instance_deconstruct(instance)
|
|
56
|
+
Type::Combinator.tuple_of(*instance.members.values)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# `deconstruct_keys(nil)` / `deconstruct_keys([:x])` both yield a
|
|
60
|
+
# subset of the member map; the conservative, always-correct answer
|
|
61
|
+
# is the full closed member shape.
|
|
62
|
+
def instance_deconstruct_keys(instance, args)
|
|
63
|
+
return nil unless args.size <= 1
|
|
64
|
+
|
|
65
|
+
Type::Combinator.hash_shape_of(instance.members.dup)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def instance_members(instance)
|
|
69
|
+
Type::Combinator.tuple_of(*instance.member_names.map { |name| Type::Combinator.constant_of(name) })
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# `#with(x: 9)` returns a new copy with the named members
|
|
73
|
+
# overridden. Only a closed keyword `HashShape` whose keys are a
|
|
74
|
+
# subset of the members folds; anything else defers (RBS resolves
|
|
75
|
+
# `with` to `self`, returning the unchanged instance type). The
|
|
76
|
+
# carrier constructor differs per folder, so the caller supplies it
|
|
77
|
+
# as a block taking the merged member map and the class name.
|
|
78
|
+
def instance_with(instance, args)
|
|
79
|
+
return instance if args.empty?
|
|
80
|
+
return nil unless args.size == 1
|
|
81
|
+
|
|
82
|
+
shape = args.first
|
|
83
|
+
return nil unless shape.is_a?(Type::HashShape) && shape.closed?
|
|
84
|
+
return nil unless shape.optional_keys.empty?
|
|
85
|
+
return nil unless shape.pairs.keys.all? { |key| instance.members.key?(key) }
|
|
86
|
+
|
|
87
|
+
merged = instance.members.merge(shape.pairs)
|
|
88
|
+
yield(merged, instance.class_name)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -25,9 +25,7 @@ module Rigor
|
|
|
25
25
|
# so without this preference, an alias-typed overload like
|
|
26
26
|
# `Array#[](::int) -> Elem` would beat the strict
|
|
27
27
|
# `Array#[](Range) -> Array[Elem]?` overload for a Range
|
|
28
|
-
# argument.
|
|
29
|
-
# "Interface-strictness on overload selection" item in
|
|
30
|
-
# `docs/ROADMAP.md`.)
|
|
28
|
+
# argument.
|
|
31
29
|
# 3. **Pass 2 — gradual fall-back.** If no fully strict overload
|
|
32
30
|
# matches, accept the first arity-and-gradual-accept match
|
|
33
31
|
# (the v0.1.1 behaviour). Alias / Interface / Intersection
|
|
@@ -45,8 +45,9 @@ module Rigor
|
|
|
45
45
|
# machinery works without duplication: `Tuple[Integer, String]`
|
|
46
46
|
# dispatches as `Array[Integer | String]`, and
|
|
47
47
|
# `HashShape{a: Integer}` dispatches as `Hash[Symbol, Integer]`.
|
|
48
|
-
# Tuple
|
|
49
|
-
# member)
|
|
48
|
+
# Tuple/HashShape element precision (e.g., `tuple[0]` returning
|
|
49
|
+
# the precise member) is handled by the preceding `ShapeDispatch`
|
|
50
|
+
# tier.
|
|
50
51
|
#
|
|
51
52
|
# Remaining limitations:
|
|
52
53
|
#
|
|
@@ -107,17 +108,11 @@ module Rigor
|
|
|
107
108
|
# @return [Rigor::Type, nil] inferred return type, or `nil`
|
|
108
109
|
# when no rule resolves (no class name, no method, dispatch
|
|
109
110
|
# on a Top/Dynamic[Top] receiver, etc.).
|
|
110
|
-
# @param scope [Rigor::Scope, nil] when supplied, enables
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
# that ancestor's RBS. `nil` (the default for every caller
|
|
116
|
-
# that does not thread a scope) keeps the legacy behaviour —
|
|
117
|
-
# such an inherited call stays unresolved and degrades to
|
|
118
|
-
# `Dynamic[Top]`, which is the false-positive-safe default
|
|
119
|
-
# for the open hierarchies (`< ActionController::Base`, …)
|
|
120
|
-
# the allow-list deliberately excludes.
|
|
111
|
+
# @param scope [Rigor::Scope, nil] when supplied, enables ADR-43
|
|
112
|
+
# RBS-complete-ancestor resolution against
|
|
113
|
+
# `ALLOWED_RBS_COMPLETE_ANCESTORS`. `nil` keeps inherited calls
|
|
114
|
+
# unresolved (`Dynamic[Top]`) — the FP-safe default for open
|
|
115
|
+
# hierarchies (`< ActionController::Base`, …).
|
|
121
116
|
def try_dispatch(context)
|
|
122
117
|
environment = context.environment
|
|
123
118
|
return nil if environment.nil?
|
|
@@ -249,15 +244,8 @@ module Rigor
|
|
|
249
244
|
["Array", :instance, tuple_type_args(receiver)]
|
|
250
245
|
when Type::HashShape
|
|
251
246
|
["Hash", :instance, hash_shape_type_args(receiver)]
|
|
252
|
-
when Type::DataInstance
|
|
253
|
-
|
|
254
|
-
# class (or the `Data` supertype) so non-member calls
|
|
255
|
-
# (`inspect`, `==`, `frozen?`, ...) resolve through RBS
|
|
256
|
-
# rather than mis-firing undefined-method. Member reads were
|
|
257
|
-
# already folded by DataFolding above this tier.
|
|
258
|
-
[receiver.class_name || "Data", :instance, []]
|
|
259
|
-
when Type::DataClass
|
|
260
|
-
[receiver.class_name || "Data", :singleton, []]
|
|
247
|
+
when Type::DataInstance, Type::DataClass, Type::StructInstance, Type::StructClass
|
|
248
|
+
member_carrier_descriptor(receiver)
|
|
261
249
|
when Type::BoundMethod
|
|
262
250
|
# `BoundMethod` is a precision-bearing alias for
|
|
263
251
|
# `Nominal[Method]`: it carries the
|
|
@@ -275,6 +263,20 @@ module Rigor
|
|
|
275
263
|
end
|
|
276
264
|
end
|
|
277
265
|
|
|
266
|
+
# ADR-48 — project a `Data`/`Struct` member carrier to its tagging
|
|
267
|
+
# class (or the `Data`/`Struct` supertype) so non-member calls
|
|
268
|
+
# (`inspect`, `==`, `frozen?`, ...) resolve through RBS rather than
|
|
269
|
+
# mis-firing undefined-method. Precise member reads were already
|
|
270
|
+
# folded by DataFolding / StructFolding above this tier.
|
|
271
|
+
def member_carrier_descriptor(receiver)
|
|
272
|
+
case receiver
|
|
273
|
+
when Type::DataInstance then [receiver.class_name || "Data", :instance, []]
|
|
274
|
+
when Type::DataClass then [receiver.class_name || "Data", :singleton, []]
|
|
275
|
+
when Type::StructInstance then [receiver.class_name || "Struct", :instance, []]
|
|
276
|
+
when Type::StructClass then [receiver.class_name || "Struct", :singleton, []]
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
278
280
|
def tuple_type_args(tuple)
|
|
279
281
|
return [] if tuple.elements.empty?
|
|
280
282
|
|
|
@@ -81,6 +81,7 @@ module Rigor
|
|
|
81
81
|
sum: :tuple_sum,
|
|
82
82
|
min: :tuple_min,
|
|
83
83
|
max: :tuple_max,
|
|
84
|
+
minmax: :tuple_minmax_pair,
|
|
84
85
|
sort: :tuple_sort,
|
|
85
86
|
reverse: :tuple_reverse,
|
|
86
87
|
to_a: :tuple_to_a,
|
|
@@ -99,9 +100,15 @@ module Rigor
|
|
|
99
100
|
index: :tuple_find_index,
|
|
100
101
|
find_index: :tuple_find_index,
|
|
101
102
|
rindex: :tuple_rindex,
|
|
102
|
-
flatten: :tuple_flatten
|
|
103
|
+
flatten: :tuple_flatten,
|
|
104
|
+
join: :tuple_join
|
|
103
105
|
}.freeze
|
|
104
106
|
|
|
107
|
+
# Byte cap on a folded `tuple.join` result — a huge tuple times a
|
|
108
|
+
# long separator must not materialise an unbounded `Constant`.
|
|
109
|
+
TUPLE_JOIN_BYTE_LIMIT = 4096
|
|
110
|
+
private_constant :TUPLE_JOIN_BYTE_LIMIT
|
|
111
|
+
|
|
105
112
|
HASH_SHAPE_HANDLERS = {
|
|
106
113
|
size: :hash_size,
|
|
107
114
|
length: :hash_size,
|
|
@@ -573,8 +580,17 @@ module Rigor
|
|
|
573
580
|
%i[lowercase upcase] => :uppercase_string,
|
|
574
581
|
%i[uppercase upcase] => :refined_self,
|
|
575
582
|
%i[uppercase downcase] => :lowercase_string,
|
|
583
|
+
# `numeric-string` is the full Ruby numeric-literal
|
|
584
|
+
# grammar (since the predicate delegates to the
|
|
585
|
+
# parser). `#downcase` preserves it — lowercasing a
|
|
586
|
+
# literal (hex digits, `0X` / `E` prefixes) yields a
|
|
587
|
+
# valid lowercase literal — but `#upcase` does NOT:
|
|
588
|
+
# the rational / imaginary suffixes are lowercase-only
|
|
589
|
+
# (`"1r".upcase == "1R"` is not a literal), so `upcase`
|
|
590
|
+
# drops to the plain base `String` — still sound (the
|
|
591
|
+
# result is a String), just no longer numeric.
|
|
576
592
|
%i[numeric downcase] => :refined_self,
|
|
577
|
-
%i[numeric upcase] => :
|
|
593
|
+
%i[numeric upcase] => :base_string,
|
|
578
594
|
# Digit-only strings are case-invariant; the prefix
|
|
579
595
|
# letters in `0o…` / `0x…` are accepted by the
|
|
580
596
|
# predicate in either case so the predicate-subset
|
|
@@ -587,19 +603,19 @@ module Rigor
|
|
|
587
603
|
%i[hex_int downcase] => :refined_self,
|
|
588
604
|
%i[hex_int upcase] => :refined_self,
|
|
589
605
|
# v0.1.1 Track 1 slice 2 — `to_i` / `to_int` on a
|
|
590
|
-
#
|
|
591
|
-
#
|
|
592
|
-
#
|
|
593
|
-
#
|
|
594
|
-
#
|
|
595
|
-
#
|
|
596
|
-
#
|
|
597
|
-
#
|
|
598
|
-
#
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
%i[
|
|
602
|
-
%i[
|
|
606
|
+
# `decimal-int-string` parses to an `Integer`. The
|
|
607
|
+
# carrier is `universal_int`, NOT `non-negative-int`:
|
|
608
|
+
# the predicate `/\A-?\d+\z/` admits a leading sign, so
|
|
609
|
+
# `"-7"` is a valid decimal-int-string and
|
|
610
|
+
# `"-7".to_i == -7 < 0`. `String#to_i` is total (never
|
|
611
|
+
# raises), so the projection is sound — just signed.
|
|
612
|
+
# `numeric-string` is deliberately NOT projected to
|
|
613
|
+
# `to_i` at all: it now spans the full numeric-literal
|
|
614
|
+
# grammar, so a `"1.5"` / `"2i"` inhabitant has a
|
|
615
|
+
# fractional or non-Integer parse — it falls through to
|
|
616
|
+
# the RBS `Integer`.
|
|
617
|
+
%i[decimal_int to_i] => :universal_int,
|
|
618
|
+
%i[decimal_int to_int] => :universal_int
|
|
603
619
|
})
|
|
604
620
|
private_constant :REFINED_STRING_PROJECTIONS
|
|
605
621
|
|
|
@@ -624,6 +640,8 @@ module Rigor
|
|
|
624
640
|
when :uppercase_string then Type::Combinator.uppercase_string
|
|
625
641
|
when :lowercase_string then Type::Combinator.lowercase_string
|
|
626
642
|
when :non_negative_int then Type::Combinator.non_negative_int
|
|
643
|
+
when :universal_int then Type::Combinator.universal_int
|
|
644
|
+
when :base_string then refined.base
|
|
627
645
|
end
|
|
628
646
|
end
|
|
629
647
|
|
|
@@ -791,6 +809,37 @@ module Rigor
|
|
|
791
809
|
Type::Combinator.constant_of(values.sum)
|
|
792
810
|
end
|
|
793
811
|
|
|
812
|
+
# `tuple.join(sep = "")` — fold to the joined `Constant[String]`
|
|
813
|
+
# when every element is a `Constant` (its `to_s` is deterministic
|
|
814
|
+
# for the scalar value classes) and the separator is absent or a
|
|
815
|
+
# `Constant[String]`. Capped at `TUPLE_JOIN_BYTE_LIMIT`.
|
|
816
|
+
def tuple_join(tuple, _method_name, args)
|
|
817
|
+
sep = tuple_join_separator(args)
|
|
818
|
+
return nil if sep.nil?
|
|
819
|
+
|
|
820
|
+
values = constant_values(tuple.elements)
|
|
821
|
+
return nil if values.nil?
|
|
822
|
+
|
|
823
|
+
result = values.join(sep)
|
|
824
|
+
return nil if result.bytesize > TUPLE_JOIN_BYTE_LIMIT
|
|
825
|
+
|
|
826
|
+
Type::Combinator.constant_of(result)
|
|
827
|
+
rescue StandardError
|
|
828
|
+
nil
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
# The join separator: `""` for the no-arg form, the value of a
|
|
832
|
+
# single `Constant[String]` arg, or `nil` to decline.
|
|
833
|
+
def tuple_join_separator(args)
|
|
834
|
+
return "" if args.empty?
|
|
835
|
+
return nil unless args.size == 1
|
|
836
|
+
|
|
837
|
+
arg = args.first
|
|
838
|
+
return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(String)
|
|
839
|
+
|
|
840
|
+
arg.value
|
|
841
|
+
end
|
|
842
|
+
|
|
794
843
|
# `tuple.min` / `tuple.max` — fold when every element is
|
|
795
844
|
# a `Constant` whose values share a Ruby-comparable
|
|
796
845
|
# domain. Empty tuples fold to `Constant[nil]`.
|
|
@@ -815,6 +864,32 @@ module Rigor
|
|
|
815
864
|
nil
|
|
816
865
|
end
|
|
817
866
|
|
|
867
|
+
# `tuple.minmax` — the `[min, max]` pair as a 2-slot
|
|
868
|
+
# `Tuple[Constant[min], Constant[max]]`, mirroring the
|
|
869
|
+
# `Range#minmax` fold. Every element must be a `Constant`
|
|
870
|
+
# and the values must Ruby-compare; an empty tuple folds to
|
|
871
|
+
# `Tuple[nil, nil]` (Ruby's `[].minmax`), incomparable
|
|
872
|
+
# mixed-class values decline.
|
|
873
|
+
def tuple_minmax_pair(tuple, _method_name, args)
|
|
874
|
+
return nil unless args.empty?
|
|
875
|
+
|
|
876
|
+
if tuple.elements.empty?
|
|
877
|
+
nil_const = Type::Combinator.constant_of(nil)
|
|
878
|
+
return Type::Combinator.tuple_of(nil_const, nil_const)
|
|
879
|
+
end
|
|
880
|
+
|
|
881
|
+
values = constant_values(tuple.elements)
|
|
882
|
+
return nil if values.nil?
|
|
883
|
+
|
|
884
|
+
low, high = values.minmax
|
|
885
|
+
Type::Combinator.tuple_of(
|
|
886
|
+
Type::Combinator.constant_of(low),
|
|
887
|
+
Type::Combinator.constant_of(high)
|
|
888
|
+
)
|
|
889
|
+
rescue StandardError
|
|
890
|
+
nil
|
|
891
|
+
end
|
|
892
|
+
|
|
818
893
|
# `tuple.sort` — every element must be a `Constant` and
|
|
819
894
|
# the values must Ruby-compare. The result is a Tuple
|
|
820
895
|
# with the same elements in sorted order. Comparison
|