rigortype 0.4.5 → 0.4.6
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 +1 -1
- data/docs/handbook/11-sig-gen.md +11 -2
- data/docs/manual/02-cli-reference.md +46 -7
- data/docs/manual/03-configuration.md +2 -1
- data/docs/manual/04-diagnostics.md +1 -1
- data/docs/manual/10-mcp-server.md +9 -1
- data/docs/manual/12-caching.md +8 -1
- data/docs/manual/plugins/rigor-actionmailer.md +30 -0
- data/docs/manual/plugins/rigor-actionpack.md +30 -0
- data/docs/manual/plugins/rigor-devise.md +15 -0
- data/docs/manual/plugins/rigor-rspec.md +2 -1
- data/lib/rigor/analysis/buffer_binding.rb +14 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -0
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
- data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +51 -0
- data/lib/rigor/analysis/check_rules.rb +93 -8
- data/lib/rigor/analysis/dependency_recorder.rb +11 -0
- data/lib/rigor/analysis/edit_replay.rb +67 -0
- data/lib/rigor/analysis/incremental.rb +37 -0
- data/lib/rigor/analysis/incremental_session.rb +175 -27
- data/lib/rigor/analysis/path_expansion.rb +41 -0
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +5 -1
- data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
- data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
- data/lib/rigor/analysis/runner/pool_coordinator.rb +17 -4
- data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
- data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
- data/lib/rigor/analysis/runner.rb +156 -49
- data/lib/rigor/analysis/worker_session.rb +14 -2
- data/lib/rigor/cache/incremental_snapshot.rb +30 -7
- data/lib/rigor/cli/check_command.rb +93 -4
- data/lib/rigor/cli/diff_command.rb +2 -1
- data/lib/rigor/cli/lens_command.rb +5 -5
- data/lib/rigor/cli/mcp_command.rb +2 -1
- data/lib/rigor/cli/options.rb +20 -3
- data/lib/rigor/cli/plugins_command.rb +5 -0
- data/lib/rigor/cli/plugins_renderer.rb +24 -2
- data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +46 -7
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/type_of_project_scope.rb +27 -0
- data/lib/rigor/config_audit.rb +16 -5
- data/lib/rigor/configuration.rb +14 -4
- data/lib/rigor/environment/rbs_loader.rb +115 -6
- data/lib/rigor/environment/required_features.rb +136 -29
- data/lib/rigor/environment/ruby_library_roots.rb +122 -0
- data/lib/rigor/environment.rb +4 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
- data/lib/rigor/inference/deep_erasure.rb +138 -0
- data/lib/rigor/inference/def_return_typer.rb +159 -9
- data/lib/rigor/inference/definer_resolution.rb +17 -3
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +62 -13
- data/lib/rigor/inference/global_write_census.rb +36 -1
- data/lib/rigor/inference/ivar_writer_census.rb +195 -0
- data/lib/rigor/inference/loop_value_record.rb +53 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +110 -9
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
- data/lib/rigor/inference/method_dispatcher.rb +46 -16
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +12 -0
- data/lib/rigor/inference/mutation_widening.rb +5 -2
- data/lib/rigor/inference/narrowing.rb +66 -5
- data/lib/rigor/inference/object_mixins.rb +13 -1
- data/lib/rigor/inference/project_seed_tables.rb +2 -1
- data/lib/rigor/inference/project_yielder.rb +128 -0
- data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
- data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
- data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
- data/lib/rigor/inference/scope_indexer.rb +480 -103
- data/lib/rigor/inference/self_call_ivar_writes.rb +18 -6
- data/lib/rigor/inference/statement_evaluator.rb +658 -90
- data/lib/rigor/inference/synthetic_method_index.rb +23 -0
- data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
- data/lib/rigor/language_server/buffer_table.rb +6 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
- data/lib/rigor/language_server/project_context.rb +59 -3
- data/lib/rigor/language_server/server.rb +5 -3
- data/lib/rigor/mcp/server.rb +9 -1
- data/lib/rigor/plugin/base.rb +26 -11
- data/lib/rigor/plugin/manifest.rb +29 -3
- data/lib/rigor/plugin/registry.rb +38 -0
- data/lib/rigor/protection/discovery_seed.rb +57 -2
- data/lib/rigor/rooted_glob.rb +10 -1
- data/lib/rigor/scope.rb +14 -5
- data/lib/rigor/sig_gen/generator.rb +308 -37
- data/lib/rigor/sig_gen/observation_collector.rb +4 -1
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +6 -3
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- data/lib/rigor/type/combinator.rb +4 -3
- data/lib/rigor/type/constant.rb +45 -3
- data/lib/rigor/type/hash_shape.rb +14 -9
- data/lib/rigor/type/struct_instance.rb +3 -2
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +60 -43
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +6 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +3 -2
- data/sig/rigor/effects/file_collection.rbs +5 -6
- data/sig/rigor/effects/registry.rbs +8 -6
- data/sig/rigor/effects/summary.rbs +6 -5
- data/sig/rigor/environment.rbs +5 -3
- data/sig/rigor/inference/optimistic_origin.rbs +11 -0
- data/sig/rigor/inference.rbs +6 -0
- data/sig/rigor/plugin/additional_initializer.rbs +5 -5
- data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
- data/sig/rigor/plugin/effect_attribution.rbs +25 -10
- data/sig/rigor/plugin/effect_edge.rbs +12 -5
- data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
- data/sig/rigor/plugin/io_boundary.rbs +4 -4
- data/sig/rigor/plugin/manifest.rbs +16 -18
- data/sig/rigor/plugin/protocol_contract.rbs +10 -9
- data/sig/rigor/plugin/registry.rbs +1 -1
- data/sig/rigor/scope.rbs +31 -50
- data/sig/rigor/type.rbs +0 -6
- metadata +20 -1
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
# Issue #1154 — the instance-variable writers {GlobalWriteCensus} records for the declared-parameter credit
|
|
8
|
+
# ({ScopeIndexer::DeclaredParamWrites}), which must not type an ivar a writer the class-ivar census never walks may
|
|
9
|
+
# replace. The standard is parity with a literal write: `@x = 1` in `initialize` already reads `1` in every sibling
|
|
10
|
+
# method whatever an unknown object's code does, so only the writers that literal writes would also have to answer
|
|
11
|
+
# for are recorded, and an unknown-receiver writer records nothing (as `ReflectiveIvarWrite` leaves
|
|
12
|
+
# `o.instance_variable_set` alone). Three entry kinds go into the census `Set`:
|
|
13
|
+
#
|
|
14
|
+
# - `[:ivar_set, name]` — a literal `o.instance_variable_set(:@x, v)` (or the same through a `send` that spells
|
|
15
|
+
# it) on an explicit receiver other than `self`, and an `@x` write in the literal block of an
|
|
16
|
+
# `o.instance_eval { … }` / `instance_exec`: code that names the ivar it writes.
|
|
17
|
+
# - `[:ivar_set_class, class_name]` — an open-world writer ({#open_world?}) with an implicit or `self` receiver
|
|
18
|
+
# inside an instance body of the class — a `def` or a `define_method` block in its class body, in the block of a
|
|
19
|
+
# `K = Struct.new` / `Data.define` / `Class.new` constant, or in a `K.class_eval` block (the host walk passes
|
|
20
|
+
# that class as `owner`): it may write any ivar of the class's instances. A writer in a class body, a
|
|
21
|
+
# `def self.` or at the top level records nothing.
|
|
22
|
+
# - `[:own_write, class_name, name]` — an `@x` write inside an instance body of the class.
|
|
23
|
+
# {ScopeIndexer#merge_constant_literal_tables} appends the file's path, so a reader can tell whether a second
|
|
24
|
+
# file writes the same ivar of the same class.
|
|
25
|
+
class IvarWriterCensus
|
|
26
|
+
SEND_CALLS = %i[send __send__ public_send].to_set.freeze
|
|
27
|
+
# The methods a `send` reaching which is an open-world writer.
|
|
28
|
+
OPEN_WORLD_SENDS = %i[instance_variable_set instance_eval instance_exec extend
|
|
29
|
+
define_singleton_method].to_set.freeze
|
|
30
|
+
EVAL_CALLS = %i[instance_eval instance_exec].to_set.freeze
|
|
31
|
+
# The calls on a `singleton_class` receiver that give the object methods or code the census cannot read.
|
|
32
|
+
SINGLETON_CLASS_CALLS = %i[
|
|
33
|
+
class_eval class_exec module_eval module_exec define_method include prepend extend attr attr_reader
|
|
34
|
+
attr_writer attr_accessor send public_send __send__
|
|
35
|
+
].to_set.freeze
|
|
36
|
+
NO_ARGUMENTS = [].freeze
|
|
37
|
+
|
|
38
|
+
# The call names any rule here reads, so every other call returns at one Set probe.
|
|
39
|
+
# (`define_method` is in {SINGLETON_CLASS_CALLS}.)
|
|
40
|
+
WATCHED_CALLS = (SEND_CALLS | OPEN_WORLD_SENDS | EVAL_CALLS | SINGLETON_CLASS_CALLS).freeze
|
|
41
|
+
|
|
42
|
+
def initialize(census)
|
|
43
|
+
@census = census
|
|
44
|
+
@instance_ranges = []
|
|
45
|
+
@owner_names = {}.compare_by_identity
|
|
46
|
+
@own_writes = {}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Fed each node of a pre-order descent. `owner` is the class whose body the node sits in, as its qualified name
|
|
50
|
+
# or as the host walk's prefix Array (joined here only when an entry needs it), or nil where no class owns an
|
|
51
|
+
# instance (`class << self`, the top level, an opaque block).
|
|
52
|
+
def visit(node, owner)
|
|
53
|
+
case node
|
|
54
|
+
when Prism::DefNode then remember(node.location, owner) if owner && node.receiver.nil?
|
|
55
|
+
when Prism::SingletonClassNode then visit_singleton_class(node)
|
|
56
|
+
when Prism::CallNode then visit_call(node, owner) if WATCHED_CALLS.include?(node.name)
|
|
57
|
+
when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
|
|
58
|
+
Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableTargetNode
|
|
59
|
+
record_own_write(node) unless @instance_ranges.empty?
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# One entry per class and ivar: a repeated write costs a Hash probe, not a location and an Array.
|
|
66
|
+
def record_own_write(node)
|
|
67
|
+
owner = instance_owner(node)
|
|
68
|
+
return if owner.nil?
|
|
69
|
+
|
|
70
|
+
seen = (@own_writes[owner] ||= {})
|
|
71
|
+
return if seen[node.name]
|
|
72
|
+
|
|
73
|
+
seen[node.name] = true
|
|
74
|
+
@census << [:own_write, owner_name(owner), node.name].freeze
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def owner_name(owner) = owner.is_a?(Array) ? (@owner_names[owner] ||= owner.join("::")) : owner
|
|
78
|
+
|
|
79
|
+
def visit_call(node, owner)
|
|
80
|
+
remember(node.block.location, owner) if owner && define_method_block?(node)
|
|
81
|
+
if self_receiver?(node) || own_singleton_class_call?(node)
|
|
82
|
+
return if @instance_ranges.empty? || !(own_singleton_class_call?(node) || open_world?(node))
|
|
83
|
+
|
|
84
|
+
instance = instance_owner(node)
|
|
85
|
+
record_class(instance) if instance
|
|
86
|
+
else
|
|
87
|
+
visit_foreign(node)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# `class << self` inside an instance `def` opens the instance's singleton class.
|
|
92
|
+
def visit_singleton_class(node)
|
|
93
|
+
return unless node.expression.is_a?(Prism::SelfNode) && !@instance_ranges.empty?
|
|
94
|
+
|
|
95
|
+
instance = instance_owner(node)
|
|
96
|
+
record_class(instance) if instance
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def record_class(owner) = @census << [:ivar_set_class, owner_name(owner)].freeze
|
|
100
|
+
|
|
101
|
+
# The self writers whose target or code the census cannot read: a `send` with a computed method name or one
|
|
102
|
+
# naming {OPEN_WORLD_SENDS}; `instance_eval` / `instance_exec` given a block argument, and `instance_eval`
|
|
103
|
+
# given a string; `extend`; `define_singleton_method` with a body; and an `instance_variable_set` whose name no
|
|
104
|
+
# literal spells. A call in {SINGLETON_CLASS_CALLS} on the object's own `singleton_class` counts as well.
|
|
105
|
+
def open_world?(node)
|
|
106
|
+
name = node.name
|
|
107
|
+
arguments = node.arguments&.arguments || NO_ARGUMENTS
|
|
108
|
+
return open_world_send?(arguments) if SEND_CALLS.include?(name)
|
|
109
|
+
return open_world_eval?(node, arguments) if EVAL_CALLS.include?(name)
|
|
110
|
+
# #1931's census already makes every ivar of the class gradual for one in the same file; this entry covers one
|
|
111
|
+
# in a reopening in another file, which that census does not see.
|
|
112
|
+
return ivar_name(arguments.first).nil? if name == :instance_variable_set
|
|
113
|
+
|
|
114
|
+
name == :extend || (name == :define_singleton_method && (!node.block.nil? || arguments.size > 1))
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def open_world_send?(arguments)
|
|
118
|
+
return false if arguments.empty?
|
|
119
|
+
|
|
120
|
+
sent = literal_name(arguments.first)
|
|
121
|
+
sent.nil? || OPEN_WORLD_SENDS.include?(sent)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def open_world_eval?(node, arguments)
|
|
125
|
+
node.block.is_a?(Prism::BlockArgumentNode) || (node.name == :instance_eval && !arguments.empty?)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# A call on another receiver: a named foreign write, or `self.singleton_class.class_eval` and its kin.
|
|
129
|
+
def visit_foreign(node)
|
|
130
|
+
arguments = node.arguments&.arguments || NO_ARGUMENTS
|
|
131
|
+
name = node.name
|
|
132
|
+
if SEND_CALLS.include?(name) && arguments.first && literal_name(arguments.first) == :instance_variable_set
|
|
133
|
+
name = :instance_variable_set
|
|
134
|
+
arguments = arguments.drop(1)
|
|
135
|
+
end
|
|
136
|
+
if name == :instance_variable_set
|
|
137
|
+
ivar = ivar_name(arguments.first)
|
|
138
|
+
@census << [:ivar_set, ivar].freeze if ivar
|
|
139
|
+
elsif EVAL_CALLS.include?(name) && node.block.is_a?(Prism::BlockNode)
|
|
140
|
+
record_block_ivar_writes(node.block)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def record_block_ivar_writes(node)
|
|
145
|
+
return unless node.is_a?(Prism::Node)
|
|
146
|
+
return if node.is_a?(Prism::DefNode) || node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode)
|
|
147
|
+
|
|
148
|
+
case node
|
|
149
|
+
when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
|
|
150
|
+
Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableTargetNode
|
|
151
|
+
@census << [:ivar_set, node.name].freeze
|
|
152
|
+
end
|
|
153
|
+
node.rigor_each_child { |child| record_block_ivar_writes(child) }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def define_method_block?(node)
|
|
157
|
+
node.name == :define_method && self_receiver?(node) && node.block.is_a?(Prism::BlockNode)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# An instance body (`def` or `define_method` block) and the class it belongs to. A `define_method` block's own
|
|
161
|
+
# nodes may reach the walk with no owner (the walk reads its `self` as opaque), so the class rides the range.
|
|
162
|
+
def remember(location, owner)
|
|
163
|
+
@instance_ranges << [(location.start_offset...location.end_offset), owner]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# The class of the innermost instance body covering `node` (pre-order, so the last covering range), or nil.
|
|
167
|
+
def instance_owner(node)
|
|
168
|
+
offset = node.location.start_offset
|
|
169
|
+
@instance_ranges.reverse_each { |(range, owner)| return owner if range.cover?(offset) }
|
|
170
|
+
nil
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def self_receiver?(node) = node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
174
|
+
|
|
175
|
+
# A call in {SINGLETON_CLASS_CALLS} on the object's own `singleton_class` (`singleton_class.class_eval { … }`).
|
|
176
|
+
def own_singleton_class_call?(node)
|
|
177
|
+
receiver = node.receiver
|
|
178
|
+
receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class && self_receiver?(receiver) &&
|
|
179
|
+
SINGLETON_CLASS_CALLS.include?(node.name)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def literal_name(argument)
|
|
183
|
+
return unless argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode)
|
|
184
|
+
|
|
185
|
+
text = argument.unescaped
|
|
186
|
+
text.to_sym if text.valid_encoding?
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def ivar_name(argument)
|
|
190
|
+
name = literal_name(argument)
|
|
191
|
+
name if name.to_s.start_with?("@")
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
# The value the statement evaluator last gave a loop node, and the scope it evaluated the loop from, so
|
|
6
|
+
# `ExpressionTyper#type_of_loop` reads it instead of evaluating the loop again (issue #2031 review). A loop's value
|
|
7
|
+
# costs an evaluation of its body, its fixpoint and its `break` passes included, and the typer types a loop again
|
|
8
|
+
# wherever it types a node that holds one: the value of a write that wraps it (`id(z = (while …))`), a call it is
|
|
9
|
+
# the receiver of (`(while …).itself`), each narrowing and rule that types that operand. Each such typing of a
|
|
10
|
+
# loop nested in another loop re-ran the inner loop's passes, once more per level of nesting.
|
|
11
|
+
#
|
|
12
|
+
# The record is reused only for the very scope object the loop was evaluated from, so it answers exactly what an
|
|
13
|
+
# evaluation would. It is not compared with `Scope#==`, which ignores several fields (`match_frame`,
|
|
14
|
+
# `declared_refinements`, `struct_fold_safe_locals`, the opaque-block flags, `lexical_nesting`, the origins tables,
|
|
15
|
+
# `discovery`): across the cost spec's 10933 reads every match was an identity match, and `lib/` read no record at
|
|
16
|
+
# all (issue #2262). It lives in a per-thread map whose keys are held weakly, so a record goes when its parse tree
|
|
17
|
+
# does.
|
|
18
|
+
module LoopValueRecord
|
|
19
|
+
KEY = :rigor_loop_value_record
|
|
20
|
+
private_constant :KEY
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
COUNT_KEY = :rigor_loop_value_count
|
|
25
|
+
private_constant :COUNT_KEY
|
|
26
|
+
|
|
27
|
+
def record(node, scope, type)
|
|
28
|
+
Thread.current[COUNT_KEY] = loops_evaluated + 1
|
|
29
|
+
conditional(node, scope, type)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# How many loops this thread has evaluated, so a conditional can tell whether its evaluation held one.
|
|
33
|
+
def loops_evaluated = Thread.current[COUNT_KEY] || 0
|
|
34
|
+
|
|
35
|
+
# The same record for a conditional that evaluated a loop in a branch: `id(c ? (while …) : 0)` types the
|
|
36
|
+
# conditional again from the scope it was evaluated from, and each typing evaluated the loop again from a
|
|
37
|
+
# branch scope of its own, which multiplied the passes of every loop nested in it (issue #2262).
|
|
38
|
+
def conditional(node, scope, type)
|
|
39
|
+
(Thread.current[KEY] ||= ObjectSpace::WeakKeyMap.new)[node] = [scope, type]
|
|
40
|
+
type
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The recorded value of `node` evaluated from `scope` itself, or nil.
|
|
44
|
+
def fetch(node, scope)
|
|
45
|
+
slot = Thread.current[KEY]&.[](node)
|
|
46
|
+
return nil if slot.nil?
|
|
47
|
+
|
|
48
|
+
recorded, type = slot
|
|
49
|
+
recorded.equal?(scope) ? type : nil
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -88,6 +88,18 @@ module Rigor
|
|
|
88
88
|
@forwarded_block == name
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
+
# Issue #1157 — whether a call's `&expr` block argument hands on the block THIS method was called with: the
|
|
92
|
+
# anonymous `&` of a method that declares an anonymous `&` parameter, or {#forwarded_block?} for `&name`.
|
|
93
|
+
# Anything else (`&:sym`, a proc made here, a rebound name) is a block of this frame's own making.
|
|
94
|
+
def forwards_block?(block_argument)
|
|
95
|
+
expression = block_argument.expression
|
|
96
|
+
case expression
|
|
97
|
+
when nil then anonymous_block_parameter?
|
|
98
|
+
when Prism::LocalVariableReadNode then forwarded_block?(expression.name)
|
|
99
|
+
else false
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
91
103
|
# The scan of `node` under `scope`, kept while `scope`'s local and instance-variable tables are the same
|
|
92
104
|
# objects: a lookup argument's answer reads them ({MatchRebinding.may_match?}), and a rebuild that leaves
|
|
93
105
|
# them alone passes the same tables on. `kind` keeps another reading of the same node apart (issue #1365:
|
|
@@ -125,6 +137,13 @@ module Rigor
|
|
|
125
137
|
name
|
|
126
138
|
end
|
|
127
139
|
|
|
140
|
+
# True when the method declares a bare `&` parameter, which only an anonymous `&` argument can read.
|
|
141
|
+
def anonymous_block_parameter?
|
|
142
|
+
parameters = @parameters
|
|
143
|
+
block = parameters.is_a?(Prism::ParametersNode) ? parameters.block : nil
|
|
144
|
+
!block.nil? && block.name.nil?
|
|
145
|
+
end
|
|
146
|
+
|
|
128
147
|
# The method's own `&block` parameter's name, or nil.
|
|
129
148
|
def block_parameter_name
|
|
130
149
|
parameters = @parameters
|
|
@@ -219,6 +219,15 @@ module Rigor
|
|
|
219
219
|
# declared `Numeric`, on which `(2 ** 20_000).even?` is an undefined method.
|
|
220
220
|
WidenedResult = Data.define(:class_name)
|
|
221
221
|
|
|
222
|
+
# #2125 — a String result past `STRING_FOLD_BYTE_LIMIT`, or an operand that would build one. Unlike a
|
|
223
|
+
# `WidenedResult` it declines the WHOLE fold, every other member of a union receiver included: dropping
|
|
224
|
+
# only the oversized member would answer the rest (`(c ? "a" : big) * 2` read `"aa"`). The next tier then
|
|
225
|
+
# answers the call — `LiteralStringFolding`'s `literal-string`, or RBS's `String`, never less than the
|
|
226
|
+
# `Nominal[String]` the fold could have widened to.
|
|
227
|
+
OversizedString = Data.define
|
|
228
|
+
OVERSIZED_STRING = [OversizedString.new].freeze
|
|
229
|
+
private_constant :OversizedString, :OVERSIZED_STRING
|
|
230
|
+
|
|
222
231
|
# Input cartesian product hard cap. Keeps fold cost bounded even when the receiver and argument are
|
|
223
232
|
# both `Union[Constant…]`. 5 × 5 = 25 inputs is permitted; 6 × 6 = 36 is not. The user-facing payoff
|
|
224
233
|
# (a precise small enum) drops off fast past this range and CRuby method invocation cost adds up.
|
|
@@ -331,6 +340,7 @@ module Rigor
|
|
|
331
340
|
arg = args.first
|
|
332
341
|
ruby_arg = format_argument_value(arg)
|
|
333
342
|
return nil if ruby_arg.nil?
|
|
343
|
+
return nil if format_blow_up?(receiver.value, ruby_arg)
|
|
334
344
|
|
|
335
345
|
result = receiver.value % ruby_arg
|
|
336
346
|
return nil unless foldable_constant_value?(result)
|
|
@@ -1200,6 +1210,8 @@ module Rigor
|
|
|
1200
1210
|
# "Integer-ness" was already implied by the inputs (so the widening fallback only fires for
|
|
1201
1211
|
# arithmetic over integers).
|
|
1202
1212
|
def build_constant_type(values, source: nil)
|
|
1213
|
+
return nil if values.any?(OversizedString)
|
|
1214
|
+
|
|
1203
1215
|
widened, values = values.partition { |value| value.is_a?(WidenedResult) }
|
|
1204
1216
|
return build_scalar_constant_type(values, source) if widened.empty?
|
|
1205
1217
|
|
|
@@ -1213,7 +1225,8 @@ module Rigor
|
|
|
1213
1225
|
def build_scalar_constant_type(values, source)
|
|
1214
1226
|
return nil if values.empty?
|
|
1215
1227
|
|
|
1216
|
-
|
|
1228
|
+
# #1513 — keyed so the possible results `0.0` and `-0.0` stay two members, as `Constant#==` keeps them.
|
|
1229
|
+
unique = values.uniq { |value| Type::Constant.value_key(value) }
|
|
1217
1230
|
return collapse_constants(unique) if unique.size <= UNION_FOLD_OUTPUT_LIMIT
|
|
1218
1231
|
|
|
1219
1232
|
widen_to_integer_range(unique, source) ||
|
|
@@ -1643,6 +1656,7 @@ module Rigor
|
|
|
1643
1656
|
# `flat_map`.
|
|
1644
1657
|
def invoke_binary(receiver_value, method_name, arg_value)
|
|
1645
1658
|
return nil unless safe?(receiver_value, method_name, arg_value)
|
|
1659
|
+
return OVERSIZED_STRING if string_blow_up?(receiver_value, method_name, arg_value)
|
|
1646
1660
|
return nil if range_enumeration_blow_up?(receiver_value, method_name)
|
|
1647
1661
|
|
|
1648
1662
|
widened = numeric_blow_up_class(receiver_value, method_name, arg_value)
|
|
@@ -1658,6 +1672,7 @@ module Rigor
|
|
|
1658
1672
|
# `false`/`nil` folds.
|
|
1659
1673
|
def invoke_ternary(receiver_value, method_name, av0, av1)
|
|
1660
1674
|
return nil unless ternary_method_allowed?(receiver_value, method_name)
|
|
1675
|
+
return OVERSIZED_STRING if string_ternary_blow_up?(receiver_value, method_name, av0, av1)
|
|
1661
1676
|
|
|
1662
1677
|
fold_result(receiver_value.public_send(method_name, av0, av1))
|
|
1663
1678
|
rescue StandardError
|
|
@@ -1667,7 +1682,7 @@ module Rigor
|
|
|
1667
1682
|
# Returns `[value]` on success, `nil` to signal "skip". See `invoke_binary` for why we wrap.
|
|
1668
1683
|
def invoke_unary(receiver_value, method_name)
|
|
1669
1684
|
return nil unless unary_safe?(receiver_value, method_name)
|
|
1670
|
-
return
|
|
1685
|
+
return OVERSIZED_STRING if string_unary_blow_up?(receiver_value, method_name)
|
|
1671
1686
|
return nil if range_enumeration_blow_up?(receiver_value, method_name)
|
|
1672
1687
|
|
|
1673
1688
|
fold_result(receiver_value.public_send(method_name))
|
|
@@ -1675,10 +1690,11 @@ module Rigor
|
|
|
1675
1690
|
nil
|
|
1676
1691
|
end
|
|
1677
1692
|
|
|
1678
|
-
# `[result]` for a foldable value, `[WidenedResult]` for a number past `INTEGER_FOLD_BIT_LIMIT`,
|
|
1679
|
-
# for anything else.
|
|
1693
|
+
# `[result]` for a foldable value, `[WidenedResult]` for a number past `INTEGER_FOLD_BIT_LIMIT`,
|
|
1694
|
+
# `OVERSIZED_STRING` for a String past `STRING_FOLD_BYTE_LIMIT`, nil for anything else.
|
|
1680
1695
|
def fold_result(result)
|
|
1681
1696
|
return [WidenedResult.new(result.class.name)] if oversized_number?(result)
|
|
1697
|
+
return OVERSIZED_STRING if oversized_string?(result)
|
|
1682
1698
|
|
|
1683
1699
|
foldable_constant_value?(result) ? [result] : nil
|
|
1684
1700
|
end
|
|
@@ -1822,10 +1838,20 @@ module Rigor
|
|
|
1822
1838
|
return false if value.is_a?(Float) && value.nan?
|
|
1823
1839
|
return false if value.is_a?(Complex) && complex_nan?(value)
|
|
1824
1840
|
return false if oversized_number?(value)
|
|
1841
|
+
return false if oversized_string?(value)
|
|
1825
1842
|
|
|
1826
1843
|
true
|
|
1827
1844
|
end
|
|
1828
1845
|
|
|
1846
|
+
# #2125 — the result gate every String-producing fold passes through, the String twin of
|
|
1847
|
+
# `oversized_number?`. The operand prechecks (`string_blow_up?`, `string_ternary_blow_up?`,
|
|
1848
|
+
# `format_blow_up?`) keep the costly cases from being built at all; this catches the rest after the
|
|
1849
|
+
# fact (`tr` into multibyte characters, `dump`, `Integer#to_s(2)`, whose receiver
|
|
1850
|
+
# `INTEGER_FOLD_BIT_LIMIT` already bounds, so its rendering is at most 13 KB).
|
|
1851
|
+
def oversized_string?(value)
|
|
1852
|
+
value.is_a?(String) && value.bytesize > STRING_FOLD_BYTE_LIMIT
|
|
1853
|
+
end
|
|
1854
|
+
|
|
1829
1855
|
# True when an Integer, or an Integer part of a Rational / Complex, is past `INTEGER_FOLD_BIT_LIMIT`.
|
|
1830
1856
|
def oversized_number?(value)
|
|
1831
1857
|
case value
|
|
@@ -1874,7 +1900,6 @@ module Rigor
|
|
|
1874
1900
|
catalog_allows?(receiver_value, method_name)
|
|
1875
1901
|
return false unless allowed
|
|
1876
1902
|
return false if integer_division_by_zero?(receiver_value, method_name, arg_value)
|
|
1877
|
-
return false if string_blow_up?(receiver_value, method_name, arg_value)
|
|
1878
1903
|
|
|
1879
1904
|
true
|
|
1880
1905
|
end
|
|
@@ -1960,16 +1985,91 @@ module Rigor
|
|
|
1960
1985
|
case method_name
|
|
1961
1986
|
when :+ then string_concat_blow_up?(receiver_value, arg_value)
|
|
1962
1987
|
when :* then string_repeat_blow_up?(receiver_value, arg_value)
|
|
1988
|
+
when :% then format_blow_up?(receiver_value, arg_value)
|
|
1963
1989
|
when :center, :ljust, :rjust then string_pad_blow_up?(arg_value)
|
|
1964
1990
|
else false
|
|
1965
1991
|
end
|
|
1966
1992
|
end
|
|
1967
1993
|
|
|
1994
|
+
# #2125 — the two-argument String folds that can grow past the input: the padded `center` / `ljust`
|
|
1995
|
+
# / `rjust` (width, pad) and `sub` / `gsub` (pattern, replacement).
|
|
1996
|
+
def string_ternary_blow_up?(receiver_value, method_name, av0, av1)
|
|
1997
|
+
return false unless receiver_value.is_a?(String)
|
|
1998
|
+
|
|
1999
|
+
case method_name
|
|
2000
|
+
when :center, :ljust, :rjust then string_pad_blow_up?(av0)
|
|
2001
|
+
when :sub, :gsub then string_substitution_blow_up?(receiver_value, method_name, av1)
|
|
2002
|
+
else false
|
|
2003
|
+
end
|
|
2004
|
+
end
|
|
2005
|
+
|
|
2006
|
+
# An upper bound on a `sub` / `gsub` result with a String replacement: `gsub` matches at most once per
|
|
2007
|
+
# position plus once at the end, and each replacement is the replacement's bytes plus, for every
|
|
2008
|
+
# backslash in it (`\0`, `\1`, `\k<name>`, `` \` ``, `\'`), at most the receiver's bytes again.
|
|
2009
|
+
def string_substitution_blow_up?(receiver_value, method_name, replacement)
|
|
2010
|
+
return false unless replacement.is_a?(String)
|
|
2011
|
+
|
|
2012
|
+
size = receiver_value.bytesize
|
|
2013
|
+
matches = method_name == :gsub ? size + 1 : 1
|
|
2014
|
+
per_match = replacement.bytesize + (replacement.count("\\") * size)
|
|
2015
|
+
size + (matches * per_match) > STRING_FOLD_BYTE_LIMIT
|
|
2016
|
+
end
|
|
2017
|
+
|
|
2018
|
+
# #2125 — `String#%` / `Kernel#format` pad to a field width and render to a precision the format
|
|
2019
|
+
# string spells (`"%02000000000d"`, `"%.100000f"`) or an argument supplies (`"%*d" % [n, 1]`).
|
|
2020
|
+
# Formatting builds the whole padded string before any result gate can look at it — two billion
|
|
2021
|
+
# bytes took 13 s and 4 GB — so the numbers are read from the directives first. A `*` width counts
|
|
2022
|
+
# every Integer argument as a possible width, and a `%%` escape matches as a whole, carrying no digits.
|
|
2023
|
+
FORMAT_DIRECTIVE = /%%|%(?:<[^>]*>|\{[^}]*\}|[-+ 0#*$.\d])*/
|
|
2024
|
+
private_constant :FORMAT_DIRECTIVE
|
|
2025
|
+
|
|
2026
|
+
def format_blow_up?(format_string, arguments)
|
|
2027
|
+
star = false
|
|
2028
|
+
format_string.scan(FORMAT_DIRECTIVE) do |directive|
|
|
2029
|
+
spec = directive.gsub(/<[^>]*>|\{[^}]*\}|\d+\$/, "")
|
|
2030
|
+
return true if spec.scan(/\d+/).any? { |digits| digits.to_i > STRING_FOLD_BYTE_LIMIT }
|
|
2031
|
+
|
|
2032
|
+
star ||= spec.include?("*")
|
|
2033
|
+
end
|
|
2034
|
+
star && format_argument_values(arguments).any? do |value|
|
|
2035
|
+
width = integer_operand(value)
|
|
2036
|
+
!width.nil? && width.abs > STRING_FOLD_BYTE_LIMIT
|
|
2037
|
+
end
|
|
2038
|
+
end
|
|
2039
|
+
|
|
2040
|
+
def format_argument_values(arguments)
|
|
2041
|
+
case arguments
|
|
2042
|
+
when Hash then arguments.values
|
|
2043
|
+
when Array then arguments
|
|
2044
|
+
else [arguments]
|
|
2045
|
+
end
|
|
2046
|
+
end
|
|
2047
|
+
|
|
1968
2048
|
# `"x".center(width)` / `#ljust` / `#rjust` produce a string of `max(width, len)` characters. A
|
|
1969
2049
|
# literal `width` far larger than the receiver would materialise a huge Constant; cap it at the same
|
|
1970
2050
|
# byte limit the concat / repeat paths use.
|
|
1971
2051
|
def string_pad_blow_up?(arg_value)
|
|
1972
|
-
|
|
2052
|
+
width = integer_operand(arg_value)
|
|
2053
|
+
!width.nil? && width > STRING_FOLD_BYTE_LIMIT
|
|
2054
|
+
end
|
|
2055
|
+
|
|
2056
|
+
# The Integer a width or count operand stands for. Ruby converts a Float or Rational one with `NUM2LONG`
|
|
2057
|
+
# (`"x" * 2.9` is `"xx"`, `"x".ljust(2e9)` pads to two billion), and a Complex one whose imaginary part is an
|
|
2058
|
+
# exact zero through its real part (`"x" * Complex(3, 0)` is `"xxx"`; `Complex(3, 0.0)` raises), so the
|
|
2059
|
+
# prechecks read it the same way. Those are every Numeric class a `Constant` carries. A non-finite Float
|
|
2060
|
+
# raises in Ruby, and any other operand is left to the call, so both answer nil.
|
|
2061
|
+
def integer_operand(value)
|
|
2062
|
+
case value
|
|
2063
|
+
when Integer then value
|
|
2064
|
+
when Float then value.finite? ? value.to_i : nil
|
|
2065
|
+
when Rational then value.to_i
|
|
2066
|
+
when Complex then exact_real_operand(value)
|
|
2067
|
+
end
|
|
2068
|
+
end
|
|
2069
|
+
|
|
2070
|
+
def exact_real_operand(value)
|
|
2071
|
+
imaginary = value.imaginary
|
|
2072
|
+
imaginary.is_a?(Integer) && imaginary.zero? ? integer_operand(value.real) : nil
|
|
1973
2073
|
end
|
|
1974
2074
|
|
|
1975
2075
|
def string_concat_blow_up?(receiver_value, arg_value)
|
|
@@ -1978,10 +2078,11 @@ module Rigor
|
|
|
1978
2078
|
end
|
|
1979
2079
|
|
|
1980
2080
|
def string_repeat_blow_up?(receiver_value, arg_value)
|
|
1981
|
-
|
|
1982
|
-
|
|
2081
|
+
# A negative count raises (`ArgumentError`), so the call itself skips that member, as any raising one is.
|
|
2082
|
+
count = integer_operand(arg_value)
|
|
2083
|
+
return false if count.nil? || count.negative?
|
|
1983
2084
|
|
|
1984
|
-
receiver_value.bytesize *
|
|
2085
|
+
receiver_value.bytesize * count > STRING_FOLD_BYTE_LIMIT
|
|
1985
2086
|
end
|
|
1986
2087
|
end
|
|
1987
2088
|
end
|
|
@@ -191,12 +191,16 @@ module Rigor
|
|
|
191
191
|
# Runs the real `Kernel#format` when everything is value-pinned. A malformed directive or an
|
|
192
192
|
# argument-count mismatch raises at fold time; the handler declines so the literal-string
|
|
193
193
|
# lift (or the RBS `String` envelope) answers instead. Results larger than the shared
|
|
194
|
-
# `ConstantFolding::STRING_FOLD_BYTE_LIMIT` decline too, keeping the carrier-size convention
|
|
194
|
+
# `ConstantFolding::STRING_FOLD_BYTE_LIMIT` decline too, keeping the carrier-size convention,
|
|
195
|
+
# and a field width or precision past it declines before the string is built (#2125).
|
|
195
196
|
def fold_format_constant(args)
|
|
196
197
|
return nil unless args.all?(Type::Constant)
|
|
197
198
|
return nil unless args.first.value.is_a?(String)
|
|
198
199
|
|
|
199
|
-
|
|
200
|
+
values = args.drop(1).map(&:value)
|
|
201
|
+
return nil if ConstantFolding.format_blow_up?(args.first.value, values)
|
|
202
|
+
|
|
203
|
+
result = format(args.first.value, *values)
|
|
200
204
|
return nil if result.bytesize > ConstantFolding::STRING_FOLD_BYTE_LIMIT
|
|
201
205
|
|
|
202
206
|
Type::Combinator.constant_of(result)
|
|
@@ -35,43 +35,76 @@ module Rigor
|
|
|
35
35
|
return nil unless SingletonFolding.receiver?(receiver, "Set")
|
|
36
36
|
|
|
37
37
|
case method_name
|
|
38
|
-
when :[] then fold_bracket(args)
|
|
39
|
-
when :new then fold_new(args)
|
|
38
|
+
when :[] then fold_bracket(args, context.call_node)
|
|
39
|
+
when :new then fold_new(args, context.call_node, context.block_type)
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
# `Set["a", "b", "c"]` — all positional args must be Constant.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
# `Set["a", "b", "c"]` — all positional args must be Constant to fold to a value. Otherwise the
|
|
44
|
+
# element type is the union of the arguments, widened off their literal pins (#2278).
|
|
45
|
+
def fold_bracket(args, call_node = nil)
|
|
46
|
+
return nil if splatted?(call_node)
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
end
|
|
48
|
+
return Type::Combinator.constant_of(::Set.new(args.map(&:value))) if args.all?(Type::Constant)
|
|
50
49
|
|
|
51
|
-
|
|
50
|
+
typed_set(args)
|
|
52
51
|
rescue StandardError
|
|
53
52
|
nil
|
|
54
53
|
end
|
|
55
54
|
|
|
56
|
-
# `Set.new` / `Set.new(
|
|
57
|
-
|
|
55
|
+
# `Set.new` / `Set.new(enum)`. A call with a block declines: the block maps each element
|
|
56
|
+
# (`Set.new([1, 2], &:to_s)` holds `"1"` and `"2"`), so the argument's elements are not the set's.
|
|
57
|
+
def fold_new(args, call_node = nil, block_type = nil)
|
|
58
|
+
return nil if block_type || block_given?(call_node) || splatted?(call_node)
|
|
58
59
|
return Type::Combinator.constant_of(::Set.new) if args.empty?
|
|
59
60
|
return nil if args.size > 1
|
|
60
61
|
|
|
61
62
|
arg = args.first
|
|
62
|
-
|
|
63
|
-
when Type::Tuple
|
|
64
|
-
return nil unless arg.elements.all?(Type::Constant)
|
|
65
|
-
|
|
66
|
-
arg.elements.map(&:value)
|
|
67
|
-
else
|
|
68
|
-
return nil
|
|
69
|
-
end
|
|
63
|
+
return fold_tuple(arg) if arg.is_a?(Type::Tuple)
|
|
70
64
|
|
|
71
|
-
|
|
65
|
+
element = nominal_element(arg)
|
|
66
|
+
element ? typed_set([element]) : nil
|
|
72
67
|
rescue StandardError
|
|
73
68
|
nil
|
|
74
69
|
end
|
|
70
|
+
|
|
71
|
+
def fold_tuple(tuple)
|
|
72
|
+
if tuple.elements.all?(Type::Constant)
|
|
73
|
+
return Type::Combinator.constant_of(::Set.new(tuple.elements.map(&:value)))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
typed_set(tuple.elements)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The element of an `Array[T]` / `Set[T]` argument, or nil for any other carrier.
|
|
80
|
+
def nominal_element(arg)
|
|
81
|
+
return nil unless arg.is_a?(Type::Nominal) && %w[Array Set].include?(arg.class_name)
|
|
82
|
+
return nil unless arg.type_args.size == 1
|
|
83
|
+
|
|
84
|
+
arg.type_args.first
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# `Nominal[Set, [E]]`, `E` the members widened as an `Array.new` fill is (value pins dropped, a literal
|
|
88
|
+
# container widened to its nominal) joined with a `Dynamic[top]` floor. The floor is the conservative half:
|
|
89
|
+
# nothing widens a Set's element when the program mutates it (`add`, `<<`, `merge`, `replace`, `clear`
|
|
90
|
+
# then `<<`, a callee that fills it), so the classes the set was built with are not a claim about what it
|
|
91
|
+
# holds later, and a read of the element must not report on them (#2278).
|
|
92
|
+
def typed_set(types)
|
|
93
|
+
return nil if types.empty?
|
|
94
|
+
|
|
95
|
+
members = types.map { |t| MethodDispatcher.array_new_element(t) }
|
|
96
|
+
Type::Combinator.nominal_of("Set", type_args: [Type::Combinator.union(*members, Type::Combinator.untyped)])
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def block_given?(call_node)
|
|
100
|
+
call_node.respond_to?(:block) && !call_node.block.nil?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def splatted?(call_node)
|
|
104
|
+
return false unless call_node.respond_to?(:arguments)
|
|
105
|
+
|
|
106
|
+
(call_node.arguments&.arguments || []).any?(Prism::SplatNode)
|
|
107
|
+
end
|
|
75
108
|
end
|
|
76
109
|
end
|
|
77
110
|
end
|