rigortype 0.3.0 → 0.3.2
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/README.md +10 -10
- data/data/builtins/ruby_core/array.yml +416 -392
- data/data/builtins/ruby_core/file.yml +42 -42
- data/data/builtins/ruby_core/hash.yml +302 -302
- data/data/builtins/ruby_core/io.yml +191 -191
- data/data/builtins/ruby_core/numeric.yml +321 -366
- data/data/builtins/ruby_core/proc.yml +124 -124
- data/data/builtins/ruby_core/range.yml +21 -21
- data/data/builtins/ruby_core/rational.yml +39 -39
- data/data/builtins/ruby_core/re.yml +65 -65
- data/data/builtins/ruby_core/set.yml +106 -106
- data/data/builtins/ruby_core/struct.yml +14 -14
- data/data/core_overlay/pathname.rbs +5 -0
- data/data/core_overlay/resolv.rbs +31 -0
- data/data/core_overlay/string_scanner.rbs +11 -5
- data/data/gem_overlay/activesupport/core_ext.rbs +10 -6
- data/data/vendored_gem_sigs/bundler/bundler.rbs +39 -27
- data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +6 -0
- data/data/vendored_gem_sigs/prism/prism_supplement.rbs +7 -0
- data/data/vendored_gem_sigs/racc/racc.rbs +67 -0
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +20 -9
- data/docs/handbook/01-getting-started.md +22 -34
- data/docs/handbook/06-classes.md +1 -1
- data/docs/handbook/07-rbs-and-extended.md +76 -101
- data/docs/handbook/08-understanding-errors.md +114 -247
- data/docs/handbook/09-plugins.md +54 -144
- data/docs/handbook/README.md +5 -3
- data/docs/handbook/appendix-liskov.md +4 -2
- data/docs/handbook/appendix-phpstan.md +14 -7
- data/docs/handbook/appendix-steep.md +4 -2
- data/docs/handbook/appendix-type-theory.md +3 -1
- data/docs/manual/02-cli-reference.md +103 -11
- data/docs/manual/03-configuration.md +16 -1
- data/docs/manual/04-diagnostics.md +36 -4
- data/docs/manual/06-baseline.md +35 -1
- data/docs/manual/08-skills.md +6 -1
- data/docs/manual/09-editor-integration.md +3 -2
- data/docs/manual/12-caching.md +17 -5
- data/docs/manual/15-type-protection-coverage.md +122 -1
- data/docs/manual/plugins/rigor-actioncable.md +32 -0
- data/docs/manual/plugins/rigor-devise.md +4 -2
- data/docs/manual/plugins/rigor-rbs-inline.md +22 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -0
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +21 -2
- data/lib/rigor/analysis/check_rules.rb +79 -19
- data/lib/rigor/analysis/incremental_session.rb +156 -11
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +14 -0
- data/lib/rigor/analysis/run_cache_key.rb +27 -1
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +201 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +28 -10
- data/lib/rigor/analysis/runner/pool_coordinator.rb +7 -10
- data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -1
- data/lib/rigor/analysis/runner.rb +80 -8
- data/lib/rigor/bleeding_edge.rb +132 -7
- data/lib/rigor/cache/descriptor.rb +6 -1
- data/lib/rigor/cache/engine_source.rb +162 -0
- data/lib/rigor/cache/incremental_snapshot.rb +87 -18
- data/lib/rigor/cache/rbs_cache_producer.rb +11 -1
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +38 -0
- data/lib/rigor/cache/store.rb +99 -24
- data/lib/rigor/cli/check_command.rb +61 -34
- data/lib/rigor/cli/check_invocation.rb +84 -0
- data/lib/rigor/cli/coverage_command.rb +15 -2
- data/lib/rigor/cli/coverage_mutation.rb +242 -11
- data/lib/rigor/cli/doctor_command.rb +6 -8
- data/lib/rigor/cli/fused_protection_renderer.rb +10 -0
- data/lib/rigor/cli/fused_protection_report.rb +12 -3
- data/lib/rigor/cli/mutation_fork_scan.rb +64 -0
- data/lib/rigor/cli/mutation_protection_renderer.rb +12 -0
- data/lib/rigor/cli/mutation_protection_report.rb +26 -5
- data/lib/rigor/cli/protection_renderer.rb +13 -0
- data/lib/rigor/cli/protection_report.rb +11 -3
- data/lib/rigor/cli/show_bleedingedge_command.rb +17 -4
- data/lib/rigor/cli/sig_gen_command.rb +5 -4
- data/lib/rigor/cli/skill_command.rb +21 -1
- data/lib/rigor/cli/skill_deep_probe.rb +172 -0
- data/lib/rigor/cli/skill_describe.rb +75 -9
- data/lib/rigor/configuration.rb +45 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +2 -1
- data/lib/rigor/environment/default_libraries.rb +5 -4
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +434 -75
- data/lib/rigor/environment.rb +38 -14
- data/lib/rigor/inference/expression_typer.rb +15 -0
- data/lib/rigor/inference/fork_map.rb +9 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +50 -2
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +35 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +6 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +16 -0
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +64 -5
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +137 -9
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +124 -1
- data/lib/rigor/inference/mutation_widening.rb +30 -25
- data/lib/rigor/inference/narrowing.rb +3 -1
- data/lib/rigor/inference/optimistic_origin.rb +58 -0
- data/lib/rigor/inference/protection_scanner.rb +18 -2
- data/lib/rigor/inference/receiver_alias.rb +57 -0
- data/lib/rigor/inference/scope_indexer.rb +78 -27
- data/lib/rigor/inference/statement_evaluator.rb +40 -2
- data/lib/rigor/language_server/buffer_resolution.rb +6 -3
- data/lib/rigor/language_server/buffer_table.rb +68 -6
- data/lib/rigor/language_server/diagnostic_publisher.rb +207 -4
- data/lib/rigor/language_server/incremental_sync.rb +159 -0
- data/lib/rigor/language_server/project_context.rb +45 -0
- data/lib/rigor/language_server/publish_batcher.rb +82 -0
- data/lib/rigor/language_server/server.rb +38 -9
- data/lib/rigor/language_server.rb +2 -0
- data/lib/rigor/plugin/base.rb +29 -2
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
- data/lib/rigor/protection/closure_kill_oracle.rb +171 -0
- data/lib/rigor/protection/dependency_closure.rb +59 -0
- data/lib/rigor/protection/diagnostic_oracle.rb +12 -8
- data/lib/rigor/protection/discovery_seed.rb +116 -0
- data/lib/rigor/protection/kill_signature.rb +31 -0
- data/lib/rigor/protection/mutation_cache.rb +355 -0
- data/lib/rigor/protection/mutation_scanner.rb +55 -11
- data/lib/rigor/protection/mutator.rb +26 -4
- data/lib/rigor/runtime/jit.rb +63 -1
- data/lib/rigor/scope/discovery_index.rb +9 -0
- data/lib/rigor/scope.rb +52 -8
- data/lib/rigor/sig_gen/generator.rb +130 -31
- data/lib/rigor/sig_gen/layout_index.rb +6 -0
- data/lib/rigor/sig_gen/meta_class_shape.rb +84 -0
- data/lib/rigor/sig_gen/renderer.rb +20 -1
- data/lib/rigor/sig_gen/write_result.rb +6 -4
- data/lib/rigor/sig_gen/writer.rb +195 -47
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +51 -1
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +6 -6
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/result_shape.rb +146 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +177 -12
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +74 -7
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +257 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +69 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +87 -11
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +34 -4
- data/sig/rigor/cache.rbs +6 -0
- data/sig/rigor/inference/void_origin.rbs +21 -0
- data/sig/rigor/plugin/base.rbs +4 -3
- data/sig/rigor/scope.rbs +8 -0
- metadata +23 -3
|
@@ -18,7 +18,8 @@ module Rigor
|
|
|
18
18
|
#
|
|
19
19
|
# Catalogue (Slice 5 phase 2):
|
|
20
20
|
#
|
|
21
|
-
# - Tuple#`first`, Tuple#`last
|
|
21
|
+
# - Tuple#`first`, Tuple#`last` (no-arg, or a single `Constant[Integer]` count lifting to a
|
|
22
|
+
# sub-`Tuple`, the `take`/`drop` shape), Tuple#`size`/`length`/`count`: no-block.
|
|
22
23
|
# - Tuple#`[]`, Tuple#`fetch` with a single `Constant[Integer]` argument inside the tuple's bounds
|
|
23
24
|
# (negative indices are normalised by length). Tuple#`[]` also handles static Range and
|
|
24
25
|
# start-length slices, returning a sliced Tuple or `Constant[nil]` for statically nil slices.
|
|
@@ -82,6 +83,16 @@ module Rigor
|
|
|
82
83
|
drop: :tuple_drop,
|
|
83
84
|
rotate: :tuple_rotate,
|
|
84
85
|
uniq: :tuple_uniq,
|
|
86
|
+
at: :tuple_at,
|
|
87
|
+
:& => :tuple_intersection,
|
|
88
|
+
intersection: :tuple_intersection,
|
|
89
|
+
:| => :tuple_union,
|
|
90
|
+
union: :tuple_union,
|
|
91
|
+
:- => :tuple_difference,
|
|
92
|
+
difference: :tuple_difference,
|
|
93
|
+
intersect?: :tuple_intersect?,
|
|
94
|
+
one?: :tuple_one?,
|
|
95
|
+
deconstruct: :shape_self,
|
|
85
96
|
index: :tuple_find_index,
|
|
86
97
|
find_index: :tuple_find_index,
|
|
87
98
|
rindex: :tuple_rindex,
|
|
@@ -672,23 +683,39 @@ module Rigor
|
|
|
672
683
|
Type::Combinator.integer_range(min, max)
|
|
673
684
|
end
|
|
674
685
|
|
|
675
|
-
# `first` (no arg) → the first element (or `Constant[nil]` when empty).
|
|
676
|
-
#
|
|
677
|
-
#
|
|
686
|
+
# `first` (no arg) → the first element (or `Constant[nil]` when empty). `first(n)` → a `Tuple` of
|
|
687
|
+
# the leading `n` elements, the same `take`/`drop` shape via `non_negative_count_arg` (`n >= size`
|
|
688
|
+
# returns the full receiver, a non-static or negative count declines to RBS).
|
|
678
689
|
def tuple_first(tuple, _method_name, args)
|
|
679
|
-
return
|
|
690
|
+
return tuple_first_n(tuple, args) unless args.empty?
|
|
680
691
|
return Type::Combinator.constant_of(nil) if tuple.elements.empty?
|
|
681
692
|
|
|
682
693
|
tuple.elements.first
|
|
683
694
|
end
|
|
684
695
|
|
|
696
|
+
def tuple_first_n(tuple, args)
|
|
697
|
+
n = non_negative_count_arg(args)
|
|
698
|
+
return nil if n.nil?
|
|
699
|
+
|
|
700
|
+
Type::Combinator.tuple_of(*tuple.elements.first(n))
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
# `last` (no arg) → the last element (or `Constant[nil]` when empty). `last(n)` → a `Tuple` of the
|
|
704
|
+
# trailing `n` elements, mirroring `tuple_first_n`.
|
|
685
705
|
def tuple_last(tuple, _method_name, args)
|
|
686
|
-
return
|
|
706
|
+
return tuple_last_n(tuple, args) unless args.empty?
|
|
687
707
|
return Type::Combinator.constant_of(nil) if tuple.elements.empty?
|
|
688
708
|
|
|
689
709
|
tuple.elements.last
|
|
690
710
|
end
|
|
691
711
|
|
|
712
|
+
def tuple_last_n(tuple, args)
|
|
713
|
+
n = non_negative_count_arg(args)
|
|
714
|
+
return nil if n.nil?
|
|
715
|
+
|
|
716
|
+
Type::Combinator.tuple_of(*tuple.elements.last(n))
|
|
717
|
+
end
|
|
718
|
+
|
|
692
719
|
def tuple_size(tuple, _method_name, args)
|
|
693
720
|
return nil unless args.empty?
|
|
694
721
|
|
|
@@ -957,6 +984,88 @@ module Rigor
|
|
|
957
984
|
|
|
958
985
|
# `tuple + other` — concatenates two Tuples. Both sides must be `Type::Tuple`. Returns a new
|
|
959
986
|
# Tuple whose elements are those of the receiver followed by those of the argument.
|
|
987
|
+
# `at(n)` — the strict single-Integer accessor. Deliberately NOT an alias of {#tuple_index}: `[]`
|
|
988
|
+
# and `slice` accept a Range or a `(start, length)` pair, while `Array#at` raises `ArgumentError`
|
|
989
|
+
# on anything but one Integer, and a fold must never invent a value for a call that raises.
|
|
990
|
+
#
|
|
991
|
+
# An out-of-range index declines rather than folding to `Constant[nil]`, matching {#tuple_index}.
|
|
992
|
+
# Ruby really does return nil there, so the fold would be correct — but it would also turn a
|
|
993
|
+
# receiver the RBS tier types as `Elem?` into a proven nil, newly surfacing diagnostics on code
|
|
994
|
+
# that may be guarded in ways a shape cannot see. Precision that fires a new diagnostic is a
|
|
995
|
+
# separate decision from precision that removes a `Dynamic` (#121).
|
|
996
|
+
def tuple_at(tuple, _method_name, args)
|
|
997
|
+
return nil unless args.size == 1
|
|
998
|
+
return nil unless args.first.is_a?(Type::Constant) && args.first.value.is_a?(Integer)
|
|
999
|
+
|
|
1000
|
+
tuple.elements[args.first.value]
|
|
1001
|
+
end
|
|
1002
|
+
|
|
1003
|
+
# `a & b` / `a.intersection(b, ...)`, `a | b` / `a.union(...)`, `a - b` / `a.difference(...)`.
|
|
1004
|
+
# Every set operation is evaluated by running Ruby's own operator over the unwrapped values, which
|
|
1005
|
+
# is the only way to reproduce its `eql?` / `hash` semantics exactly — `[1] & [1.0]` is empty even
|
|
1006
|
+
# though `1 == 1.0`, so an equality-based reimplementation would fold the wrong answer.
|
|
1007
|
+
def tuple_intersection(tuple, _method_name, args)
|
|
1008
|
+
set_operation(tuple, args) { |values, others| others.reduce(values, :&) }
|
|
1009
|
+
end
|
|
1010
|
+
|
|
1011
|
+
def tuple_union(tuple, _method_name, args)
|
|
1012
|
+
set_operation(tuple, args) { |values, others| others.reduce(values, :|) }
|
|
1013
|
+
end
|
|
1014
|
+
|
|
1015
|
+
def tuple_difference(tuple, _method_name, args)
|
|
1016
|
+
set_operation(tuple, args) { |values, others| others.reduce(values, :-) }
|
|
1017
|
+
end
|
|
1018
|
+
|
|
1019
|
+
# `a.intersect?(b)` — the predicate form, `Constant[bool]`. `nil` is the decline signal every
|
|
1020
|
+
# handler shares, not a boolean answer, so the predicate-return cop is disabled as it is for the
|
|
1021
|
+
# other `?`-named handlers above.
|
|
1022
|
+
# rubocop:disable Style/ReturnNilInPredicateMethodDefinition
|
|
1023
|
+
def tuple_intersect?(tuple, _method_name, args)
|
|
1024
|
+
result = set_operation(tuple, args) { |values, others| others.reduce(values, :&) }
|
|
1025
|
+
return nil if result.nil?
|
|
1026
|
+
|
|
1027
|
+
Type::Combinator.constant_of(!result.elements.empty?)
|
|
1028
|
+
end
|
|
1029
|
+
|
|
1030
|
+
# Shared body for the set operations: unwrap the receiver and every Tuple argument, hand the raw
|
|
1031
|
+
# values to `block`, and lift the result back. Declines unless every element on both sides is a
|
|
1032
|
+
# `Constant` (an unknown element makes membership undecidable) and every argument is a `Tuple` —
|
|
1033
|
+
# `Array#&` also accepts anything that responds to `to_ary`, which a shape cannot prove.
|
|
1034
|
+
def set_operation(tuple, args)
|
|
1035
|
+
return nil if args.empty? || args.size > MAX_SET_OPERATION_ARITY
|
|
1036
|
+
|
|
1037
|
+
values = constant_values(tuple.elements)
|
|
1038
|
+
return nil if values.nil?
|
|
1039
|
+
|
|
1040
|
+
others = args.map do |arg|
|
|
1041
|
+
return nil unless arg.is_a?(Type::Tuple)
|
|
1042
|
+
|
|
1043
|
+
constant_values(arg.elements) || (return nil)
|
|
1044
|
+
end
|
|
1045
|
+
result = yield(values, others)
|
|
1046
|
+
return nil if result.size > MAX_SET_OPERATION_SIZE
|
|
1047
|
+
|
|
1048
|
+
Type::Combinator.tuple_of(*result.map { |value| Type::Combinator.constant_of(value) })
|
|
1049
|
+
end
|
|
1050
|
+
|
|
1051
|
+
# An argument list longer than this, or a result wider than this, declines to the RBS tier rather
|
|
1052
|
+
# than materialising an unbounded Tuple — the same discipline as {MAX_ZIP_ARITY} / the join cap.
|
|
1053
|
+
MAX_SET_OPERATION_ARITY = 8
|
|
1054
|
+
MAX_SET_OPERATION_SIZE = 64
|
|
1055
|
+
private_constant :MAX_SET_OPERATION_ARITY, :MAX_SET_OPERATION_SIZE
|
|
1056
|
+
|
|
1057
|
+
# `one?` with no block and no pattern — `Constant[bool]` of "exactly one truthy element". Only
|
|
1058
|
+
# `Constant` elements have decidable truthiness, and the block / pattern forms defer.
|
|
1059
|
+
def tuple_one?(tuple, _method_name, args)
|
|
1060
|
+
return nil unless args.empty?
|
|
1061
|
+
|
|
1062
|
+
values = constant_values(tuple.elements)
|
|
1063
|
+
return nil if values.nil?
|
|
1064
|
+
|
|
1065
|
+
Type::Combinator.constant_of(values.one? { |value| value })
|
|
1066
|
+
end
|
|
1067
|
+
# rubocop:enable Style/ReturnNilInPredicateMethodDefinition
|
|
1068
|
+
|
|
960
1069
|
def tuple_concat(tuple, _method_name, args)
|
|
961
1070
|
return nil unless args.size == 1
|
|
962
1071
|
|
|
@@ -1558,13 +1667,16 @@ module Rigor
|
|
|
1558
1667
|
Type::Combinator.hash_shape_of(shape.pairs.merge(other.pairs))
|
|
1559
1668
|
end
|
|
1560
1669
|
|
|
1561
|
-
# `shape[k]` and `shape.fetch(k)` for a value-pinned scalar key.
|
|
1562
|
-
# on the method:
|
|
1670
|
+
# `shape[k]` and `shape.fetch(k)` for a value-pinned scalar key. On a CLOSED shape a key outside
|
|
1671
|
+
# `pairs` is provably missing, and the resolution depends on the method:
|
|
1563
1672
|
#
|
|
1564
1673
|
# - `[]` returns `nil` at runtime; we surface `Constant[nil]` so the carrier is visible to
|
|
1565
1674
|
# downstream narrowing.
|
|
1566
1675
|
# - `fetch` (no default, no block) raises `KeyError`; we let the projection answer apply because
|
|
1567
1676
|
# the runtime would not produce a value.
|
|
1677
|
+
#
|
|
1678
|
+
# On an `:open` shape neither applies — the key is unknown rather than missing, so both methods
|
|
1679
|
+
# answer the `untyped` step from {#hash_dig_step}.
|
|
1568
1680
|
def hash_lookup(shape, method_name, args)
|
|
1569
1681
|
return nil unless args.size == 1
|
|
1570
1682
|
|
|
@@ -1594,6 +1706,12 @@ module Rigor
|
|
|
1594
1706
|
# Returns the per-step value type for a HashShape lookup (or `Constant[nil]` for a known-missing
|
|
1595
1707
|
# key). Returns `nil` when the argument is not a value-pinned scalar key so the caller can fall
|
|
1596
1708
|
# through to the projection answer.
|
|
1709
|
+
#
|
|
1710
|
+
# A key outside `pairs` only resolves to `Constant[nil]` on a CLOSED shape, where `pairs` is the
|
|
1711
|
+
# whole key set and the runtime read provably returns nil. On an `:open` shape the key set is by
|
|
1712
|
+
# definition not exhausted, so an undeclared key is *unknown*, not absent — it may hold a value
|
|
1713
|
+
# of any type. Typing it `Constant[nil]` there would put `call.undefined-method` on the next line
|
|
1714
|
+
# of correct code (`payload[:undeclared].upcase`), so the open case answers `untyped`.
|
|
1597
1715
|
def hash_dig_step(shape, arg)
|
|
1598
1716
|
return nil unless arg.is_a?(Type::Constant)
|
|
1599
1717
|
|
|
@@ -1607,6 +1725,8 @@ module Rigor
|
|
|
1607
1725
|
return Type::Combinator.union(value, Type::Combinator.constant_of(nil))
|
|
1608
1726
|
end
|
|
1609
1727
|
|
|
1728
|
+
return Type::Combinator.untyped if shape.open?
|
|
1729
|
+
|
|
1610
1730
|
Type::Combinator.constant_of(nil)
|
|
1611
1731
|
end
|
|
1612
1732
|
|
|
@@ -1616,8 +1736,13 @@ module Rigor
|
|
|
1616
1736
|
shape.optional_key?(arg.value)
|
|
1617
1737
|
end
|
|
1618
1738
|
|
|
1739
|
+
# Whether the key is provably ABSENT — which only a closed shape can establish. An open shape
|
|
1740
|
+
# permits keys outside `pairs`, so a key it does not declare is unknown rather than missing, and
|
|
1741
|
+
# `fetch` must not be deferred on the theory that Ruby would raise `KeyError`: the key may well
|
|
1742
|
+
# be there. {#hash_dig_step} has already answered `untyped` for that case.
|
|
1619
1743
|
def missing_key_step?(shape, arg)
|
|
1620
1744
|
return false unless arg.is_a?(Type::Constant)
|
|
1745
|
+
return false if shape.open?
|
|
1621
1746
|
|
|
1622
1747
|
!shape.pairs.key?(arg.value)
|
|
1623
1748
|
end
|
|
@@ -1685,7 +1810,9 @@ module Rigor
|
|
|
1685
1810
|
|
|
1686
1811
|
# Continues a `dig` chain after the first step. Tuple and HashShape members re-dispatch into the
|
|
1687
1812
|
# catalogue; `Constant[nil]` short-circuits the chain (Hash#dig and Array#dig do the same at
|
|
1688
|
-
# runtime);
|
|
1813
|
+
# runtime); an `untyped` step (an undeclared key on an open shape) keeps the whole chain
|
|
1814
|
+
# `untyped`, since digging further into an unknown value cannot recover a bound; anything else
|
|
1815
|
+
# falls through so the projection answer applies.
|
|
1689
1816
|
def chain_dig(receiver, args)
|
|
1690
1817
|
return receiver if args.empty?
|
|
1691
1818
|
|
|
@@ -1693,6 +1820,7 @@ module Rigor
|
|
|
1693
1820
|
when Type::Tuple then tuple_dig(receiver, :dig, args)
|
|
1694
1821
|
when Type::HashShape then hash_dig(receiver, :dig, args)
|
|
1695
1822
|
when Type::Constant then receiver.value.nil? ? Type::Combinator.constant_of(nil) : nil
|
|
1823
|
+
when Type::Dynamic then Type::Combinator.untyped
|
|
1696
1824
|
end
|
|
1697
1825
|
end
|
|
1698
1826
|
end
|
|
@@ -141,7 +141,7 @@ module Rigor
|
|
|
141
141
|
map = member_map_for_new(members, keyword_init, context)
|
|
142
142
|
return degraded_instance if map.nil?
|
|
143
143
|
|
|
144
|
-
Type::Combinator.struct_instance_of(members: map, class_name: class_name)
|
|
144
|
+
Type::Combinator.struct_instance_of(members: widen_unowned_emptiness(map), class_name: class_name)
|
|
145
145
|
end
|
|
146
146
|
|
|
147
147
|
# Builds the member -> type map honouring the class's `keyword_init` flag: a `keyword_init: true`
|
|
@@ -309,7 +309,8 @@ module Rigor
|
|
|
309
309
|
return scope if member.nil?
|
|
310
310
|
|
|
311
311
|
rebound = Type::Combinator.struct_instance_of(
|
|
312
|
-
members: current.members.merge(member => assigned_type),
|
|
312
|
+
members: widen_unowned_emptiness(current.members.merge(member => assigned_type)),
|
|
313
|
+
class_name: current.class_name
|
|
313
314
|
)
|
|
314
315
|
scope.with_local(receiver.name, rebound)
|
|
315
316
|
end
|
|
@@ -20,12 +20,31 @@ module Rigor
|
|
|
20
20
|
# decode. Returns `Constant[String]`.
|
|
21
21
|
# * `encode_uri_component(str)` / `decode_uri_component(str)` — Same encoding but may preserve
|
|
22
22
|
# additional reserved chars (Ruby 3.2+). Returns `Constant[String]`.
|
|
23
|
+
# * `encode_www_form(enum)` — the whole-form encoder, over a `Tuple` of `[key, value]` `Tuple`s (an
|
|
24
|
+
# array literal) or a `HashShape` (a hash literal) whose keys and values are all `Constant`.
|
|
25
|
+
# Returns `Constant[String]`.
|
|
26
|
+
# * `decode_www_form(str)` — the inverse, over a `Constant[String]`. Returns a `Tuple` of two-element
|
|
27
|
+
# `Tuple[Constant[String], Constant[String]]` pairs rather than the RBS tier's
|
|
28
|
+
# `Array[[String, String]]`, so a destructuring read of a known form gets the concrete strings.
|
|
29
|
+
# * `extract(str)` — the URIs a constant string contains, as a `Tuple` of `Constant[String]` rather
|
|
30
|
+
# than `Array[String]`. Single-argument form only (see {.fold_extract}).
|
|
31
|
+
#
|
|
32
|
+
# === Deliberately NOT folded
|
|
33
|
+
#
|
|
34
|
+
# `URI.parse` and `URI.join` are the two remaining gaps in this module's coverage note, and both are
|
|
35
|
+
# declined on the repo's own existing rule rather than on judgement: they return `URI::Generic` and
|
|
36
|
+
# friends, which are absent from `ConstantFolding::FOLDABLE_CONSTANT_CLASSES`, so there is no
|
|
37
|
+
# `Constant[…]` for this tier to produce. Narrowing `URI.parse`'s ten-arm return union to the one
|
|
38
|
+
# scheme class a constant string selects WOULD be a real precision win, but it makes a gradually-valid
|
|
39
|
+
# dispatch precise and can therefore surface a diagnostic that does not fire today — which puts it in
|
|
40
|
+
# the bucket-3 / P0 category, not in this FP-safe fold category ([#121](
|
|
41
|
+
# https://github.com/rigortype/rigor/issues/121)).
|
|
23
42
|
#
|
|
24
43
|
# === Non-constant / unsupported cases
|
|
25
44
|
#
|
|
26
45
|
# Returns `nil` (deferring to the next dispatcher tier) when:
|
|
27
46
|
# - the receiver is not `Singleton[URI]`,
|
|
28
|
-
# -
|
|
47
|
+
# - an argument is not the constant shape the method's fold requires,
|
|
29
48
|
# - the method is not in the supported set.
|
|
30
49
|
module URIFolding
|
|
31
50
|
URI_COMPONENT_METHODS = Set[
|
|
@@ -34,6 +53,12 @@ module Rigor
|
|
|
34
53
|
].freeze
|
|
35
54
|
private_constant :URI_COMPONENT_METHODS
|
|
36
55
|
|
|
56
|
+
# A form long enough to suggest generated or accumulated data rather than a literal a reader wrote;
|
|
57
|
+
# past it the folded `Constant[String]` stops being useful in a diagnostic and only costs memory.
|
|
58
|
+
# Mirrors the byte ceiling `ShapeDispatch`'s own `tuple_join` fold applies for the same reason.
|
|
59
|
+
FORM_PAIR_LIMIT = 64
|
|
60
|
+
private_constant :FORM_PAIR_LIMIT
|
|
61
|
+
|
|
37
62
|
module_function
|
|
38
63
|
|
|
39
64
|
# @return [Rigor::Type, nil] folded result, or nil to defer.
|
|
@@ -42,11 +67,109 @@ module Rigor
|
|
|
42
67
|
method_name = context.method_name
|
|
43
68
|
args = context.args
|
|
44
69
|
return nil unless SingletonFolding.receiver?(receiver, "URI")
|
|
70
|
+
return fold_encode_www_form(args) if method_name == :encode_www_form
|
|
71
|
+
return fold_decode_www_form(args) if method_name == :decode_www_form
|
|
72
|
+
return fold_extract(args) if method_name == :extract
|
|
45
73
|
return nil unless URI_COMPONENT_METHODS.include?(method_name)
|
|
46
74
|
|
|
47
75
|
fold_uri_call(method_name, args)
|
|
48
76
|
end
|
|
49
77
|
|
|
78
|
+
# `URI.encode_www_form(enum)` — deterministic over its whole input, so it folds only when EVERY key
|
|
79
|
+
# and value is already a `Constant`. One non-constant pair declines the whole call: a partially
|
|
80
|
+
# known form has no `Constant[String]` to stand for it.
|
|
81
|
+
def fold_encode_www_form(args)
|
|
82
|
+
return nil unless args.size == 1
|
|
83
|
+
|
|
84
|
+
pairs = form_pairs(args.first)
|
|
85
|
+
return nil if pairs.nil? || pairs.size > FORM_PAIR_LIMIT
|
|
86
|
+
|
|
87
|
+
Type::Combinator.constant_of(URI.encode_www_form(pairs))
|
|
88
|
+
rescue StandardError
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The `[key, value]` pairs of a literal form argument, or nil when the shape is not fully known:
|
|
93
|
+
# a `Tuple` of two-element `Tuple`s (the array-of-pairs spelling) or a `HashShape` (the hash
|
|
94
|
+
# spelling). `HashShape` pairs are read only when the shape is CLOSED and carries no optional key —
|
|
95
|
+
# an open or partially-optional shape describes a hash whose real contents this fold cannot see.
|
|
96
|
+
def form_pairs(arg)
|
|
97
|
+
case arg
|
|
98
|
+
when Type::Tuple then tuple_form_pairs(arg)
|
|
99
|
+
when Type::HashShape then hash_form_pairs(arg)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def tuple_form_pairs(tuple)
|
|
104
|
+
tuple.elements.map do |element|
|
|
105
|
+
return nil unless element.is_a?(Type::Tuple) && element.elements.size == 2
|
|
106
|
+
return nil unless element.elements.all?(Type::Constant)
|
|
107
|
+
|
|
108
|
+
element.elements.map(&:value)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def hash_form_pairs(shape)
|
|
113
|
+
return nil unless shape.closed? && shape.optional_keys.empty?
|
|
114
|
+
|
|
115
|
+
shape.pairs.map do |key, value|
|
|
116
|
+
return nil unless value.is_a?(Type::Constant)
|
|
117
|
+
|
|
118
|
+
[key, value.value]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# `URI.decode_www_form(str)` — the pair list of a constant form, as a `Tuple` of two-element
|
|
123
|
+
# `Tuple`s so a destructuring read (`k, v = URI.decode_www_form(s).first`) reaches the concrete
|
|
124
|
+
# strings instead of the RBS tier's `Array[[String, String]]`.
|
|
125
|
+
def fold_decode_www_form(args)
|
|
126
|
+
return nil unless args.size == 1
|
|
127
|
+
|
|
128
|
+
str = SingletonFolding.constant_string(args.first)
|
|
129
|
+
return nil if str.nil?
|
|
130
|
+
|
|
131
|
+
pairs = URI.decode_www_form(str)
|
|
132
|
+
return nil if pairs.size > FORM_PAIR_LIMIT
|
|
133
|
+
|
|
134
|
+
Type::Combinator.tuple_of(*pairs.map { |key, value| constant_pair(key, value) })
|
|
135
|
+
rescue StandardError
|
|
136
|
+
nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def constant_pair(key, value)
|
|
140
|
+
Type::Combinator.tuple_of(Type::Combinator.constant_of(key), Type::Combinator.constant_of(value))
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# `URI.extract(str)` — the URIs a constant string contains, as a `Tuple` of `Constant[String]`
|
|
144
|
+
# instead of the RBS tier's `Array[String]`. Only the single-argument form folds: the optional
|
|
145
|
+
# second argument is a schema FILTER, and honouring it means reproducing `URI.extract`'s own
|
|
146
|
+
# argument handling rather than the one call this fold makes, so it declines instead.
|
|
147
|
+
#
|
|
148
|
+
# Ruby emits an obsolescence warning for this method under `$VERBOSE`; the fold calls it with
|
|
149
|
+
# warnings silenced, because a warning about the ANALYZED program's API choice must not appear on
|
|
150
|
+
# the analyzer's own stderr, where a reader would attribute it to Rigor.
|
|
151
|
+
def fold_extract(args)
|
|
152
|
+
return nil unless args.size == 1
|
|
153
|
+
|
|
154
|
+
str = SingletonFolding.constant_string(args.first)
|
|
155
|
+
return nil if str.nil?
|
|
156
|
+
|
|
157
|
+
found = silence_warnings { URI.extract(str) }
|
|
158
|
+
return nil if found.nil? || found.size > FORM_PAIR_LIMIT
|
|
159
|
+
|
|
160
|
+
Type::Combinator.tuple_of(*found.map { |uri| Type::Combinator.constant_of(uri) })
|
|
161
|
+
rescue StandardError
|
|
162
|
+
nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def silence_warnings
|
|
166
|
+
previous = $VERBOSE
|
|
167
|
+
$VERBOSE = nil
|
|
168
|
+
yield
|
|
169
|
+
ensure
|
|
170
|
+
$VERBOSE = previous
|
|
171
|
+
end
|
|
172
|
+
|
|
50
173
|
def fold_uri_call(method_name, args)
|
|
51
174
|
return nil unless args.size == 1
|
|
52
175
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../type"
|
|
4
4
|
require_relative "../source/node_children"
|
|
5
|
+
require_relative "receiver_alias"
|
|
5
6
|
|
|
6
7
|
module Rigor
|
|
7
8
|
module Inference
|
|
@@ -45,6 +46,10 @@ module Rigor
|
|
|
45
46
|
#
|
|
46
47
|
# - `arr.<mutator>(...)` where `arr` is a local variable.
|
|
47
48
|
# - `@arr.<mutator>(...)` where `@arr` is an instance variable.
|
|
49
|
+
# - a receiver that *selects* among such variables rather than naming one —
|
|
50
|
+
# `(kind == :required ? required : optional)[key] = info`, an `if`/`else`, `||` / `&&`. Every
|
|
51
|
+
# variable the expression can evaluate to is a possible mutation target, so every one widens
|
|
52
|
+
# (issue #277). See {ReceiverAlias}.
|
|
48
53
|
#
|
|
49
54
|
# Out of scope (left for a separate cycle):
|
|
50
55
|
#
|
|
@@ -100,10 +105,10 @@ module Rigor
|
|
|
100
105
|
PURE_SELF_RETURNERS.include?(method_name)
|
|
101
106
|
end
|
|
102
107
|
|
|
103
|
-
# Returns a scope with the call's receiver widened,
|
|
104
|
-
#
|
|
108
|
+
# Returns a scope with the call's receiver widened, for every variable the receiver expression
|
|
109
|
+
# can evaluate to ({ReceiverAlias.candidates}) whose current binding is a literal-shape carrier
|
|
105
110
|
# (`Tuple` / `HashShape`) or an empty-witness refinement (`non-empty-array` /
|
|
106
|
-
# `non-empty-hash`) AND
|
|
111
|
+
# `non-empty-hash`) AND whose call name is a known in-place mutator for that shape.
|
|
107
112
|
# Returns `current_scope` unchanged otherwise.
|
|
108
113
|
#
|
|
109
114
|
# @param call_node [Prism::CallNode]
|
|
@@ -115,13 +120,8 @@ module Rigor
|
|
|
115
120
|
receiver = call_node.receiver
|
|
116
121
|
return current_scope if receiver.nil?
|
|
117
122
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
widen_local(call_node.name, receiver.name, current_scope)
|
|
121
|
-
when Prism::InstanceVariableReadNode
|
|
122
|
-
widen_ivar(call_node.name, receiver.name, current_scope)
|
|
123
|
-
else
|
|
124
|
-
current_scope
|
|
123
|
+
ReceiverAlias.candidates(receiver).reduce(current_scope) do |acc, read|
|
|
124
|
+
widen_alias_read(call_node.name, read, acc)
|
|
125
125
|
end
|
|
126
126
|
end
|
|
127
127
|
|
|
@@ -130,13 +130,13 @@ module Rigor
|
|
|
130
130
|
# LOCALS are otherwise invisible to the post-call outer scope (ivars are handled correctly
|
|
131
131
|
# already because they live in the method-body scope, not the block-local scope).
|
|
132
132
|
#
|
|
133
|
-
# Walks the block AST for `<receiver>.<method>(...)` calls
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
# `
|
|
137
|
-
#
|
|
138
|
-
# precision — so blindly propagating is sound regardless of
|
|
139
|
-
# runs.
|
|
133
|
+
# Walks the block AST for `<receiver>.<method>(...)` calls, resolves the receiver expression
|
|
134
|
+
# to the variables it can evaluate to ({ReceiverAlias.candidates}), and widens each one
|
|
135
|
+
# against the outer scope. A `LocalVariableReadNode` with `depth == 0` is skipped — Prism's
|
|
136
|
+
# `depth` counts scope hops outward, so `0` means a block-local, not a capture; an
|
|
137
|
+
# `InstanceVariableReadNode` is always method-scope and always applies. The widening is
|
|
138
|
+
# always safe — it can only LOSE precision — so blindly propagating is sound regardless of
|
|
139
|
+
# whether the block actually runs.
|
|
140
140
|
#
|
|
141
141
|
# Recurses into nested expression nodes so chained / nested forms (`arr << f(x); arr <<
|
|
142
142
|
# g(y)`, `arr.push(x) if cond`) are all caught. Does NOT recurse into nested
|
|
@@ -174,15 +174,20 @@ module Rigor
|
|
|
174
174
|
receiver = call_node.receiver
|
|
175
175
|
return scope if receiver.nil?
|
|
176
176
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
ReceiverAlias.candidates(receiver).reduce(scope) do |acc, read|
|
|
178
|
+
# A block-local read (`depth == 0`) is not a capture of the outer scope, so widening its
|
|
179
|
+
# name against the OUTER scope would hit an unrelated same-named binding.
|
|
180
|
+
next acc if read.is_a?(Prism::LocalVariableReadNode) && read.depth.zero?
|
|
180
181
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
182
|
+
widen_alias_read(call_node.name, read, acc)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def widen_alias_read(method_name, read, scope)
|
|
187
|
+
case read
|
|
188
|
+
when Prism::LocalVariableReadNode then widen_local(method_name, read.name, scope)
|
|
189
|
+
when Prism::InstanceVariableReadNode then widen_ivar(method_name, read.name, scope)
|
|
190
|
+
else scope
|
|
186
191
|
end
|
|
187
192
|
end
|
|
188
193
|
|
|
@@ -1169,7 +1169,9 @@ module Rigor
|
|
|
1169
1169
|
# so the proven-absent key reads `nil`. Returns the input unchanged when nothing
|
|
1170
1170
|
# applies (caller detects "no narrowing"). Only an *optional* present key is removed: a
|
|
1171
1171
|
# required key makes `key?` always true (the false edge is dead, leave the shape opaque)
|
|
1172
|
-
# and a key absent from `pairs` already reads `nil
|
|
1172
|
+
# and a key absent from `pairs` already reads `nil` on a closed shape. On an open shape an
|
|
1173
|
+
# undeclared key keeps reading `untyped` on this edge — sound but imprecise, since the shape
|
|
1174
|
+
# carries no per-key exclusion to record what the guard just proved absent.
|
|
1173
1175
|
def narrow_hash_key_absent(type, key)
|
|
1174
1176
|
case type
|
|
1175
1177
|
when Type::HashShape
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
# Marks a value whose *nil-freeness* rests on Rigor's deliberate choice to ignore core RBS's
|
|
6
|
+
# `%a{implicitly-returns-nil}` annotation rather than on the value's class. See
|
|
7
|
+
# docs/internal-spec/inference-engine.md § "That deferred-to answer is nil-free for a lookup that can
|
|
8
|
+
# miss": `Hash#[]` reads as `V` and `Array#first` as `E` because pessimising them to `V?` costs 25
|
|
9
|
+
# measured false positives on Rigor's own `lib`. The consequence the spec draws is that such a value is
|
|
10
|
+
# **optimistic, not proof** — `MAP[key]` reading as `"x" | "y"` asserts nothing about whether the key was
|
|
11
|
+
# present.
|
|
12
|
+
#
|
|
13
|
+
# A side channel in the ADR-75 / ADR-82 sense: it never participates in subtyping, consistency,
|
|
14
|
+
# normalization or erasure, and no diagnostic fires from it. It differs from {DynamicOrigin} in what it
|
|
15
|
+
# attaches to — the value here is *not* `Dynamic`; it is an ordinary `Union` / `Constant` / `Nominal`
|
|
16
|
+
# that happens to have been produced optimistically.
|
|
17
|
+
#
|
|
18
|
+
# Issue #286: the `if` / `unless` branch elision is a third consumer of `Narrowing.predicate_certainty`,
|
|
19
|
+
# and unlike `flow.always-truthy-condition` and the `&&` / `||` gate it is not constrained by the spec
|
|
20
|
+
# passage above. This channel is what lets a certainty judgment tell the two apart.
|
|
21
|
+
module OptimisticOrigin
|
|
22
|
+
# The core-RBS annotation `RbsDispatch` reads the return type past.
|
|
23
|
+
ANNOTATION = "implicitly-returns-nil"
|
|
24
|
+
|
|
25
|
+
# The single cause carried today. Kept as a symbol (rather than a bare `true`) so a later slice can
|
|
26
|
+
# distinguish further optimistic families without changing the table's shape.
|
|
27
|
+
IMPLICITLY_RETURNS_NIL = :implicitly_returns_nil
|
|
28
|
+
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
# Whether the overload the selector actually picked carries the ignored annotation. The judgment is
|
|
32
|
+
# per-overload, which is what makes it precise: `Array#first` is optimistic while `Array#first(3)` is
|
|
33
|
+
# not, and `String#[]` / `Enumerable#find` are honest because they already spell the miss as `?`.
|
|
34
|
+
#
|
|
35
|
+
# @param method_definition [RBS::Definition::Method]
|
|
36
|
+
# @param method_type [RBS::MethodType] the overload {OverloadSelector.select} returned
|
|
37
|
+
# @return [Boolean]
|
|
38
|
+
def optimistic_overload?(method_definition, method_type)
|
|
39
|
+
type_def = matching_type_def(method_definition, method_type)
|
|
40
|
+
return false unless type_def.respond_to?(:overload_annotations)
|
|
41
|
+
|
|
42
|
+
type_def.overload_annotations.any? { |annotation| annotation.string == ANNOTATION }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# `RBS::Definition::Method#defs` runs parallel to `#method_types`, and the selector returns one of the
|
|
46
|
+
# latter's elements verbatim (`ReceiverAffinity.reorder` permutes the array without copying its
|
|
47
|
+
# members), so identity resolves the overload exactly. Equality is a fallback for any future path that
|
|
48
|
+
# rebuilds the method type.
|
|
49
|
+
def matching_type_def(method_definition, method_type)
|
|
50
|
+
return nil unless method_definition.respond_to?(:defs)
|
|
51
|
+
|
|
52
|
+
defs = method_definition.defs
|
|
53
|
+
defs.find { |type_def| type_def.type.equal?(method_type) } ||
|
|
54
|
+
defs.find { |type_def| type_def.type == method_type }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "scope_indexer"
|
|
4
4
|
require_relative "origin_lookup"
|
|
5
5
|
require_relative "../source/node_walker"
|
|
6
|
+
require_relative "../analysis/check_rules/inferred_param_guard"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
module Inference
|
|
@@ -20,7 +21,11 @@ module Rigor
|
|
|
20
21
|
# A single unprotected call site.
|
|
21
22
|
Site = Data.define(:line, :receiver, :method_name, :dynamic_origin)
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
# `lower_bound_typed` is a split WITHIN `protected_count`, never a move out of it (issue #263):
|
|
25
|
+
# a site whose receiver is concrete but rooted at a WD6b-marked inferred parameter (
|
|
26
|
+
# {Analysis::CheckRules::InferredParamGuard.rooted?}) is a lower-bound type — `protected_count`
|
|
27
|
+
# and `ratio` stay exactly as before, so `--threshold` never moves on unchanged code.
|
|
28
|
+
FileResult = Data.define(:protected_count, :unprotected_count, :lower_bound_typed, :sites) do
|
|
24
29
|
def total = protected_count + unprotected_count
|
|
25
30
|
|
|
26
31
|
# Protected ratio; a file with no dispatch sites is vacuously fully protected (nothing to get
|
|
@@ -37,6 +42,7 @@ module Rigor
|
|
|
37
42
|
def scan(root)
|
|
38
43
|
index = ScopeIndexer.index(root, default_scope: @scope)
|
|
39
44
|
protected_count = 0
|
|
45
|
+
lower_bound_typed = 0
|
|
40
46
|
sites = []
|
|
41
47
|
|
|
42
48
|
Source::NodeWalker.each(root) do |node|
|
|
@@ -46,6 +52,7 @@ module Rigor
|
|
|
46
52
|
receiver_type = scope.type_of(node.receiver)
|
|
47
53
|
if concrete_receiver?(receiver_type)
|
|
48
54
|
protected_count += 1
|
|
55
|
+
lower_bound_typed += 1 if lower_bound_typed?(node.receiver, scope)
|
|
49
56
|
else
|
|
50
57
|
origin = OriginLookup.origin_for(scope, node.receiver)
|
|
51
58
|
sites << Site.new(
|
|
@@ -57,7 +64,8 @@ module Rigor
|
|
|
57
64
|
end
|
|
58
65
|
end
|
|
59
66
|
|
|
60
|
-
FileResult.new(protected_count: protected_count, unprotected_count: sites.size,
|
|
67
|
+
FileResult.new(protected_count: protected_count, unprotected_count: sites.size,
|
|
68
|
+
lower_bound_typed: lower_bound_typed, sites: sites)
|
|
61
69
|
end
|
|
62
70
|
|
|
63
71
|
private
|
|
@@ -77,6 +85,14 @@ module Rigor
|
|
|
77
85
|
end
|
|
78
86
|
end
|
|
79
87
|
|
|
88
|
+
# ADR-67 WD6b (issue #263) — true when a concrete receiver is rooted at an inferred-but-undeclared
|
|
89
|
+
# parameter: the type is a call-site lower bound, so the negative in-body rules decline on it (no
|
|
90
|
+
# diagnostic can actually fire here yet). Delegates to the single-homed shared predicate every
|
|
91
|
+
# negative rule already consults — nothing is re-derived.
|
|
92
|
+
def lower_bound_typed?(receiver, scope)
|
|
93
|
+
Analysis::CheckRules::InferredParamGuard.rooted?(receiver, scope)
|
|
94
|
+
end
|
|
95
|
+
|
|
80
96
|
def safe_describe(type)
|
|
81
97
|
type.respond_to?(:describe) ? type.describe(:short) : type.to_s
|
|
82
98
|
rescue StandardError
|