rigortype 0.3.1 → 0.3.3
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/core_overlay/pathname.rbs +5 -0
- data/data/core_overlay/resolv.rbs +31 -0
- data/data/core_overlay/string_scanner.rbs +5 -0
- 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/manual/02-cli-reference.md +71 -11
- data/docs/manual/03-configuration.md +16 -1
- data/docs/manual/04-diagnostics.md +8 -0
- data/docs/manual/12-caching.md +17 -5
- data/docs/manual/15-type-protection-coverage.md +122 -1
- data/docs/manual/plugins/rigor-rbs-inline.md +22 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +8 -0
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +53 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +7 -0
- data/lib/rigor/analysis/check_rules.rb +107 -28
- data/lib/rigor/analysis/incremental_session.rb +156 -11
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +14 -0
- data/lib/rigor/analysis/rule_catalog.rb +5 -1
- data/lib/rigor/analysis/run_cache_key.rb +17 -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 +78 -7
- data/lib/rigor/analysis/severity_stamp.rb +2 -1
- 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/cli/check_command.rb +49 -28
- data/lib/rigor/cli/coverage_command.rb +15 -2
- data/lib/rigor/cli/coverage_mutation.rb +242 -11
- 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/configuration.rb +45 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +2 -1
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +434 -75
- data/lib/rigor/environment.rb +28 -13
- data/lib/rigor/inference/anonymous_meta_class.rb +81 -0
- data/lib/rigor/inference/expression_typer.rb +62 -4
- data/lib/rigor/inference/fork_map.rb +47 -4
- 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/rbs_dispatch.rb +158 -12
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +64 -5
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +234 -11
- 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/method_dispatcher.rb +41 -10
- data/lib/rigor/inference/mutation_widening.rb +30 -25
- data/lib/rigor/inference/narrowing.rb +3 -1
- data/lib/rigor/inference/optimistic_origin.rb +123 -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 +197 -55
- data/lib/rigor/inference/singleton_object_constant.rb +63 -0
- data/lib/rigor/inference/statement_evaluator.rb +55 -4
- data/lib/rigor/language_server/buffer_table.rb +22 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +203 -4
- 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 +20 -1
- data/lib/rigor/language_server.rb +1 -0
- data/lib/rigor/plugin/node_rule_walk.rb +6 -0
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
- data/lib/rigor/protection/closure_kill_oracle.rb +190 -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 +114 -10
- 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 +12 -0
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/source/node_walker.rb +12 -0
- data/lib/rigor/type/anonymous_class_name.rb +40 -0
- data/lib/rigor/type/nominal.rb +5 -2
- data/lib/rigor/type/singleton.rb +4 -1
- data/lib/rigor/version.rb +1 -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/inference/void_origin.rbs +6 -3
- data/sig/rigor/scope.rbs +12 -0
- data/sig/rigor.rbs +4 -0
- metadata +21 -1
|
@@ -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,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
# Marks a value whose *nil-freeness* rests on Rigor's deliberate choice to ignore core RBS's
|
|
8
|
+
# `%a{implicitly-returns-nil}` annotation rather than on the value's class. See
|
|
9
|
+
# docs/internal-spec/inference-engine.md § "That deferred-to answer is nil-free for a lookup that can
|
|
10
|
+
# miss": `Hash#[]` reads as `V` and `Array#first` as `E` because pessimising them to `V?` costs 25
|
|
11
|
+
# measured false positives on Rigor's own `lib`. The consequence the spec draws is that such a value is
|
|
12
|
+
# **optimistic, not proof** — `MAP[key]` reading as `"x" | "y"` asserts nothing about whether the key was
|
|
13
|
+
# present.
|
|
14
|
+
#
|
|
15
|
+
# A side channel in the ADR-75 / ADR-82 sense: it never participates in subtyping, consistency,
|
|
16
|
+
# normalization or erasure, and no diagnostic fires from it. It differs from {DynamicOrigin} in what it
|
|
17
|
+
# attaches to — the value here is *not* `Dynamic`; it is an ordinary `Union` / `Constant` / `Nominal`
|
|
18
|
+
# that happens to have been produced optimistically.
|
|
19
|
+
#
|
|
20
|
+
# Issue #286: the `if` / `unless` branch elision is one consumer of `Narrowing.predicate_certainty`; the
|
|
21
|
+
# other two are `flow.always-truthy-condition` and the `&&` / `||` `constant_value_polarity` gate, and the
|
|
22
|
+
# spec passage above binds all three. This channel is what lets a certainty judgment tell the two apart.
|
|
23
|
+
#
|
|
24
|
+
# Issue #313: the mark is attached to a *value* — the call node that produced it, or the local / ivar it
|
|
25
|
+
# was bound to — but every one of those consumers reads a *predicate expression*, and a predicate is
|
|
26
|
+
# rarely the bare carrier. `x.nil?` collapses the carrier's nil-freeness into a `Constant[false]` of its
|
|
27
|
+
# own, `!x.nil?` inverts it, and `x.nil? || y.nil?` composes two of them; each step produced an unmarked
|
|
28
|
+
# `Constant` that the gates then read as proof. {.resolve} therefore derives the mark through exactly
|
|
29
|
+
# those shapes, so the exclusion survives composition instead of stopping at the read.
|
|
30
|
+
module OptimisticOrigin
|
|
31
|
+
# The core-RBS annotation `RbsDispatch` reads the return type past.
|
|
32
|
+
ANNOTATION = "implicitly-returns-nil"
|
|
33
|
+
|
|
34
|
+
# The single cause carried today. Kept as a symbol (rather than a bare `true`) so a later slice can
|
|
35
|
+
# distinguish further optimistic families without changing the table's shape.
|
|
36
|
+
IMPLICITLY_RETURNS_NIL = :implicitly_returns_nil
|
|
37
|
+
|
|
38
|
+
# The argument-free unary predicates whose folded result is a statement about the receiver's
|
|
39
|
+
# *nil-freeness* and nothing else, which is what makes the derivation sound rather than a general taint:
|
|
40
|
+
# `nil?` answers the exact question the optimism is a bet on, and `!` (which Prism spells as a `CallNode`
|
|
41
|
+
# named `:!`, covering both `!x` and `not x`) inverts whatever it is applied to. Value predicates —
|
|
42
|
+
# `empty?`, `zero?`, `any?` — are deliberately absent: they fold from the carrier's *value*, and marking
|
|
43
|
+
# them would widen this channel into a taint that silences genuine diagnostics.
|
|
44
|
+
NIL_COLLAPSING_PREDICATES = %i[nil? !].freeze
|
|
45
|
+
|
|
46
|
+
module_function
|
|
47
|
+
|
|
48
|
+
# The effective optimistic-nil-free cause of an expression under `scope`, or nil when its nil-freeness is
|
|
49
|
+
# a property of the value rather than a bet. The single owner of the judgment: `ExpressionTyper`,
|
|
50
|
+
# `StatementEvaluator` and `AlwaysTruthyConditionCollector` all route here, so the three consumers the
|
|
51
|
+
# spec binds cannot drift apart.
|
|
52
|
+
#
|
|
53
|
+
# Resolution order — the mark recorded on the node itself, then the binding a bare local / ivar read (or
|
|
54
|
+
# a write in value position, `if (x = MAP[k])`) resolves through, then the predicate-fold derivation
|
|
55
|
+
# issue #313 added.
|
|
56
|
+
#
|
|
57
|
+
# @param node [Prism::Node, nil]
|
|
58
|
+
# @param scope [Rigor::Scope, nil]
|
|
59
|
+
# @return [Symbol, nil]
|
|
60
|
+
def resolve(node, scope)
|
|
61
|
+
return nil if node.nil? || scope.nil?
|
|
62
|
+
|
|
63
|
+
recorded = scope.optimistic_origins[node]
|
|
64
|
+
return recorded if recorded
|
|
65
|
+
|
|
66
|
+
case node
|
|
67
|
+
when Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode then scope.optimistic_local(node.name)
|
|
68
|
+
when Prism::InstanceVariableReadNode, Prism::InstanceVariableWriteNode then scope.optimistic_ivar(node.name)
|
|
69
|
+
when Prism::AndNode, Prism::OrNode then resolve(node.left, scope) || resolve(node.right, scope)
|
|
70
|
+
when Prism::CallNode then resolve_through_predicate(node, scope)
|
|
71
|
+
when Prism::ParenthesesNode then resolve_through_parentheses(node, scope)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# `recv.nil?` / `!recv` — the fold is a statement about `recv`, so it is exactly as optimistic as `recv`
|
|
76
|
+
# is. A block or any argument means this is not the unary predicate it looks like (`x.!(y)` is a
|
|
77
|
+
# user-defined operator), and the derivation declines.
|
|
78
|
+
def resolve_through_predicate(node, scope)
|
|
79
|
+
return nil unless NIL_COLLAPSING_PREDICATES.include?(node.name)
|
|
80
|
+
return nil unless node.block.nil?
|
|
81
|
+
return nil unless node.arguments.nil? || node.arguments.arguments.empty?
|
|
82
|
+
|
|
83
|
+
resolve(node.receiver, scope)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# `(x.nil?)` — a single-statement parenthesised body is its own value, and authors do parenthesise a
|
|
87
|
+
# composed guard. A multi-statement body's value is its last statement, but the earlier statements can
|
|
88
|
+
# rebind, so only the single-statement form is derived.
|
|
89
|
+
def resolve_through_parentheses(node, scope)
|
|
90
|
+
body = node.body
|
|
91
|
+
return nil unless body.is_a?(Prism::StatementsNode) && body.body.size == 1
|
|
92
|
+
|
|
93
|
+
resolve(body.body.first, scope)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Whether the overload the selector actually picked carries the ignored annotation. The judgment is
|
|
97
|
+
# per-overload, which is what makes it precise: `Array#first` is optimistic while `Array#first(3)` is
|
|
98
|
+
# not, and `String#[]` / `Enumerable#find` are honest because they already spell the miss as `?`.
|
|
99
|
+
#
|
|
100
|
+
# @param method_definition [RBS::Definition::Method]
|
|
101
|
+
# @param method_type [RBS::MethodType] the overload {OverloadSelector.select} returned
|
|
102
|
+
# @return [Boolean]
|
|
103
|
+
def optimistic_overload?(method_definition, method_type)
|
|
104
|
+
type_def = matching_type_def(method_definition, method_type)
|
|
105
|
+
return false unless type_def.respond_to?(:overload_annotations)
|
|
106
|
+
|
|
107
|
+
type_def.overload_annotations.any? { |annotation| annotation.string == ANNOTATION }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# `RBS::Definition::Method#defs` runs parallel to `#method_types`, and the selector returns one of the
|
|
111
|
+
# latter's elements verbatim (`ReceiverAffinity.reorder` permutes the array without copying its
|
|
112
|
+
# members), so identity resolves the overload exactly. Equality is a fallback for any future path that
|
|
113
|
+
# rebuilds the method type.
|
|
114
|
+
def matching_type_def(method_definition, method_type)
|
|
115
|
+
return nil unless method_definition.respond_to?(:defs)
|
|
116
|
+
|
|
117
|
+
defs = method_definition.defs
|
|
118
|
+
defs.find { |type_def| type_def.type.equal?(method_type) } ||
|
|
119
|
+
defs.find { |type_def| type_def.type == method_type }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
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
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
# Which variables can a receiver EXPRESSION evaluate to?
|
|
8
|
+
#
|
|
9
|
+
# A receiver-fact invalidation (see {MutationWidening}) has to name the binding it invalidates,
|
|
10
|
+
# and the overwhelmingly common receiver — a bare `arr` / `@arr` read — names exactly one. But a
|
|
11
|
+
# receiver may also *select* among variables without naming any of them:
|
|
12
|
+
#
|
|
13
|
+
# (kind == :required ? required : optional)[key] = info
|
|
14
|
+
#
|
|
15
|
+
# The mutation lands on whichever of `required` / `optional` the ternary picked, so BOTH are
|
|
16
|
+
# possible targets and both must forget their literal shape. Reading only the syntactic head
|
|
17
|
+
# left both hashes carrying the empty `HashShape` the literal `{}` wrote, and a downstream
|
|
18
|
+
# `.empty?` then constant-folded into a false `flow.always-truthy-condition`
|
|
19
|
+
# ([#277](https://github.com/rigortype/rigor/issues/277)).
|
|
20
|
+
#
|
|
21
|
+
# The recursion covers only the forms whose value IS one of the sub-expressions: `if` / `unless`
|
|
22
|
+
# (including the ternary spelling), the short-circuit operators, and the transparent wrappers.
|
|
23
|
+
# Anything else — an index read (`declared[kind] << key`), a call result, a literal — names an
|
|
24
|
+
# object no binding can be attributed to and yields `[]`, which is what every receiver form
|
|
25
|
+
# outside the single-read case already contributed. The walk is depth-capped so a pathological
|
|
26
|
+
# nest cannot make receiver classification unbounded.
|
|
27
|
+
module ReceiverAlias
|
|
28
|
+
# Deep enough for any hand-written selection; a nest beyond it degrades to "names no binding".
|
|
29
|
+
WALK_DEPTH_CAP = 6
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
# @param node [Prism::Node, nil] the receiver expression.
|
|
34
|
+
# @param depth [Integer] recursion depth, internal.
|
|
35
|
+
# @return [Array<Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode>] every variable
|
|
36
|
+
# read the expression can evaluate to; empty when it can evaluate to none.
|
|
37
|
+
def candidates(node, depth = 0)
|
|
38
|
+
return [] if node.nil? || depth > WALK_DEPTH_CAP
|
|
39
|
+
|
|
40
|
+
case node
|
|
41
|
+
when Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode then [node]
|
|
42
|
+
when Prism::ParenthesesNode then candidates(node.body, depth + 1)
|
|
43
|
+
when Prism::StatementsNode then candidates(node.body.last, depth + 1)
|
|
44
|
+
when Prism::ElseNode then candidates(node.statements, depth + 1)
|
|
45
|
+
when Prism::IfNode then branches(node.statements, node.subsequent, depth)
|
|
46
|
+
when Prism::UnlessNode then branches(node.statements, node.else_clause, depth)
|
|
47
|
+
when Prism::OrNode, Prism::AndNode then branches(node.left, node.right, depth)
|
|
48
|
+
else []
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def branches(first, second, depth)
|
|
53
|
+
candidates(first, depth + 1) + candidates(second, depth + 1)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|