rigortype 0.4.2 → 0.4.4
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/data/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +28 -5
- data/docs/manual/02-cli-reference.md +23 -4
- data/docs/manual/03-configuration.md +21 -8
- data/docs/manual/04-diagnostics.md +44 -2
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/10-mcp-server.md +32 -3
- data/docs/manual/12-caching.md +18 -1
- data/docs/manual/14-rails-quickstart.md +2 -2
- data/docs/manual/18-removing-dead-code.md +26 -9
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- data/lib/rigor/analysis/check_rules/rule_ids.rb +64 -0
- data/lib/rigor/analysis/check_rules.rb +68 -57
- data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
- data/lib/rigor/analysis/incremental.rb +12 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -3
- data/lib/rigor/analysis/incremental_session.rb +207 -40
- data/lib/rigor/analysis/path_expansion.rb +62 -6
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
- data/lib/rigor/analysis/reachability/graph.rb +139 -15
- data/lib/rigor/analysis/reachability/scan.rb +374 -173
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/rule_catalog.rb +6 -1
- data/lib/rigor/analysis/run_cache_key.rb +31 -7
- data/lib/rigor/analysis/run_cache_probe.rb +6 -4
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +74 -13
- data/lib/rigor/analysis/runner/pool_coordinator.rb +133 -19
- data/lib/rigor/analysis/runner/project_pre_passes.rb +46 -1
- data/lib/rigor/analysis/runner.rb +62 -18
- data/lib/rigor/analysis/severity_stamp.rb +34 -1
- data/lib/rigor/analysis/worker_session.rb +58 -2
- data/lib/rigor/cache/descriptor.rb +40 -6
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +56 -12
- data/lib/rigor/cli/check_command.rb +138 -39
- data/lib/rigor/cli/docs_command.rb +5 -4
- data/lib/rigor/cli/lens_command.rb +144 -0
- data/lib/rigor/cli/lsp_command.rb +5 -2
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/cli/skill_command.rb +3 -3
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/unused_command.rb +157 -24
- data/lib/rigor/cli.rb +30 -4
- data/lib/rigor/config_audit.rb +111 -15
- data/lib/rigor/configuration/dependencies.rb +19 -11
- data/lib/rigor/configuration.rb +15 -0
- data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
- data/lib/rigor/environment/installed_gem_set.rb +38 -11
- data/lib/rigor/environment/lockfile_resolver.rb +37 -6
- data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
- data/lib/rigor/environment/rbs_loader.rb +79 -10
- data/lib/rigor/environment/required_features.rb +4 -2
- data/lib/rigor/environment.rb +12 -5
- data/lib/rigor/inference/block_call_timing.rb +5 -0
- data/lib/rigor/inference/builtins/cruby_definers.rb +22 -5
- data/lib/rigor/inference/captured_locals.rb +69 -6
- data/lib/rigor/inference/definer_resolution.rb +16 -7
- data/lib/rigor/inference/expression_typer.rb +117 -29
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +21 -17
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- data/lib/rigor/inference/match_rebinding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -16
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
- data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +52 -3
- data/lib/rigor/inference/module_function_state.rb +80 -13
- data/lib/rigor/inference/narrowing.rb +184 -68
- data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
- data/lib/rigor/inference/project_patched_methods.rb +2 -2
- data/lib/rigor/inference/project_patched_scanner.rb +38 -24
- data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +112 -40
- data/lib/rigor/inference/refined_dispatch.rb +62 -14
- data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +416 -1
- data/lib/rigor/inference/scope_indexer.rb +565 -61
- data/lib/rigor/inference/self_call_ivar_writes.rb +341 -0
- data/lib/rigor/inference/statement_evaluator.rb +307 -60
- data/lib/rigor/inference/untyped_rejoin.rb +75 -0
- data/lib/rigor/language_server/project_context.rb +56 -5
- data/lib/rigor/lens/anchor.rb +115 -0
- data/lib/rigor/lens/contract_reader.rb +133 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +802 -0
- data/lib/rigor/lens/members.rb +203 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +322 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +41 -0
- data/lib/rigor/mcp/server.rb +61 -20
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/project_environment.rb +23 -1
- data/lib/rigor/protection/mutation_cache.rb +2 -1
- data/lib/rigor/scope.rb +33 -17
- data/lib/rigor/sig_gen/generator.rb +45 -5
- data/lib/rigor/sig_gen/layout_index.rb +36 -0
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- data/lib/rigor/sig_gen/writer.rb +47 -34
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +28 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- data/sig/rigor/scope.rbs +3 -0
- data/sig/rigor.rbs +6 -3
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- metadata +21 -1
|
@@ -101,13 +101,17 @@ module Rigor
|
|
|
101
101
|
# bodies from their CONVERGED bindings so per-line probes
|
|
102
102
|
# (`rigor annotate`) reflect the post-writeback state, not the
|
|
103
103
|
# cap-N intermediate constants. Off for the check path.
|
|
104
|
+
# @param widen_clauses — check-path flag (issue #1231): a rescue or
|
|
105
|
+
# `ensure` clause records the variables the `begin` body writes as
|
|
106
|
+
# `Dynamic[top]`. Off for every other reader, so parameter inference
|
|
107
|
+
# and sig-gen observe the clause's plain entry.
|
|
104
108
|
# @return identity-comparing
|
|
105
109
|
# table whose default value is `default_scope`.
|
|
106
110
|
# The no-op re-anchoring table (#722 residue 2): shared so the overwhelmingly common
|
|
107
111
|
# nothing-to-re-anchor walk allocates none.
|
|
108
112
|
EMPTY_RENAMES = {}.freeze
|
|
109
113
|
|
|
110
|
-
def index(root, default_scope:, converged_loop_recording: false) # rubocop:disable Metrics/AbcSize
|
|
114
|
+
def index(root, default_scope:, converged_loop_recording: false, widen_clauses: false) # rubocop:disable Metrics/AbcSize
|
|
111
115
|
# Slice A-declarations. Build the declaration overrides first so every scope handed to the StatementEvaluator
|
|
112
116
|
# already carries the table; structural sharing through `Scope#with_local` / `#with_fact` / `#with_self_type`
|
|
113
117
|
# propagates it across every derived scope.
|
|
@@ -194,7 +198,8 @@ module Rigor
|
|
|
194
198
|
|
|
195
199
|
on_enter = ->(node, scope) { table[node] = scope }
|
|
196
200
|
StatementEvaluator.new(scope: seeded_scope, on_enter: on_enter,
|
|
197
|
-
converged_loop_recording: converged_loop_recording
|
|
201
|
+
converged_loop_recording: converged_loop_recording,
|
|
202
|
+
widen_clauses: widen_clauses).evaluate(root)
|
|
198
203
|
|
|
199
204
|
propagate(root, table, seeded_scope)
|
|
200
205
|
table
|
|
@@ -502,6 +507,7 @@ module Rigor
|
|
|
502
507
|
method_assign_effects = build_method_assign_effects(root)
|
|
503
508
|
walk_class_ivars(root, [], default_scope, accumulator, mutated_ivars,
|
|
504
509
|
read_before_write, init_writes, method_assign_effects)
|
|
510
|
+
exempt_class_body_writes!(init_writes, accumulator.delete(DEFINE_METHOD_WRITES) || {})
|
|
505
511
|
merge_held_ivar_writes!(accumulator)
|
|
506
512
|
record_aliased_ivar_mutations!(root, mutated_ivars)
|
|
507
513
|
widen_mutated_ivar_entries!(accumulator, mutated_ivars)
|
|
@@ -509,6 +515,19 @@ module Rigor
|
|
|
509
515
|
accumulator.transform_values(&:freeze).freeze
|
|
510
516
|
end
|
|
511
517
|
|
|
518
|
+
# Folds the class-body writes {#collect_class_body_ivar_writes} held under {CLASS_BODY_WRITES} into the
|
|
519
|
+
# `init_writes` exemption, except for an ivar a `define_method` block of the class writes (#1768). Such an ivar
|
|
520
|
+
# had no census entry before the block's writes were counted, so the exemption never applied to it; a
|
|
521
|
+
# class-body `@x = nil` writes the class object's ivar and says nothing about whether an instance's `@x` is set,
|
|
522
|
+
# and exempting it would seed `@x` non-nil from the block's write alone (`if @x` folding always-truthy on an
|
|
523
|
+
# instance whose `@x` is unset). It keeps the read-before-write `nil` instead, which only widens.
|
|
524
|
+
def exempt_class_body_writes!(init_writes, define_method_writes)
|
|
525
|
+
(init_writes.delete(CLASS_BODY_WRITES) || {}).each do |class_name, ivars|
|
|
526
|
+
kept = ivars - (define_method_writes[class_name] || EMPTY_GUARDED_IVARS)
|
|
527
|
+
(init_writes[class_name] ||= Set.new).merge(kept)
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
|
|
512
531
|
# B2.3 — finalize the read-before-write nil contribution. For each class, for each ivar where SOME method body
|
|
513
532
|
# observed a read-before-write AND no `initialize` write exists for that ivar, contribute `Constant[nil]` to the
|
|
514
533
|
# class-wide accumulator.
|
|
@@ -849,6 +868,7 @@ module Rigor
|
|
|
849
868
|
owner = def_owner || qualified_prefix
|
|
850
869
|
collect_initializer_block_ivars(node, owner, default_scope, accumulator, mutated_ivars, init_writes)
|
|
851
870
|
record_attr_writer_ivars(node, owner, accumulator) if ATTR_WRITER_MACROS.include?(node.name)
|
|
871
|
+
collect_define_method_block_ivars(node, owner, default_scope, accumulator, mutated_ivars)
|
|
852
872
|
false
|
|
853
873
|
else
|
|
854
874
|
walk_ivars_meta_new?(node, qualified_prefix, default_scope, accumulator,
|
|
@@ -1060,6 +1080,44 @@ module Rigor
|
|
|
1060
1080
|
end
|
|
1061
1081
|
end
|
|
1062
1082
|
|
|
1083
|
+
# The `define_method` arm of {#walk_class_ivars} (#1768). `define_method(:m) { … }` on the implicit
|
|
1084
|
+
# or `self` receiver installs its block as an instance method of `owner`, and Ruby runs the body on
|
|
1085
|
+
# the instance, so its `@x = …` writes are the instance's ivars exactly as a `def m` body's are —
|
|
1086
|
+
# `DefineMethodBlockSelf` narrows the block's `self` the same way when it is evaluated. Without this
|
|
1087
|
+
# arm an ivar `initialize` sets to `nil` and a `define_method` block sets to a String read as `nil`
|
|
1088
|
+
# in every sibling `def`. The block is typed under the instance `self`, as a def body is. Only the
|
|
1089
|
+
# literal-block form counts: a `define_method(:m, callable)` or `&callable` argument is not walked.
|
|
1090
|
+
# `owner` is empty in a `class << self` body (the call defines a class method there) and in an
|
|
1091
|
+
# anonymous factory body, and the block contributes nothing there. A `define_method` inside a `def`
|
|
1092
|
+
# body is already reached by that body's walk ({#gather_ivar_writes} enters blocks).
|
|
1093
|
+
def collect_define_method_block_ivars(node, owner, default_scope, accumulator, mutated_ivars)
|
|
1094
|
+
return if owner.empty?
|
|
1095
|
+
return unless define_method_block_call?(node)
|
|
1096
|
+
|
|
1097
|
+
block = node.block
|
|
1098
|
+
return unless block.body
|
|
1099
|
+
|
|
1100
|
+
class_name = owner.join("::")
|
|
1101
|
+
body_scope = census_body_scope(default_scope, owner, Type::Combinator.nominal_of(class_name))
|
|
1102
|
+
gather_ivar_writes(block.body, body_scope, class_name, accumulator, EMPTY_GUARDED_IVARS, mutated_ivars)
|
|
1103
|
+
written = (accumulator[DEFINE_METHOD_WRITES] ||= {})[class_name] ||= Set.new
|
|
1104
|
+
define_method_written_ivars(block.body, written)
|
|
1105
|
+
end
|
|
1106
|
+
|
|
1107
|
+
# The ivars a `define_method` block body writes, in any write form, outside a nested `def` / `class` / `module`.
|
|
1108
|
+
def define_method_written_ivars(node, acc)
|
|
1109
|
+
return unless node.is_a?(Prism::Node)
|
|
1110
|
+
return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
1111
|
+
|
|
1112
|
+
case node
|
|
1113
|
+
when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode,
|
|
1114
|
+
Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableOperatorWriteNode,
|
|
1115
|
+
Prism::InstanceVariableTargetNode
|
|
1116
|
+
acc << node.name
|
|
1117
|
+
end
|
|
1118
|
+
node.rigor_each_child { |child| define_method_written_ivars(child, acc) }
|
|
1119
|
+
end
|
|
1120
|
+
|
|
1063
1121
|
# ADR-38 block-form gate: true when a loaded plugin declares `method_name` a block-form initializer for
|
|
1064
1122
|
# `class_name` (or an ancestor). Mirrors `additional_initializer?` but queries `covers_block_method?` instead of
|
|
1065
1123
|
# `covers_method?`.
|
|
@@ -1151,15 +1209,21 @@ module Rigor
|
|
|
1151
1209
|
# every `@x = …` write target as a class-body init. Consumed by `contribute_read_before_write_nil!` to exempt
|
|
1152
1210
|
# ivars the author already knows might be nil (the `@x = nil` at class-body level is the canonical nullability
|
|
1153
1211
|
# acknowledgement; the instance @x is technically a separate store, but the pragmatic intent is unambiguous).
|
|
1212
|
+
#
|
|
1213
|
+
# A literal `define_method` block is a method body, not class-body code (#1768): its writes reach the census as
|
|
1214
|
+
# a `def` body's do, so they must not exempt the ivar from the read-before-write `nil` either. Only its receiver
|
|
1215
|
+
# and arguments are walked.
|
|
1154
1216
|
def collect_class_body_ivar_writes(node, class_name, init_writes)
|
|
1155
1217
|
return unless node.is_a?(Prism::Node)
|
|
1156
1218
|
return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
1157
1219
|
|
|
1220
|
+
return collect_define_method_argument_writes(node, class_name, init_writes) if define_method_block_call?(node)
|
|
1221
|
+
|
|
1158
1222
|
if node.is_a?(Prism::InstanceVariableWriteNode) ||
|
|
1159
1223
|
node.is_a?(Prism::InstanceVariableOrWriteNode) ||
|
|
1160
1224
|
node.is_a?(Prism::InstanceVariableAndWriteNode) ||
|
|
1161
1225
|
node.is_a?(Prism::InstanceVariableOperatorWriteNode)
|
|
1162
|
-
(init_writes[class_name] ||= Set.new) << node.name
|
|
1226
|
+
((init_writes[CLASS_BODY_WRITES] ||= {})[class_name] ||= Set.new) << node.name
|
|
1163
1227
|
end
|
|
1164
1228
|
|
|
1165
1229
|
node.rigor_each_child do |child|
|
|
@@ -1167,6 +1231,18 @@ module Rigor
|
|
|
1167
1231
|
end
|
|
1168
1232
|
end
|
|
1169
1233
|
|
|
1234
|
+
# The arguments of a `define_method` block call, walked by {#collect_class_body_ivar_writes} in place of the
|
|
1235
|
+
# call's children so the block body stays out of the class-body init writes.
|
|
1236
|
+
def collect_define_method_argument_writes(node, class_name, init_writes)
|
|
1237
|
+
node.arguments&.arguments&.each { |arg| collect_class_body_ivar_writes(arg, class_name, init_writes) }
|
|
1238
|
+
end
|
|
1239
|
+
|
|
1240
|
+
# A `define_method` call with a literal block on the implicit or `self` receiver: the block is an instance
|
|
1241
|
+
# method body of the class the walk is in (#1768).
|
|
1242
|
+
def define_method_block_call?(node)
|
|
1243
|
+
DefineMethodBlockSelf.define_method_on_lexical_self?(node) && node.block.is_a?(Prism::BlockNode)
|
|
1244
|
+
end
|
|
1245
|
+
|
|
1170
1246
|
def detect_read_before_write(node, seen_writes, read_first)
|
|
1171
1247
|
return unless node.is_a?(Prism::Node)
|
|
1172
1248
|
return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
@@ -2006,6 +2082,13 @@ module Rigor
|
|
|
2006
2082
|
OPERATOR_WRITE_CONTRIBUTIONS = :operator_write_contributions
|
|
2007
2083
|
private_constant :AND_WRITE_CONTRIBUTIONS, :OPERATOR_WRITE_CONTRIBUTIONS
|
|
2008
2084
|
|
|
2085
|
+
# The same Symbol-key convention for two side tables the walk fills and {#build_class_ivar_index} removes
|
|
2086
|
+
# before anything reads class names: the ivars each class's `define_method` blocks write (in the
|
|
2087
|
+
# accumulator), and the class-body writes held out of `init_writes` until {#exempt_class_body_writes!}.
|
|
2088
|
+
DEFINE_METHOD_WRITES = :define_method_writes
|
|
2089
|
+
CLASS_BODY_WRITES = :class_body_writes
|
|
2090
|
+
private_constant :DEFINE_METHOD_WRITES, :CLASS_BODY_WRITES
|
|
2091
|
+
|
|
2009
2092
|
# Unions `type` into the class-ivar accumulator for `(class_name, ivar_name)`. Shared by the single-write and
|
|
2010
2093
|
# multi-write (parallel-assignment) collectors.
|
|
2011
2094
|
def accumulate_ivar_type(accumulator, class_name, ivar_name, type)
|
|
@@ -3990,6 +4073,7 @@ module Rigor
|
|
|
3990
4073
|
if methods_acc.mentions_refine? && methods_acc.refine_in?(location.start_offset, location.end_offset)
|
|
3991
4074
|
node.rigor_each_child { |child| census_scan(child, qualified_prefix, methods_acc) }
|
|
3992
4075
|
end
|
|
4076
|
+
record_foreign_singleton_def(node, qualified_prefix, owner_prefix, methods_acc.at(node))
|
|
3993
4077
|
unless def_owner_prefix&.empty? || defs_singleton == :unnameable ||
|
|
3994
4078
|
(singleton_cref && owner_prefix.empty?)
|
|
3995
4079
|
singleton_def = in_singleton_class || defs_singleton
|
|
@@ -4729,6 +4813,50 @@ module Rigor
|
|
|
4729
4813
|
acc[:deferred_ranges][path] = build_deferred_ranges(root)
|
|
4730
4814
|
record_class_sources(acc[:class_sources], path, root, superclasses, includes, file_def_nodes,
|
|
4731
4815
|
acc[:compact_headers])
|
|
4816
|
+
record_singleton_class_sources(acc[:singleton_class_sources], path, root)
|
|
4817
|
+
end
|
|
4818
|
+
|
|
4819
|
+
# `{name => Set[path]}` for a bundle's name list (a bundle written before the list existed has none).
|
|
4820
|
+
def bundle_path_sets(names, path) = (names || []).to_h { |name| [name, Set[path]] }
|
|
4821
|
+
|
|
4822
|
+
# Issue #1752 — the constants a `class << Const` body opens in this file, and those whose singleton side a call
|
|
4823
|
+
# there may rewrite ({#singleton_side_call?}). Such a body records facts about
|
|
4824
|
+
# `Const`'s singleton class (a `remove_method`, a `def`) without declaring `Const`, so it stays out of
|
|
4825
|
+
# `class_sources` (whose readers take a name there as one the project declares) and joins only the per-file
|
|
4826
|
+
# class-declaration sets the incremental `class:` edges diff (`Runner#class_declarations`).
|
|
4827
|
+
def record_singleton_class_sources(sources, path, root)
|
|
4828
|
+
collect_singleton_class_targets(root, []) { |name| (sources[name] ||= Set.new) << path }
|
|
4829
|
+
end
|
|
4830
|
+
|
|
4831
|
+
def collect_singleton_class_targets(node, lexical_prefix, &)
|
|
4832
|
+
case node
|
|
4833
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
4834
|
+
prefix = Source::ConstantPath.declaration_prefix(lexical_prefix, node.constant_path)
|
|
4835
|
+
node.body && prefix && collect_singleton_class_targets(node.body, prefix, &)
|
|
4836
|
+
return
|
|
4837
|
+
when Prism::SingletonClassNode
|
|
4838
|
+
rendered = singleton_receiver_constant_name(node.expression)
|
|
4839
|
+
yield eval_constant_receiver_prefix(node, node.expression, rendered, lexical_prefix).join("::") if rendered
|
|
4840
|
+
when Prism::CallNode
|
|
4841
|
+
yield_singleton_side_constant(node, node.receiver, lexical_prefix, &) if singleton_side_call?(node)
|
|
4842
|
+
end
|
|
4843
|
+
node.rigor_each_child { |child| collect_singleton_class_targets(child, lexical_prefix, &) }
|
|
4844
|
+
end
|
|
4845
|
+
|
|
4846
|
+
# A call on a constant that may rewrite its singleton side (`Const.singleton_class`,
|
|
4847
|
+
# `Const.define_singleton_method`, `Const.extend(M)`, `Const.instance_eval { … }`) records facts about `Const`'s
|
|
4848
|
+
# singleton methods the refinement walk reads from a file that need not declare `Const`, so the file joins the
|
|
4849
|
+
# constant's set too. Written inside `Const`'s own body, the name is in the file's set already; a `def Const.x`
|
|
4850
|
+
# files its own `method:` key.
|
|
4851
|
+
def singleton_side_call?(node)
|
|
4852
|
+
node.name == :singleton_class || surface_rewriting_call?(node)
|
|
4853
|
+
end
|
|
4854
|
+
|
|
4855
|
+
def yield_singleton_side_constant(node, receiver, lexical_prefix)
|
|
4856
|
+
return unless receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
|
|
4857
|
+
|
|
4858
|
+
rendered = singleton_receiver_constant_name(receiver)
|
|
4859
|
+
yield eval_constant_receiver_prefix(node, receiver, rendered, lexical_prefix).join("::") if rendered
|
|
4732
4860
|
end
|
|
4733
4861
|
|
|
4734
4862
|
def build_deferred_ranges(root)
|
|
@@ -6954,7 +7082,9 @@ module Rigor
|
|
|
6954
7082
|
def extends_block_handled?(node, qualified_prefix, current_class, accumulator, in_singleton, singleton_self,
|
|
6955
7083
|
singleton_cref)
|
|
6956
7084
|
case extends_block_plan(node, accumulator)
|
|
6957
|
-
when :skip
|
|
7085
|
+
when :skip
|
|
7086
|
+
taint_skipped_singleton_rewrites(node.block, qualified_prefix, current_class, accumulator)
|
|
7087
|
+
true
|
|
6958
7088
|
when :rebound
|
|
6959
7089
|
walk_extends_block_call(node, qualified_prefix, current_class, accumulator,
|
|
6960
7090
|
in_singleton, singleton_self, singleton_cref)
|
|
@@ -6963,6 +7093,31 @@ module Rigor
|
|
|
6963
7093
|
end
|
|
6964
7094
|
end
|
|
6965
7095
|
|
|
7096
|
+
# Issue #1906 — a block the extends walk skips may still run, and a `singleton_class` call in it, or in a `def`
|
|
7097
|
+
# it holds (`[1].each { D.singleton_class.alias_method :own, :other }` in a method body,
|
|
7098
|
+
# `to_prepare do def helper = D.singleton_class.alias_method(...) end`), reaches no mark the methods walk records
|
|
7099
|
+
# there. Every such call taints the side it may rewrite, as {#singleton_class_rewrite_tainted?} does for one the
|
|
7100
|
+
# walk visits; a constant receiver taints every name it can denote from here.
|
|
7101
|
+
def taint_skipped_singleton_rewrites(node, qualified_prefix, current_class, accumulator)
|
|
7102
|
+
return unless node.is_a?(Prism::Node)
|
|
7103
|
+
|
|
7104
|
+
if node.is_a?(Prism::CallNode) && singleton_class_of_receiver?(node.receiver) && surface_rewriting_call?(node)
|
|
7105
|
+
skipped_rewrite_owners(node.receiver.receiver, qualified_prefix, current_class).each do |owner|
|
|
7106
|
+
accumulator.taint(owner, :extend)
|
|
7107
|
+
end
|
|
7108
|
+
end
|
|
7109
|
+
node.rigor_each_child do |child|
|
|
7110
|
+
taint_skipped_singleton_rewrites(child, qualified_prefix, current_class, accumulator)
|
|
7111
|
+
end
|
|
7112
|
+
end
|
|
7113
|
+
|
|
7114
|
+
def skipped_rewrite_owners(object, qualified_prefix, current_class)
|
|
7115
|
+
return [current_class].compact if object.nil? || object.is_a?(Prism::SelfNode)
|
|
7116
|
+
return [] unless object.is_a?(Prism::ConstantReadNode) || object.is_a?(Prism::ConstantPathNode)
|
|
7117
|
+
|
|
7118
|
+
constant_receiver_candidates(object, qualified_prefix)
|
|
7119
|
+
end
|
|
7120
|
+
|
|
6966
7121
|
ARRAY_ITERATORS = %i[each each_with_index map].freeze
|
|
6967
7122
|
HASH_ITERATORS = %i[each each_pair each_key each_value map].freeze
|
|
6968
7123
|
private_constant :ARRAY_ITERATORS, :HASH_ITERATORS
|
|
@@ -7160,6 +7315,7 @@ module Rigor
|
|
|
7160
7315
|
return record_module_function(node, current_class, accumulator, in_singleton) if node.name == :module_function
|
|
7161
7316
|
|
|
7162
7317
|
taint_opaque_eval(node, current_class, accumulator)
|
|
7318
|
+
return if singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator)
|
|
7163
7319
|
|
|
7164
7320
|
kind, _arguments, via_send = mixin_call_view(node)
|
|
7165
7321
|
return if kind.nil?
|
|
@@ -7172,6 +7328,76 @@ module Rigor
|
|
|
7172
7328
|
effects.each { |owner, side| accumulator.taint(owner, side) }
|
|
7173
7329
|
end
|
|
7174
7330
|
|
|
7331
|
+
# Issue #1752 — a call that rewrites a singleton class reached through `singleton_class` (`singleton_class.
|
|
7332
|
+
# class_eval { def x }`, `singleton_class.send(:define_method, :x)`, `Const.singleton_class.prepend(M)`,
|
|
7333
|
+
# `Const.singleton_class.send(:include, M)`) puts methods or modules on that singleton class that no edge
|
|
7334
|
+
# records, so its singleton side is unpositioned: the owner is the enclosing class for a bare or `self.`
|
|
7335
|
+
# receiver, else the constant. True when it tainted.
|
|
7336
|
+
#
|
|
7337
|
+
# Issue #1906 — a naming call with literal names ({#singleton_class_named_call?}) marks only its names instead,
|
|
7338
|
+
# but only where it is a direct statement (a top-level statement, or one of a direct declaration body or of a
|
|
7339
|
+
# block that runs there once): there the methods walk records those marks under the same owner. In a method
|
|
7340
|
+
# body, a hook or a conditional it may run on a receiver or at a time the marks cannot place, so it still taints.
|
|
7341
|
+
def singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator)
|
|
7342
|
+
receiver = node.receiver
|
|
7343
|
+
return false unless singleton_class_of_receiver?(receiver) && surface_rewriting_call?(node)
|
|
7344
|
+
return false if singleton_class_named_call?(node) && accumulator.direct?(node)
|
|
7345
|
+
|
|
7346
|
+
owner = singleton_class_owner(node, receiver.receiver, qualified_prefix, current_class)
|
|
7347
|
+
return false if owner.nil?
|
|
7348
|
+
|
|
7349
|
+
accumulator.taint(owner, :extend)
|
|
7350
|
+
true
|
|
7351
|
+
end
|
|
7352
|
+
|
|
7353
|
+
# A bare, `self.` or constant receiver's `singleton_class` call.
|
|
7354
|
+
def singleton_class_of_receiver?(receiver)
|
|
7355
|
+
receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class && receiver.arguments.nil?
|
|
7356
|
+
end
|
|
7357
|
+
|
|
7358
|
+
# An `attr_*`, `alias_method`, `define_method` or `remove_method` on a singleton class
|
|
7359
|
+
# (`singleton_class.attr_accessor :conf`, `singleton_class.alias_method :o2, :own`) rewrites only the names it
|
|
7360
|
+
# spells, so it makes those names' singleton envelopes opaque and contested ({#record_singleton_class_names}) and
|
|
7361
|
+
# leaves the rest of the singleton side readable. One whose rewritten name is not a literal Symbol or String
|
|
7362
|
+
# (`attr_accessor(*KEYS)`, `alias_method name, :own`, `define_method(sym)`), and any other surface call there
|
|
7363
|
+
# (`class_eval`, `send`, `prepend`), still taints the side: the own-definer read would otherwise answer the body
|
|
7364
|
+
# Ruby replaced (`definer_resolution_witness_spec.rb`).
|
|
7365
|
+
def singleton_class_named_call?(node)
|
|
7366
|
+
!singleton_class_rewritten_names(node).nil?
|
|
7367
|
+
end
|
|
7368
|
+
|
|
7369
|
+
# Issue #1906 — the names a singleton-class call rewrites, or nil where it is not one of the naming forms or
|
|
7370
|
+
# does not spell every rewritten name as a literal. `alias_method` rewrites its new name (the aliased one keeps
|
|
7371
|
+
# its body), `define_method` its first argument, `remove_method` and `attr_*` every argument.
|
|
7372
|
+
def singleton_class_rewritten_names(node)
|
|
7373
|
+
arguments = node.arguments&.arguments || []
|
|
7374
|
+
rewritten = case node.name
|
|
7375
|
+
when :alias_method, :define_method then arguments.first(1)
|
|
7376
|
+
when :remove_method then arguments
|
|
7377
|
+
else ATTR_MACROS.include?(node.name) && arguments
|
|
7378
|
+
end
|
|
7379
|
+
return nil if !rewritten || arguments.empty? || !arguments.all? { |argument| literal_argument?(node, argument) }
|
|
7380
|
+
|
|
7381
|
+
rewritten.map { |argument| literal_method_name(argument) }
|
|
7382
|
+
end
|
|
7383
|
+
|
|
7384
|
+
# `define_method(:x, body)` takes a body that is not a name; every other argument must be a literal name.
|
|
7385
|
+
def literal_argument?(node, argument)
|
|
7386
|
+
return true if node.name == :define_method && !argument.equal?(node.arguments.arguments.first)
|
|
7387
|
+
|
|
7388
|
+
!literal_method_name(argument).nil?
|
|
7389
|
+
end
|
|
7390
|
+
|
|
7391
|
+
def singleton_class_owner(node, object, qualified_prefix, current_class)
|
|
7392
|
+
return current_class if object.nil? || object.is_a?(Prism::SelfNode)
|
|
7393
|
+
|
|
7394
|
+
rendered = Source::ConstantPath.qualified_name_or_nil(object)
|
|
7395
|
+
return nil if rendered.nil?
|
|
7396
|
+
return rendered if Source::ConstantPath.rooted?(object) || qualified_prefix.empty?
|
|
7397
|
+
|
|
7398
|
+
eval_constant_receiver_prefix(node, object, rendered, qualified_prefix).join("::")
|
|
7399
|
+
end
|
|
7400
|
+
|
|
7175
7401
|
# The bare `module_function` toggle: the module extends itself. A direct statement only.
|
|
7176
7402
|
def record_module_function(node, current_class, accumulator, in_singleton)
|
|
7177
7403
|
return unless current_class && node.receiver.nil? && !in_singleton && ModuleFunctionState.extends_self?(node)
|
|
@@ -7353,19 +7579,15 @@ module Rigor
|
|
|
7353
7579
|
|
|
7354
7580
|
# v0.1.2 — per-class method-visibility table for the `def.method-visibility-mismatch` CheckRule.
|
|
7355
7581
|
#
|
|
7356
|
-
# Tracks
|
|
7582
|
+
# Tracks three visibility-changing forms ({#apply_visibility_call}):
|
|
7357
7583
|
#
|
|
7358
|
-
# - **Modifier blocks**: a bare `private` / `protected` /
|
|
7359
|
-
# `
|
|
7360
|
-
#
|
|
7361
|
-
#
|
|
7362
|
-
#
|
|
7363
|
-
#
|
|
7364
|
-
#
|
|
7365
|
-
# args are recognised; `private def foo; end` (the
|
|
7366
|
-
# wrap-around form) is not yet — it would need
|
|
7367
|
-
# tracking the def-call's return-value visibility,
|
|
7368
|
-
# which is a separate slice.
|
|
7584
|
+
# - **Modifier blocks**: a bare `private` / `protected` / `public` call inside a class body (also written
|
|
7585
|
+
# `self.private`, `send(:private)` or with a block) switches the "current default" visibility for every
|
|
7586
|
+
# subsequent `def` and `attr_*` member until another modifier flips it again.
|
|
7587
|
+
# - **Named-argument form**: `private :foo, :bar` or `private %i[foo bar]` (or the same with `protected` /
|
|
7588
|
+
# `public`) marks specific names already recorded under the class.
|
|
7589
|
+
# - **Wrap-around form** (#1911, #1819, #1633): `private def foo` and `private attr_reader :foo` record the
|
|
7590
|
+
# members with the call's visibility and leave the default alone.
|
|
7369
7591
|
#
|
|
7370
7592
|
# Top-level (no surrounding class) defs do not contribute — Ruby's top-level visibility nuances (private at
|
|
7371
7593
|
# top-level marks the method on `Object`) are out of scope for v0.1.2.
|
|
@@ -7422,6 +7644,26 @@ module Rigor
|
|
|
7422
7644
|
end
|
|
7423
7645
|
end
|
|
7424
7646
|
|
|
7647
|
+
# Contests the `[class_name, name]` slot, recorded or not: a visibility call changed it in a way the walk
|
|
7648
|
+
# cannot read (`private def self.x` reaching an instance `x`).
|
|
7649
|
+
def contest_slot(class_name, name)
|
|
7650
|
+
(@contested ||= Set.new) << [class_name, name].freeze
|
|
7651
|
+
end
|
|
7652
|
+
|
|
7653
|
+
# Contests every slot of `class_name` recorded so far (`private(*names)` may name any of them).
|
|
7654
|
+
def contest_class(class_name)
|
|
7655
|
+
self[class_name]&.each_key { |name| contest_slot(class_name, name) }
|
|
7656
|
+
end
|
|
7657
|
+
|
|
7658
|
+
# Runs the block with the running default uncertain, so a `def` it records is contested.
|
|
7659
|
+
def uncertainly
|
|
7660
|
+
saved = @uncertain
|
|
7661
|
+
@uncertain = true
|
|
7662
|
+
yield
|
|
7663
|
+
ensure
|
|
7664
|
+
@uncertain = saved
|
|
7665
|
+
end
|
|
7666
|
+
|
|
7425
7667
|
# Runs the block as a fresh body (a class or module, `class << self`, a meta-new or eval block), whose
|
|
7426
7668
|
# default starts certain and whose toggles do not reach the enclosing body.
|
|
7427
7669
|
def body
|
|
@@ -7443,10 +7685,37 @@ module Rigor
|
|
|
7443
7685
|
end
|
|
7444
7686
|
end
|
|
7445
7687
|
|
|
7446
|
-
# A bare toggle ({VISIBILITY_TOGGLES}):
|
|
7688
|
+
# A bare toggle ({VISIBILITY_TOGGLES}): a visibility call ({#visibility_call_parts}) with no arguments.
|
|
7447
7689
|
def bare_visibility_toggle?(node)
|
|
7448
|
-
|
|
7449
|
-
|
|
7690
|
+
parts = visibility_call_parts(node)
|
|
7691
|
+
!parts.nil? && parts.last.empty?
|
|
7692
|
+
end
|
|
7693
|
+
|
|
7694
|
+
# Whether a visibility call with these arguments may set the running default: none at all, or a splat that
|
|
7695
|
+
# may expand to none (`private(*[])` is a bare `private`).
|
|
7696
|
+
def visibility_toggle_arguments?(arguments)
|
|
7697
|
+
arguments.empty? || arguments.any?(Prism::SplatNode)
|
|
7698
|
+
end
|
|
7699
|
+
|
|
7700
|
+
VISIBILITY_SEND_CALLS = %i[send __send__].freeze
|
|
7701
|
+
EMPTY_VISIBILITY_ARGUMENTS = [].freeze
|
|
7702
|
+
private_constant :VISIBILITY_SEND_CALLS, :EMPTY_VISIBILITY_ARGUMENTS
|
|
7703
|
+
|
|
7704
|
+
# Issue #1633 — `[name, arguments]` for a call Ruby runs as one of {VISIBILITY_TOGGLES} on the body's self:
|
|
7705
|
+
# written receiverless or on `self` (`self.private`), or sent with a literal name (`send(:private, :x)`,
|
|
7706
|
+
# `__send__(:private)`). A block is ignored, as `Module#private` ignores it (`private {}` is a bare
|
|
7707
|
+
# `private`). nil for any other call.
|
|
7708
|
+
def visibility_call_parts(node)
|
|
7709
|
+
return nil unless node.is_a?(Prism::CallNode)
|
|
7710
|
+
return nil unless node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
7711
|
+
|
|
7712
|
+
name = node.name
|
|
7713
|
+
arguments = node.arguments&.arguments || EMPTY_VISIBILITY_ARGUMENTS
|
|
7714
|
+
if VISIBILITY_SEND_CALLS.include?(name)
|
|
7715
|
+
name = Source::Literals.symbol_or_string(arguments.first)
|
|
7716
|
+
arguments = arguments.drop(1)
|
|
7717
|
+
end
|
|
7718
|
+
VISIBILITY_TOGGLES.include?(name) ? [name, arguments] : nil
|
|
7450
7719
|
end
|
|
7451
7720
|
|
|
7452
7721
|
# rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize
|
|
@@ -7510,16 +7779,31 @@ module Rigor
|
|
|
7510
7779
|
defs_singleton: defs_singleton)
|
|
7511
7780
|
return current_visibility
|
|
7512
7781
|
end
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7782
|
+
if AnonymousMetaClass.block_form_receiver(node)
|
|
7783
|
+
walk_anonymous_meta_visibilities(node, qualified_prefix, in_singleton_class, current_visibility,
|
|
7784
|
+
accumulator, def_owner_prefix, singleton_cref: singleton_cref,
|
|
7785
|
+
defs_singleton: defs_singleton)
|
|
7786
|
+
return current_visibility
|
|
7787
|
+
end
|
|
7788
|
+
if (parts = visibility_call_parts(node))
|
|
7789
|
+
accumulator.toggles += 1 if visibility_toggle_arguments?(parts.last)
|
|
7790
|
+
# The visibility table is instance-side only — `private :x` inside `class <<`
|
|
7791
|
+
# (or a receiver-eval body that re-evaluates a singleton self) marks the
|
|
7792
|
+
# SINGLETON method, which this table cannot express. The `owner_prefix` check is a
|
|
7793
|
+
# guard: an ownerless body (the top level, an anonymous factory block) records nothing,
|
|
7794
|
+
# and its calls walk as any other call.
|
|
7795
|
+
if !in_singleton_class && !owner_prefix.empty? && VISIBILITY_MODIFIERS.include?(parts.first)
|
|
7796
|
+
return apply_visibility_call(node, *parts, owner_prefix, current_visibility, accumulator,
|
|
7797
|
+
defs_singleton: defs_singleton) do |argument|
|
|
7798
|
+
walk_method_visibilities(argument, qualified_prefix, in_singleton_class, current_visibility,
|
|
7799
|
+
accumulator, def_owner_prefix, singleton_cref: singleton_cref,
|
|
7800
|
+
defs_singleton: defs_singleton)
|
|
7801
|
+
end
|
|
7802
|
+
end
|
|
7803
|
+
end
|
|
7804
|
+
unless in_singleton_class || defs_singleton
|
|
7805
|
+
record_attr_visibility(node, owner_prefix, current_visibility, accumulator, toggled: true)
|
|
7806
|
+
end
|
|
7523
7807
|
end
|
|
7524
7808
|
|
|
7525
7809
|
# Statement-position StatementsNode preserves left-to-right visibility flow; everything else recurses with the
|
|
@@ -7573,6 +7857,23 @@ module Rigor
|
|
|
7573
7857
|
end
|
|
7574
7858
|
end
|
|
7575
7859
|
|
|
7860
|
+
# A `Class.new` / `Module.new` / `Struct.new` / `Data.define` block no constant write names
|
|
7861
|
+
# ({#meta_new_block_split} takes the named ones): `klass = Class.new do … end`, `@k = Struct.new(:a) do … end`,
|
|
7862
|
+
# `register(Class.new do … end)` or a bare `Data.define(:a) do … end`. Its body is the anonymous class's, so
|
|
7863
|
+
# it walks as a fresh body with no owner and records nothing for the enclosing class; the receiver and the
|
|
7864
|
+
# arguments are walked where they stand.
|
|
7865
|
+
def walk_anonymous_meta_visibilities(node, qualified_prefix, in_singleton_class, current_visibility,
|
|
7866
|
+
accumulator, def_owner_prefix, singleton_cref:, defs_singleton:) # rubocop:disable Metrics/ParameterLists
|
|
7867
|
+
[node.receiver, *node.arguments&.arguments].each do |part|
|
|
7868
|
+
walk_method_visibilities(part, qualified_prefix, in_singleton_class, current_visibility, accumulator,
|
|
7869
|
+
def_owner_prefix, singleton_cref: singleton_cref, defs_singleton: defs_singleton)
|
|
7870
|
+
end
|
|
7871
|
+
accumulator.body do
|
|
7872
|
+
walk_method_visibilities(node.block.body, qualified_prefix, false, :public, accumulator,
|
|
7873
|
+
EMPTY_PREFIX, singleton_cref: singleton_cref)
|
|
7874
|
+
end
|
|
7875
|
+
end
|
|
7876
|
+
|
|
7576
7877
|
# The eval-block arm of {#walk_method_visibilities}: the block is a fresh class body under
|
|
7577
7878
|
# the receiver's def-owner — visibility starts at `:public`, and nothing inside it leaks
|
|
7578
7879
|
# back to this body's modifier state. An unnameable receiver walks the body ownerless;
|
|
@@ -7610,47 +7911,80 @@ module Rigor
|
|
|
7610
7911
|
end
|
|
7611
7912
|
end
|
|
7612
7913
|
|
|
7613
|
-
def
|
|
7914
|
+
# `toggled` when the def reads the running default (a statement), false for one a visibility call wraps.
|
|
7915
|
+
def record_def_visibility(def_node, qualified_prefix, in_singleton_class, current_visibility, accumulator,
|
|
7916
|
+
toggled: true)
|
|
7614
7917
|
return if def_node.receiver.is_a?(Prism::SelfNode) || in_singleton_class
|
|
7615
7918
|
return if qualified_prefix.empty?
|
|
7616
7919
|
|
|
7617
7920
|
class_name = qualified_prefix.join("::")
|
|
7618
7921
|
accumulator[class_name] ||= {}
|
|
7619
7922
|
accumulator[class_name][def_node.name] = current_visibility
|
|
7620
|
-
accumulator.wrote(class_name, def_node.name, def_node, toggled:
|
|
7621
|
-
end
|
|
7923
|
+
accumulator.wrote(class_name, def_node.name, def_node, toggled: toggled)
|
|
7924
|
+
end
|
|
7925
|
+
|
|
7926
|
+
# Applies one `private` / `protected` / `public` call ({#visibility_call_parts}) written on the body's self
|
|
7927
|
+
# and returns the running default after it:
|
|
7928
|
+
#
|
|
7929
|
+
# - no arguments — the call's visibility becomes the default for subsequent defs.
|
|
7930
|
+
# - `private :foo, :bar`, `private %i[foo bar]` — back-patch the named methods in the accumulator; the
|
|
7931
|
+
# default does NOT change for this form.
|
|
7932
|
+
# - Issues #1911, #1819, #1633 — `private def foo` (endless or not) and `private attr_reader :foo` define
|
|
7933
|
+
# the members with the call's visibility, whatever the running default is (`private; public def foo` is
|
|
7934
|
+
# public); `private def self.foo` changes the instance method `foo`, if one exists, so its slot is
|
|
7935
|
+
# contested.
|
|
7936
|
+
# - a splat (`private(*names)`) may name any member or none (a bare toggle), so the class's slots this file
|
|
7937
|
+
# wrote so far are contested, and the statement loop reads the default after it as uncertain
|
|
7938
|
+
# ({#visibility_toggle_arguments?}).
|
|
7939
|
+
# - any other argument (`private memoize def foo`, `private delegate(...)`) is walked with the default
|
|
7940
|
+
# uncertain, so a `def` under it is contested: the call's visibility reaches it through a return value
|
|
7941
|
+
# the walk does not read.
|
|
7942
|
+
#
|
|
7943
|
+
# `walk` walks an argument the call does not consume itself.
|
|
7944
|
+
def apply_visibility_call(call_node, name, args, qualified_prefix, current_visibility, accumulator,
|
|
7945
|
+
defs_singleton: false, &walk)
|
|
7946
|
+
return name if args.empty?
|
|
7622
7947
|
|
|
7623
|
-
|
|
7624
|
-
|
|
7625
|
-
|
|
7626
|
-
|
|
7627
|
-
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
# change for this form.
|
|
7632
|
-
def apply_visibility_call(call_node, qualified_prefix, current_visibility, accumulator)
|
|
7633
|
-
return current_visibility unless call_node.receiver.nil?
|
|
7634
|
-
return current_visibility unless VISIBILITY_MODIFIERS.include?(call_node.name)
|
|
7635
|
-
return current_visibility if qualified_prefix.empty?
|
|
7948
|
+
class_name = qualified_prefix.join("::")
|
|
7949
|
+
accumulator.contest_class(class_name) if args.any?(Prism::SplatNode)
|
|
7950
|
+
args.each do |arg|
|
|
7951
|
+
apply_visibility_argument(arg, name, call_node, qualified_prefix, accumulator,
|
|
7952
|
+
defs_singleton: defs_singleton, &walk)
|
|
7953
|
+
end
|
|
7954
|
+
current_visibility
|
|
7955
|
+
end
|
|
7636
7956
|
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7957
|
+
def apply_visibility_argument(arg, name, call_node, qualified_prefix, accumulator, defs_singleton:, &walk)
|
|
7958
|
+
case arg
|
|
7959
|
+
when Prism::DefNode
|
|
7960
|
+
if arg.receiver.nil?
|
|
7961
|
+
record_def_visibility(arg, qualified_prefix, defs_singleton, name, accumulator, toggled: false)
|
|
7962
|
+
else
|
|
7963
|
+
accumulator.contest_slot(qualified_prefix.join("::"), arg.name)
|
|
7964
|
+
end
|
|
7965
|
+
when Prism::SymbolNode, Prism::StringNode, Prism::ArrayNode
|
|
7966
|
+
apply_named_visibility(arg, name, qualified_prefix, call_node, accumulator, &walk)
|
|
7640
7967
|
else
|
|
7641
|
-
|
|
7642
|
-
|
|
7968
|
+
# An `instance_eval` body's wrapped attr is walked like any other argument and records nothing.
|
|
7969
|
+
if !defs_singleton && attr_visibility_call?(arg)
|
|
7970
|
+
record_attr_visibility(arg, qualified_prefix, name, accumulator, toggled: false)
|
|
7971
|
+
else
|
|
7972
|
+
accumulator.uncertainly { walk.call(arg) }
|
|
7973
|
+
end
|
|
7643
7974
|
end
|
|
7644
7975
|
end
|
|
7645
7976
|
|
|
7646
|
-
|
|
7977
|
+
# `private :foo` / `private "foo"` / `private %i[foo bar]`: each literal name the call's visibility. An array
|
|
7978
|
+
# element that is not a literal name is walked like any other argument.
|
|
7979
|
+
def apply_named_visibility(arg, visibility, qualified_prefix, call_node, accumulator, &walk)
|
|
7980
|
+
elements = arg.is_a?(Prism::ArrayNode) ? arg.elements : [arg]
|
|
7647
7981
|
class_name = qualified_prefix.join("::")
|
|
7648
|
-
|
|
7649
|
-
name = visibility_target_name(
|
|
7650
|
-
next if name.nil?
|
|
7982
|
+
elements.each do |element|
|
|
7983
|
+
name = visibility_target_name(element)
|
|
7984
|
+
next accumulator.uncertainly { walk.call(element) } if name.nil?
|
|
7651
7985
|
|
|
7652
7986
|
accumulator[class_name] ||= {}
|
|
7653
|
-
accumulator[class_name][name] =
|
|
7987
|
+
accumulator[class_name][name] = visibility
|
|
7654
7988
|
accumulator.wrote(class_name, name, call_node, toggled: false)
|
|
7655
7989
|
end
|
|
7656
7990
|
end
|
|
@@ -7659,6 +7993,32 @@ module Rigor
|
|
|
7659
7993
|
Source::Literals.symbol_or_string(arg)
|
|
7660
7994
|
end
|
|
7661
7995
|
|
|
7996
|
+
# Issue #1911 — an `attr_reader` / `attr_writer` / `attr_accessor` call written on the body's self records
|
|
7997
|
+
# each member it defines (`x`, `x=`) with `visibility`: the running default for a statement (`toggled`), the
|
|
7998
|
+
# wrapping call's for `private attr_accessor :x`, which Ruby applies to the `[:x, :x=]` the macro returns.
|
|
7999
|
+
# A non-literal name (`attr_reader(*FIELDS)`) records nothing; a call that is no such macro records nothing.
|
|
8000
|
+
def record_attr_visibility(call_node, qualified_prefix, visibility, accumulator, toggled:)
|
|
8001
|
+
return unless attr_visibility_call?(call_node)
|
|
8002
|
+
return if qualified_prefix.empty?
|
|
8003
|
+
|
|
8004
|
+
class_name = qualified_prefix.join("::")
|
|
8005
|
+
each_attr_macro_name(call_node) do |base|
|
|
8006
|
+
members = []
|
|
8007
|
+
members << base unless call_node.name == :attr_writer
|
|
8008
|
+
members << :"#{base}=" unless call_node.name == :attr_reader
|
|
8009
|
+
members.each do |member|
|
|
8010
|
+
accumulator[class_name] ||= {}
|
|
8011
|
+
accumulator[class_name][member] = visibility
|
|
8012
|
+
accumulator.wrote(class_name, member, call_node, toggled: toggled)
|
|
8013
|
+
end
|
|
8014
|
+
end
|
|
8015
|
+
end
|
|
8016
|
+
|
|
8017
|
+
# A receiverless `attr_reader` / `attr_writer` / `attr_accessor` call ({#record_attr_visibility}).
|
|
8018
|
+
def attr_visibility_call?(node)
|
|
8019
|
+
node.is_a?(Prism::CallNode) && node.receiver.nil? && ATTR_MACROS.include?(node.name)
|
|
8020
|
+
end
|
|
8021
|
+
|
|
7662
8022
|
# Registers the alias name in the `discovered_methods` table so `undefined-method` diagnostics are not emitted for
|
|
7663
8023
|
# calls to the aliased name. The kind mirrors the surrounding class context (instance inside a regular class body,
|
|
7664
8024
|
# singleton inside `class << self`).
|
|
@@ -7758,6 +8118,41 @@ module Rigor
|
|
|
7758
8118
|
constant_receiver_candidates(receiver, qualified_prefix).each do |name|
|
|
7759
8119
|
record_surface_mark(tables, name, Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK)
|
|
7760
8120
|
end
|
|
8121
|
+
elsif singleton_class_of_receiver?(receiver) && singleton_class_named_call?(node)
|
|
8122
|
+
record_singleton_class_names(node, receiver.receiver, qualified_prefix, tables)
|
|
8123
|
+
end
|
|
8124
|
+
end
|
|
8125
|
+
|
|
8126
|
+
# Issue #1752 — the names a {#singleton_class_named_call?} rewrites on its singleton class's owner, each
|
|
8127
|
+
# singleton envelope opaque and contested: the evidence the refinement typer reads as a rewritten singleton name.
|
|
8128
|
+
def record_singleton_class_names(node, object, qualified_prefix, tables)
|
|
8129
|
+
current_class = qualified_prefix.empty? ? nil : qualified_prefix.join("::")
|
|
8130
|
+
owner = singleton_class_owner(node, object, qualified_prefix, current_class)
|
|
8131
|
+
return if owner.nil?
|
|
8132
|
+
|
|
8133
|
+
singleton_class_rewritten_names(node).each do |method_name|
|
|
8134
|
+
names = [method_name]
|
|
8135
|
+
names << :"#{method_name}=" if %i[attr_writer attr_accessor].include?(node.name)
|
|
8136
|
+
names.each do |name|
|
|
8137
|
+
record_envelope(tables.envelopes, owner, [:singleton, name], Source::ParameterEnvelope::OPAQUE)
|
|
8138
|
+
# Contested even where the call is certain: the slot's `def` is not the body Ruby runs, so the
|
|
8139
|
+
# own-definer read declines on the name rather than answering it.
|
|
8140
|
+
contest_envelope(tables, owner, [:singleton, name])
|
|
8141
|
+
end
|
|
8142
|
+
end
|
|
8143
|
+
end
|
|
8144
|
+
|
|
8145
|
+
# Issue #1752 — `def Const.x` whose receiver is not the lexical `self` (written at the top level or in another
|
|
8146
|
+
# class's body) defines `Const`'s singleton method, which no table files under `Const`: its singleton name's
|
|
8147
|
+
# envelope turns opaque, the evidence the refinement typer reads as a rewritten singleton name.
|
|
8148
|
+
def record_foreign_singleton_def(node, qualified_prefix, owner_prefix, tables)
|
|
8149
|
+
receiver = node.receiver
|
|
8150
|
+
return unless receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
|
|
8151
|
+
return if def_receiver_targets_lexical_self?(receiver, owner_prefix)
|
|
8152
|
+
|
|
8153
|
+
constant_receiver_candidates(receiver, qualified_prefix).each do |name|
|
|
8154
|
+
record_envelope(tables.envelopes, name, [:singleton, node.name], Source::ParameterEnvelope::OPAQUE)
|
|
8155
|
+
contest_envelope(tables, name, [:singleton, node.name]) unless tables.certain
|
|
7761
8156
|
end
|
|
7762
8157
|
end
|
|
7763
8158
|
|
|
@@ -8221,12 +8616,14 @@ module Rigor
|
|
|
8221
8616
|
# {#discovered_project_index_incremental}'s changed-file branch) yields the live def nodes the signature
|
|
8222
8617
|
# reads their parameter structure from. Issue #1120 — and each file's own refinement table, which the merged
|
|
8223
8618
|
# def-index cannot attribute back to a file, so the session can diff it against the file's seed bundle.
|
|
8224
|
-
#
|
|
8619
|
+
# Issue #1459 — and each file's {#defined_method_names}, diffed the same way.
|
|
8620
|
+
# @return `{ def_index:, code_fingerprints:, declaration_signatures:, refinements:, defined_method_names: }`.
|
|
8225
8621
|
def scan_summary_for_paths(paths, buffer: nil)
|
|
8226
8622
|
acc = new_def_index_accumulator
|
|
8227
8623
|
code_fingerprints = {}
|
|
8228
8624
|
declaration_signatures = {}
|
|
8229
8625
|
refinements = {}
|
|
8626
|
+
defined_method_names = {}
|
|
8230
8627
|
paths.each do |path|
|
|
8231
8628
|
physical = buffer ? buffer.resolve(path) : path
|
|
8232
8629
|
source = File.read(physical)
|
|
@@ -8236,13 +8633,29 @@ module Rigor
|
|
|
8236
8633
|
code_fingerprints[path] = code_fingerprint(source, parsed.comments)
|
|
8237
8634
|
declaration_signatures[path] = declaration_signature(file_index)
|
|
8238
8635
|
refinements[path] = file_index[:refinements] if file_index[:refinements]
|
|
8636
|
+
defined_method_names[path] = defined_method_names(file_index)
|
|
8239
8637
|
rescue DeclarationWalk::ContractError
|
|
8240
8638
|
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
8241
8639
|
rescue StandardError
|
|
8242
8640
|
next
|
|
8243
8641
|
end
|
|
8244
8642
|
{ def_index: finalize_def_index(acc), code_fingerprints: code_fingerprints,
|
|
8245
|
-
declaration_signatures: declaration_signatures, refinements: refinements
|
|
8643
|
+
declaration_signatures: declaration_signatures, refinements: refinements,
|
|
8644
|
+
defined_method_names: defined_method_names }
|
|
8645
|
+
end
|
|
8646
|
+
|
|
8647
|
+
# Issue #1459 — every method name one file's index defines, on any class or module and either side: the keys of
|
|
8648
|
+
# `methods` (which also carries an `attr_*`, `alias` or `define_method` name) and of the instance and singleton
|
|
8649
|
+
# def-node tables. A single file's `methods` already holds its `def` names, since `subtract_def_methods` runs only
|
|
8650
|
+
# on the merged accumulator; the def-node tables are read as well so the census reads exactly the tables
|
|
8651
|
+
# `BlockCallTiming.project_defines_anywhere?` reads. Takes a live file index or a seed bundle, which hold the
|
|
8652
|
+
# same three keys.
|
|
8653
|
+
def defined_method_names(file_index)
|
|
8654
|
+
names = Set.new
|
|
8655
|
+
%i[methods def_nodes singleton_def_nodes].each do |key|
|
|
8656
|
+
file_index[key]&.each_value { |table| names.merge(table.keys) }
|
|
8657
|
+
end
|
|
8658
|
+
names.freeze
|
|
8246
8659
|
end
|
|
8247
8660
|
|
|
8248
8661
|
# ADR-89 WD1 — a per-file digest of every cross-file DECLARATION surface an ancestry / file-level
|
|
@@ -8804,6 +9217,9 @@ module Rigor
|
|
|
8804
9217
|
merge_header_nestings(acc[:header_nestings], file_index[:header_nestings] || {})
|
|
8805
9218
|
fold_mixin_lists(acc, file_index)
|
|
8806
9219
|
file_index[:class_sources].each { |cn, files| (acc[:class_sources][cn] ||= Set.new).merge(files) }
|
|
9220
|
+
(file_index[:singleton_class_sources] || {}).each do |cn, files|
|
|
9221
|
+
(acc[:singleton_class_sources][cn] ||= Set.new).merge(files)
|
|
9222
|
+
end
|
|
8807
9223
|
# Issue #722 residue 2 — a pre-#722 seed bundle carries no candidates; the SCHEMA bump makes such a
|
|
8808
9224
|
# blob a cold rebuild, but default so any in-flight fold stays total.
|
|
8809
9225
|
acc[:compact_headers].merge!(file_index[:compact_headers] || {})
|
|
@@ -8892,6 +9308,7 @@ module Rigor
|
|
|
8892
9308
|
# data, so the bundle stays Marshal-clean and a warm file joins the envelopes its cold walk records.
|
|
8893
9309
|
parameter_envelopes: file_index[:parameter_envelopes],
|
|
8894
9310
|
class_source_names: file_index[:class_sources].keys,
|
|
9311
|
+
singleton_class_source_names: (file_index[:singleton_class_sources] || {}).keys,
|
|
8895
9312
|
# Issue #722 residue 2 — plain data, so the bundle stays Marshal-clean and a warm incremental file
|
|
8896
9313
|
# re-anchors its compact headers the way a cold walk of it does.
|
|
8897
9314
|
compact_headers: file_index[:compact_headers],
|
|
@@ -8916,7 +9333,7 @@ module Rigor
|
|
|
8916
9333
|
# ADR-85 WD2 — reconstitutes a cached bundle into a single-file index {#fold_file_index} folds: the
|
|
8917
9334
|
# def-node triples become {DefHandle}s bound to this file's `path`, and the class-source names become a
|
|
8918
9335
|
# `{name => Set[path]}` table (the shape `accumulate_project_index` produces).
|
|
8919
|
-
def bundle_to_file_index(bundle, path) # rubocop:disable Metrics/
|
|
9336
|
+
def bundle_to_file_index(bundle, path) # rubocop:disable Metrics/AbcSize -- one read per bundle slot
|
|
8920
9337
|
{
|
|
8921
9338
|
def_nodes: bundle_defs_to_handles(bundle[:def_nodes], path),
|
|
8922
9339
|
singleton_def_nodes: bundle_defs_to_handles(bundle[:singleton_def_nodes], path),
|
|
@@ -8937,7 +9354,8 @@ module Rigor
|
|
|
8937
9354
|
method_visibilities: bundle[:method_visibilities],
|
|
8938
9355
|
methods: bundle[:methods],
|
|
8939
9356
|
parameter_envelopes: bundle[:parameter_envelopes] || {},
|
|
8940
|
-
class_sources: bundle[:class_source_names]
|
|
9357
|
+
class_sources: bundle_path_sets(bundle[:class_source_names], path),
|
|
9358
|
+
singleton_class_sources: bundle_path_sets(bundle[:singleton_class_source_names], path),
|
|
8941
9359
|
compact_headers: bundle[:compact_headers] || {},
|
|
8942
9360
|
# Issue #644 — a pre-#644 bundle carries no census; the SCHEMA bump makes such a blob a cold
|
|
8943
9361
|
# rebuild, but default so any in-flight fold stays total.
|
|
@@ -8987,7 +9405,7 @@ module Rigor
|
|
|
8987
9405
|
method_visibilities: {},
|
|
8988
9406
|
methods: {},
|
|
8989
9407
|
deferred_ranges: {},
|
|
8990
|
-
parameter_envelopes: {}, class_sources: {},
|
|
9408
|
+
parameter_envelopes: {}, class_sources: {}, singleton_class_sources: {},
|
|
8991
9409
|
# Issue #722 residue 2 — compact-header re-anchor candidates, adjudicated in {#finalize_def_index}.
|
|
8992
9410
|
compact_headers: {},
|
|
8993
9411
|
constant_writes: {},
|
|
@@ -10454,8 +10872,15 @@ module Rigor
|
|
|
10454
10872
|
|
|
10455
10873
|
# Issue #1717 — a `def` / `class` / `module` body opens its own `self`, so an enclosing unentered block's
|
|
10456
10874
|
# unknown-`self` mark ({Scope#block_self_unknown?}) stops at it, as the evaluator's fresh body scope does.
|
|
10457
|
-
#
|
|
10875
|
+
# A `case` narrows its clauses ({#propagate_subjectless_case}, {#propagate_subject_case}). Every other node
|
|
10876
|
+
# hands its children the scope as is.
|
|
10458
10877
|
def propagate_children(node, table, current_scope)
|
|
10878
|
+
if node.is_a?(Prism::CaseNode) || node.is_a?(Prism::CaseMatchNode)
|
|
10879
|
+
return propagate_subjectless_case(node, table, current_scope) if node.predicate.nil?
|
|
10880
|
+
|
|
10881
|
+
return propagate_subject_case(node, table, current_scope)
|
|
10882
|
+
end
|
|
10883
|
+
|
|
10459
10884
|
child_scope =
|
|
10460
10885
|
SELF_OPENING_BODIES.include?(node.class) ? current_scope.with_block_self_unknown(false) : current_scope
|
|
10461
10886
|
node.rigor_each_child { |child| propagate(child, table, child_scope) }
|
|
@@ -10661,6 +11086,85 @@ module Rigor
|
|
|
10661
11086
|
propagate(node.statements, table, falsey_scope) if node.statements
|
|
10662
11087
|
propagate(node.else_clause, table, truthy_scope) if node.else_clause
|
|
10663
11088
|
end
|
|
11089
|
+
|
|
11090
|
+
# Issue #1495 — a `case` without a subject the evaluator did not enter (`[case; when x.nil? then 0; else x.size
|
|
11091
|
+
# end]`) narrows by its conditions' edges, as {StatementEvaluator#subjectless_when_scopes} does: each condition
|
|
11092
|
+
# under the falsey edges of the conditions before it, each clause under the join of its conditions' truthy edges,
|
|
11093
|
+
# and the `else` under the last falsey edge. Like the walk of an `if`, it applies no write a condition makes, so
|
|
11094
|
+
# the edges are taken from a scope where every bound local the condition writes reads `Dynamic[top]`
|
|
11095
|
+
# ({#widen_condition_writes}): neither the local's old type nor an earlier condition's narrowing of it reaches
|
|
11096
|
+
# them, as `when (x = s ? String(s) : nil) then x.upcase` after `x = 1` would.
|
|
11097
|
+
def propagate_subjectless_case(node, table, current_scope)
|
|
11098
|
+
falsey = current_scope
|
|
11099
|
+
node.conditions.each do |when_node|
|
|
11100
|
+
truthy_edges = when_node.conditions.map do |condition|
|
|
11101
|
+
propagate(condition, table, falsey)
|
|
11102
|
+
truthy, falsey = Narrowing.predicate_scopes(condition, widen_condition_writes(condition, falsey))
|
|
11103
|
+
truthy
|
|
11104
|
+
end
|
|
11105
|
+
propagate(when_node, table, truthy_edges.reduce(:join))
|
|
11106
|
+
end
|
|
11107
|
+
propagate(node.else_clause, table, falsey) if node.else_clause
|
|
11108
|
+
end
|
|
11109
|
+
|
|
11110
|
+
# Issue #1484 — a `case` with a subject the evaluator did not enter (`[case io when StringIO then io.string
|
|
11111
|
+
# end]`, `[case io in StringIO then io.string else nil end]`) narrows each clause as the evaluator does
|
|
11112
|
+
# ({Narrowing.case_clause_scopes}): the body under the clause's narrowed edge, the next clause and the `else`
|
|
11113
|
+
# under its falsey edge. The walk applies no write the subject, a `when` condition or an `in` pattern makes, so
|
|
11114
|
+
# every bound local one of them writes reads `Dynamic[top]` from that point on ({#widen_condition_writes}).
|
|
11115
|
+
# Nor does it run the evaluator's per-clause `LastLine.forget_if_set`: the statement holding the `case`
|
|
11116
|
+
# already forgets a `$_` any of its tests may set. A clause's tests are recorded under the scope the earlier
|
|
11117
|
+
# clauses' falsey edges left, where the evaluator's `when` tests read the post-subject scope: a test reading
|
|
11118
|
+
# the subject sees what the earlier clauses ruled out (`when NilClass then 0 when v.succ`).
|
|
11119
|
+
#
|
|
11120
|
+
# A test that calls a method may rebind an instance-variable or global subject before the body reads it
|
|
11121
|
+
# (`when String then 1 when reset! then @x.upcase`, where `reset!` writes `@x`), and the walk cannot tell. From
|
|
11122
|
+
# the first such test on, the subject reads `Dynamic[top]` on both edges ({#forget_called_subject}). The
|
|
11123
|
+
# evaluator keeps the narrowing there (a separate defect); the walk declines it rather than add a report.
|
|
11124
|
+
def propagate_subject_case(node, table, current_scope)
|
|
11125
|
+
subject = node.predicate
|
|
11126
|
+
propagate(subject, table, current_scope)
|
|
11127
|
+
falsey = widen_condition_writes(subject, current_scope)
|
|
11128
|
+
called = false
|
|
11129
|
+
node.conditions.each do |clause|
|
|
11130
|
+
tests = clause.is_a?(Prism::WhenNode) ? clause.conditions : [clause.pattern]
|
|
11131
|
+
tests.each { |test| propagate(test, table, falsey) }
|
|
11132
|
+
falsey = tests.reduce(falsey) { |acc, test| widen_condition_writes(test, acc) }
|
|
11133
|
+
called ||= tests.any? { |test| runs_call?(test) }
|
|
11134
|
+
body, falsey = Narrowing.case_clause_scopes(subject, clause, falsey)
|
|
11135
|
+
body, falsey = [body, falsey].map { |scope| forget_called_subject(subject, scope) } if called
|
|
11136
|
+
propagate(clause, table, body)
|
|
11137
|
+
end
|
|
11138
|
+
propagate(node.else_clause, table, falsey) if node.else_clause
|
|
11139
|
+
end
|
|
11140
|
+
|
|
11141
|
+
CODE_RUNNING_NODES = [Prism::CallNode, Prism::SuperNode, Prism::ForwardingSuperNode, Prism::YieldNode].freeze
|
|
11142
|
+
private_constant :CODE_RUNNING_NODES
|
|
11143
|
+
|
|
11144
|
+
# Whether `node` holds a method call, `super` or `yield` anywhere, a lambda body's included.
|
|
11145
|
+
def runs_call?(node)
|
|
11146
|
+
return false unless node.is_a?(Prism::Node)
|
|
11147
|
+
return true if CODE_RUNNING_NODES.include?(node.class)
|
|
11148
|
+
|
|
11149
|
+
node.rigor_each_child { |child| return true if runs_call?(child) }
|
|
11150
|
+
false
|
|
11151
|
+
end
|
|
11152
|
+
|
|
11153
|
+
# `scope` with an instance-variable or global `subject` rebound to `Dynamic[top]`; any other subject as is.
|
|
11154
|
+
def forget_called_subject(subject, scope)
|
|
11155
|
+
case subject
|
|
11156
|
+
when Prism::InstanceVariableReadNode then scope.with_ivar(subject.name, Type::Combinator.untyped)
|
|
11157
|
+
when Prism::GlobalVariableReadNode then scope.with_global(subject.name, Type::Combinator.untyped)
|
|
11158
|
+
else scope
|
|
11159
|
+
end
|
|
11160
|
+
end
|
|
11161
|
+
|
|
11162
|
+
# `scope` with every bound local `condition` writes rebound to `Dynamic[top]`.
|
|
11163
|
+
def widen_condition_writes(condition, scope)
|
|
11164
|
+
OperandEffects.written_variables(condition).reduce(scope) do |acc, name|
|
|
11165
|
+
acc.local(name) ? acc.with_local(name, Type::Combinator.untyped) : acc
|
|
11166
|
+
end
|
|
11167
|
+
end
|
|
10664
11168
|
end
|
|
10665
11169
|
end
|
|
10666
11170
|
end
|