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
|
@@ -43,6 +43,8 @@ require_relative "scope_indexer/layered_def_nestings"
|
|
|
43
43
|
require_relative "scope_indexer/mixin_accumulator"
|
|
44
44
|
require_relative "scope_indexer/certainty"
|
|
45
45
|
require_relative "scope_indexer/member_layouts_collector"
|
|
46
|
+
require_relative "scope_indexer/declared_ivar_types"
|
|
47
|
+
require_relative "scope_indexer/declared_param_writes"
|
|
46
48
|
|
|
47
49
|
module Rigor
|
|
48
50
|
module Inference
|
|
@@ -112,7 +114,17 @@ module Rigor
|
|
|
112
114
|
# nothing-to-re-anchor walk allocates none.
|
|
113
115
|
EMPTY_RENAMES = {}.freeze
|
|
114
116
|
|
|
115
|
-
|
|
117
|
+
# No path whose contributions are all `possible` (issue #2021), the cross-file walks' default.
|
|
118
|
+
EMPTY_PATH_SET = Set.new.freeze
|
|
119
|
+
|
|
120
|
+
# `declared_ivar_types:` — whether an RBS-declared ivar type replaces the census's join of the writes (#1406).
|
|
121
|
+
# `rigor sig-gen` passes false: it proposes what the code says, and reading a declaration back would make a row
|
|
122
|
+
# count as generated only because it is declared.
|
|
123
|
+
# `ivar_writer_scope:` — the scope whose project tables the #1154 declared-parameter credit reads to find the
|
|
124
|
+
# ivar writers outside this file ({DeclaredParamWrites}); defaults to `default_scope`. `rigor sig-gen` indexes
|
|
125
|
+
# each file under an unseeded scope, so it passes one carrying the project's tables.
|
|
126
|
+
def index(root, default_scope:, converged_loop_recording: false, widen_clauses: false, # rubocop:disable Metrics/AbcSize
|
|
127
|
+
declared_ivar_types: true, ivar_writer_scope: nil)
|
|
116
128
|
# Slice A-declarations. Build the declaration overrides first so every scope handed to the StatementEvaluator
|
|
117
129
|
# already carries the table; structural sharing through `Scope#with_local` / `#with_fact` / `#with_self_type`
|
|
118
130
|
# propagates it across every derived scope.
|
|
@@ -134,7 +146,9 @@ module Rigor
|
|
|
134
146
|
|
|
135
147
|
# Slice 7 phase 2. Pre-pass over every class/module body to collect the per-class ivar accumulator. Seeded after
|
|
136
148
|
# declared_types so the rvalue typer in the pre-pass can see declaration overrides.
|
|
137
|
-
class_ivars =
|
|
149
|
+
class_ivars = DeclaredParamWrites.with_writer_scope(ivar_writer_scope) do
|
|
150
|
+
build_class_ivar_index(root, seeded_scope, declared_ivar_types: declared_ivar_types)
|
|
151
|
+
end
|
|
138
152
|
seeded_scope = seeded_scope.with_discovery(seeded_scope.discovery.with(class_ivars: class_ivars))
|
|
139
153
|
|
|
140
154
|
# Slice 7 phase 6. Same pre-pass shape for cvars (per class) and globals (program-wide). Globals are also
|
|
@@ -197,15 +211,27 @@ module Rigor
|
|
|
197
211
|
# share.
|
|
198
212
|
seeded_scope = seed_struct_fold_safe(seeded_scope, root).with_match_frame(root)
|
|
199
213
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
converged_loop_recording: converged_loop_recording,
|
|
203
|
-
widen_clauses: widen_clauses).evaluate(root)
|
|
214
|
+
evaluate = recording_evaluator(table, converged_loop_recording, widen_clauses)
|
|
215
|
+
evaluate.call(root, seeded_scope)
|
|
204
216
|
|
|
205
|
-
propagate(root, table, seeded_scope)
|
|
217
|
+
with_unentered_evaluator(evaluate) { propagate(root, table, seeded_scope) }
|
|
206
218
|
table
|
|
207
219
|
end
|
|
208
220
|
|
|
221
|
+
# The `(node, scope, excluded = nil) ->` evaluation that records every node it visits into `table`, except the
|
|
222
|
+
# nodes in `excluded`: the file's own pass, and each construct {#evaluate_unentered_flow} evaluates in the walk
|
|
223
|
+
# after it.
|
|
224
|
+
def recording_evaluator(table, converged_loop_recording, widen_clauses)
|
|
225
|
+
lambda do |node, scope, excluded = nil|
|
|
226
|
+
on_enter =
|
|
227
|
+
if excluded.nil? || excluded.empty? then ->(visited, at) { table[visited] = at }
|
|
228
|
+
else ->(visited, at) { table[visited] = at unless excluded.include?(visited) }
|
|
229
|
+
end
|
|
230
|
+
StatementEvaluator.new(scope: scope, on_enter: on_enter, converged_loop_recording: converged_loop_recording,
|
|
231
|
+
widen_clauses: widen_clauses).evaluate(node)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
209
235
|
# Runs the combined methods/def-nodes descent (one walk of the file), seeds the discovered-methods existence table
|
|
210
236
|
# onto `seeded_scope` (merged UNDER the cross-file pre-pass seed `default_scope` carries), and returns `[scope,
|
|
211
237
|
# file_def_nodes, file_envelopes]` so the caller can thread the def-node and issue #992 envelope tables into
|
|
@@ -498,7 +524,7 @@ module Rigor
|
|
|
498
524
|
# (singleton vs instance). The rvalue is typed with NO local bindings — the pre-pass lacks statement-level
|
|
499
525
|
# threading — so `@x = 1` records `Constant[1]` but `@x = some_local + 1` records `Dynamic[Top]` (since
|
|
500
526
|
# `some_local` is unbound at pre-pass time). Multiple writes to the same ivar union via `Type::Combinator.union`.
|
|
501
|
-
def build_class_ivar_index(root, default_scope)
|
|
527
|
+
def build_class_ivar_index(root, default_scope, declared_ivar_types: true)
|
|
502
528
|
accumulator = {}
|
|
503
529
|
mutated_ivars = {}
|
|
504
530
|
read_before_write = {}
|
|
@@ -517,6 +543,8 @@ module Rigor
|
|
|
517
543
|
widen_mutated_ivar_entries!(accumulator, mutated_ivars)
|
|
518
544
|
contribute_read_before_write_nil!(accumulator, read_before_write, init_writes)
|
|
519
545
|
make_reflective_classes_gradual!(accumulator, reflective_opaque)
|
|
546
|
+
# #1406 — an RBS `@x: T` or `attr_* x: T` declaration replaces the census's join of the writes.
|
|
547
|
+
DeclaredIvarTypes.apply!(accumulator, default_scope) if declared_ivar_types
|
|
520
548
|
accumulator.transform_values(&:freeze).freeze
|
|
521
549
|
end
|
|
522
550
|
|
|
@@ -1024,8 +1052,11 @@ module Rigor
|
|
|
1024
1052
|
# nodes to skip; soundness is post-domination at the top statement level, so dropping the nil never hides a real
|
|
1025
1053
|
# runtime-nil read.
|
|
1026
1054
|
dead_writes = dead_transient_nil_writes(def_node.body, class_name, method_assign_effects)
|
|
1055
|
+
# #1154 — an `@x = x` from a declared parameter records the parameter's declared type.
|
|
1056
|
+
declared = DeclaredParamWrites::EMPTY
|
|
1057
|
+
declared = DeclaredParamWrites.collect(def_node, class_name, body_scope) unless singleton
|
|
1027
1058
|
gather_ivar_writes(def_node.body, body_scope, class_name, accumulator,
|
|
1028
|
-
EMPTY_GUARDED_IVARS, mutated_ivars, dead_writes)
|
|
1059
|
+
EMPTY_GUARDED_IVARS, mutated_ivars, dead_writes, declared)
|
|
1029
1060
|
|
|
1030
1061
|
# B2.3 — collect per-method evidence for the read-before- write nil contribution. The accumulator-level decision
|
|
1031
1062
|
# ("is this ivar truly read-before-write across the class lifetime?") is finalised at
|
|
@@ -1341,14 +1372,14 @@ module Rigor
|
|
|
1341
1372
|
private_constant :TAIL_VALUE_DEPTH_CAP
|
|
1342
1373
|
private_constant :EMPTY_GUARDED_IVARS
|
|
1343
1374
|
|
|
1344
|
-
def gather_ivar_writes(node, scope, class_name, accumulator, guarded_ivars = EMPTY_GUARDED_IVARS,
|
|
1345
|
-
mutated_ivars = nil, dead_writes = nil)
|
|
1375
|
+
def gather_ivar_writes(node, scope, class_name, accumulator, guarded_ivars = EMPTY_GUARDED_IVARS, # rubocop:disable Metrics/ParameterLists
|
|
1376
|
+
mutated_ivars = nil, dead_writes = nil, declared = DeclaredParamWrites::EMPTY)
|
|
1346
1377
|
return unless node.is_a?(Prism::Node)
|
|
1347
1378
|
|
|
1348
1379
|
if node.is_a?(Prism::InstanceVariableWriteNode)
|
|
1349
1380
|
unless dead_writes&.include?(node.object_id)
|
|
1350
1381
|
record_ivar_write(node, scope, class_name, accumulator,
|
|
1351
|
-
guarded: guarded_ivars.include?(node.name))
|
|
1382
|
+
guarded: guarded_ivars.include?(node.name), declared: declared[node])
|
|
1352
1383
|
end
|
|
1353
1384
|
else
|
|
1354
1385
|
record_compound_ivar_write(node, scope, class_name, accumulator)
|
|
@@ -1371,12 +1402,12 @@ module Rigor
|
|
|
1371
1402
|
|
|
1372
1403
|
if node.is_a?(Prism::IfNode) || node.is_a?(Prism::UnlessNode)
|
|
1373
1404
|
walk_conditional_ivar_writes(node, scope, class_name, accumulator, guarded_ivars,
|
|
1374
|
-
mutated_ivars, dead_writes)
|
|
1405
|
+
mutated_ivars, dead_writes, declared)
|
|
1375
1406
|
return
|
|
1376
1407
|
end
|
|
1377
1408
|
|
|
1378
1409
|
node.rigor_each_child do |c|
|
|
1379
|
-
gather_ivar_writes(c, scope, class_name, accumulator, guarded_ivars, mutated_ivars, dead_writes)
|
|
1410
|
+
gather_ivar_writes(c, scope, class_name, accumulator, guarded_ivars, mutated_ivars, dead_writes, declared)
|
|
1380
1411
|
end
|
|
1381
1412
|
end
|
|
1382
1413
|
|
|
@@ -1452,22 +1483,22 @@ module Rigor
|
|
|
1452
1483
|
# init; end` would otherwise be marked too — but that pattern (write @x in the else of `if @x`) is a separate
|
|
1453
1484
|
# idiom whose surrounding reads of `@x` would then surface a nil-receiver FP. The ELSE branch is left ungarded so
|
|
1454
1485
|
# those reads continue to type as they did before this fix.
|
|
1455
|
-
def walk_conditional_ivar_writes(node, scope, class_name, accumulator, guarded_ivars,
|
|
1456
|
-
mutated_ivars = nil, dead_writes = nil)
|
|
1486
|
+
def walk_conditional_ivar_writes(node, scope, class_name, accumulator, guarded_ivars, # rubocop:disable Metrics/ParameterLists
|
|
1487
|
+
mutated_ivars = nil, dead_writes = nil, declared = DeclaredParamWrites::EMPTY)
|
|
1457
1488
|
then_guards = then_body_guarded_ivars(node)
|
|
1458
1489
|
then_guarded = then_guards.empty? ? guarded_ivars : (guarded_ivars | then_guards)
|
|
1459
1490
|
|
|
1460
1491
|
gather_ivar_writes(node.predicate, scope, class_name, accumulator, guarded_ivars,
|
|
1461
|
-
mutated_ivars, dead_writes)
|
|
1492
|
+
mutated_ivars, dead_writes, declared)
|
|
1462
1493
|
if node.statements
|
|
1463
1494
|
gather_ivar_writes(node.statements, scope, class_name, accumulator, then_guarded,
|
|
1464
|
-
mutated_ivars, dead_writes)
|
|
1495
|
+
mutated_ivars, dead_writes, declared)
|
|
1465
1496
|
end
|
|
1466
1497
|
branch = node.is_a?(Prism::IfNode) ? node.subsequent : node.else_clause
|
|
1467
1498
|
return unless branch
|
|
1468
1499
|
|
|
1469
1500
|
gather_ivar_writes(branch, scope, class_name, accumulator, guarded_ivars,
|
|
1470
|
-
mutated_ivars, dead_writes)
|
|
1501
|
+
mutated_ivars, dead_writes, declared)
|
|
1471
1502
|
end
|
|
1472
1503
|
|
|
1473
1504
|
# Returns the set of ivar names that, in the THEN body of this conditional, are statically known to be in a nil /
|
|
@@ -1944,8 +1975,8 @@ module Rigor
|
|
|
1944
1975
|
assigns.include?(target)
|
|
1945
1976
|
end
|
|
1946
1977
|
|
|
1947
|
-
def record_ivar_write(node, scope, class_name, accumulator, guarded: false)
|
|
1948
|
-
rvalue_type = scope.type_of(node.value)
|
|
1978
|
+
def record_ivar_write(node, scope, class_name, accumulator, guarded: false, declared: nil)
|
|
1979
|
+
rvalue_type = declared || scope.type_of(node.value)
|
|
1949
1980
|
|
|
1950
1981
|
# `@x = nil unless @x` / `@y = false unless @y` — follow-up to the polarity-aware defensive-init guard fix
|
|
1951
1982
|
# (ROADMAP § Future cycles — "Defensive ivar-init with nil / false rvalue"). When the rvalue is itself a falsey
|
|
@@ -3433,7 +3464,17 @@ module Rigor
|
|
|
3433
3464
|
# walk worked out for it ({#refine_context}).
|
|
3434
3465
|
MethodTables = Struct.new(:existence, :envelopes, :refinements, :certainty, :certain, :possible,
|
|
3435
3466
|
:contested_envelopes, :contested_def_nodes, :root, :in_effect, :census,
|
|
3436
|
-
:refine_text, :literal_seen, :source_bytes, :loose_constants, :block_spans
|
|
3467
|
+
:refine_text, :literal_seen, :source_bytes, :loose_constants, :block_spans,
|
|
3468
|
+
:definer_history) do
|
|
3469
|
+
# Issue #2006 — records, in walk order, that a definer other than an alias wrote the instance method `name`
|
|
3470
|
+
# of `class_name` at `node`: `def_node` for a `def`, nil for a `define_method(:x)` or an `attr_*` reader or
|
|
3471
|
+
# writer, and whether the write is {#certain}. {ScopeIndexer#apply_alias_def_nodes} reads the history to copy
|
|
3472
|
+
# what an alias's old name held at the alias, and to tell an alias a later definer replaced.
|
|
3473
|
+
def wrote_definer(class_name, name, node, def_node)
|
|
3474
|
+
entry = [node.location.start_offset, def_node, certain ? true : false].freeze
|
|
3475
|
+
((self.definer_history ||= {})[[class_name, name]] ||= []) << entry
|
|
3476
|
+
end
|
|
3477
|
+
|
|
3437
3478
|
# {ScopeIndexer.module_refine_target} against this tree's {InEffectRefinements}.
|
|
3438
3479
|
def refine_target_of(node)
|
|
3439
3480
|
return nil if ScopeIndexer.refine_target(node).nil?
|
|
@@ -4171,7 +4212,9 @@ module Rigor
|
|
|
4171
4212
|
anonymous = record_call_node_methods(node, owner_prefix, in_singleton_class, methods_acc.at(node),
|
|
4172
4213
|
source_path)
|
|
4173
4214
|
unless in_singleton_class
|
|
4174
|
-
|
|
4215
|
+
superseded = call_superseded_names(node)
|
|
4216
|
+
supersede_def_nodes(superseded, owner_prefix, def_nodes_acc, methods_acc.at(node))
|
|
4217
|
+
note_call_definers(node, superseded, owner_prefix, methods_acc.at(node))
|
|
4175
4218
|
end
|
|
4176
4219
|
tables = methods_acc.at(node)
|
|
4177
4220
|
record_singleton_prepend(node, qualified_prefix, tables,
|
|
@@ -4811,6 +4854,7 @@ module Rigor
|
|
|
4811
4854
|
accumulator[class_name] ||= {}
|
|
4812
4855
|
accumulator[class_name][def_node.name] = def_node
|
|
4813
4856
|
follow_slot_writer(tables, :contested_def_nodes, [class_name, def_node.name], tables.nil? || tables.certain)
|
|
4857
|
+
tables&.wrote_definer(class_name, def_node.name, def_node, def_node)
|
|
4814
4858
|
record_anonymous_body_def_as_toplevel(def_node, qualified_prefix, accumulator, tables)
|
|
4815
4859
|
end
|
|
4816
4860
|
|
|
@@ -4870,6 +4914,16 @@ module Rigor
|
|
|
4870
4914
|
end
|
|
4871
4915
|
end
|
|
4872
4916
|
|
|
4917
|
+
# Issue #2006 — a `define_method(:x)` or `attr_*` call is a definer the alias pass reads in source order
|
|
4918
|
+
# ({#apply_alias_def_nodes}). An `alias_method` call, whose new name {#call_superseded_names} also lists, is
|
|
4919
|
+
# itself an alias: the pass records its copy, so the history must not hold it as a definer with no `def`.
|
|
4920
|
+
def note_call_definers(call_node, names, qualified_prefix, tables)
|
|
4921
|
+
return if names.empty? || qualified_prefix.empty? || call_node.name == :alias_method
|
|
4922
|
+
|
|
4923
|
+
class_name = qualified_prefix.join("::")
|
|
4924
|
+
names.each { |name| tables.wrote_definer(class_name, name, call_node, nil) }
|
|
4925
|
+
end
|
|
4926
|
+
|
|
4873
4927
|
# #319 — a `def` inside an anonymous `Class.new` / `Module.new` body ALSO stays in the `<toplevel>` table.
|
|
4874
4928
|
# Before the anonymous class had a name the body was walked with an empty prefix, so every such `def`
|
|
4875
4929
|
# landed there; that is the same leniency this key already grants a `def` nested in any other DSL block
|
|
@@ -5035,7 +5089,7 @@ module Rigor
|
|
|
5035
5089
|
# nested `def` or block inside its body is still a range the containment half needs.
|
|
5036
5090
|
# `def_owner_prefix` is the eval-body override described on {#walk_methods_and_def_nodes}:
|
|
5037
5091
|
# an explicit owner for def rows while declarations stay lexical (`[]` = ownerless).
|
|
5038
|
-
def walk_deferred_ranges(node, qualified_prefix, in_singleton_class, inside_deferred, # rubocop:disable Metrics/ParameterLists
|
|
5092
|
+
def walk_deferred_ranges(node, qualified_prefix, in_singleton_class, inside_deferred, # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
|
|
5039
5093
|
mf_offsets, ranges, def_owner_prefix = nil, singleton_cref: false,
|
|
5040
5094
|
defs_singleton: false)
|
|
5041
5095
|
return unless node.is_a?(Prism::Node)
|
|
@@ -5071,11 +5125,34 @@ module Rigor
|
|
|
5071
5125
|
singleton_cref: singleton_cref)
|
|
5072
5126
|
end
|
|
5073
5127
|
|
|
5128
|
+
record_definer_name_row(node, ranges) if node.is_a?(Prism::CallNode)
|
|
5074
5129
|
walk_deferred_children(node, qualified_prefix, in_singleton_class, inside_deferred,
|
|
5075
5130
|
mf_offsets, ranges, def_owner_prefix, singleton_cref: singleton_cref,
|
|
5076
5131
|
defs_singleton: defs_singleton)
|
|
5077
5132
|
end
|
|
5078
5133
|
|
|
5134
|
+
DEFINER_NAME_CALLS = Set[:define_method, :define_singleton_method, :alias_method].freeze
|
|
5135
|
+
DEFINER_SEND_CALLS = Set[:send, :__send__, :public_send].freeze
|
|
5136
|
+
private_constant :DEFINER_NAME_CALLS, :DEFINER_SEND_CALLS
|
|
5137
|
+
|
|
5138
|
+
# Issue #2184 — a `define_method` / `define_singleton_method` / `alias_method` call with a literal (new) name,
|
|
5139
|
+
# on any receiver and through `send` too, records an ownerless, kindless row over the name literal: no owner or
|
|
5140
|
+
# side is claimed, so no ordering scan matches it and the literal contains no call. It only tells a by-name
|
|
5141
|
+
# reader that the project gives the name a body no def table holds. A computed name records nothing.
|
|
5142
|
+
def record_definer_name_row(node, ranges)
|
|
5143
|
+
arguments = node.arguments&.arguments || []
|
|
5144
|
+
if DEFINER_SEND_CALLS.include?(node.name)
|
|
5145
|
+
return unless DEFINER_NAME_CALLS.include?(literal_method_name(arguments.first))
|
|
5146
|
+
|
|
5147
|
+
arguments = arguments.drop(1)
|
|
5148
|
+
end
|
|
5149
|
+
return unless DEFINER_NAME_CALLS.include?(node.name) || DEFINER_SEND_CALLS.include?(node.name)
|
|
5150
|
+
|
|
5151
|
+
argument = arguments.first
|
|
5152
|
+
name = literal_method_name(argument)
|
|
5153
|
+
ranges << [argument.location.start_offset, argument.location.end_offset, name, nil, nil] if name
|
|
5154
|
+
end
|
|
5155
|
+
|
|
5079
5156
|
# The `def` arm of {#walk_deferred_ranges}: records the range row — singleton-side when the
|
|
5080
5157
|
# def sits in a `class <<` body or an `instance_eval` block (`defs_singleton`) — then walks
|
|
5081
5158
|
# the body as a deferred range.
|
|
@@ -5586,7 +5663,9 @@ module Rigor
|
|
|
5586
5663
|
mf_offsets, ranges)
|
|
5587
5664
|
start = def_node.location.start_offset
|
|
5588
5665
|
if inside_deferred
|
|
5589
|
-
|
|
5666
|
+
# Issue #2184 — the def installs at invocation under a `self` the walk does not know, so the row names the
|
|
5667
|
+
# method but claims no owner or side.
|
|
5668
|
+
ranges << [start, def_node.location.end_offset, def_node.name, nil, nil]
|
|
5590
5669
|
return
|
|
5591
5670
|
end
|
|
5592
5671
|
|
|
@@ -7564,7 +7643,8 @@ module Rigor
|
|
|
7564
7643
|
return record_module_function(node, current_class, accumulator, in_singleton) if node.name == :module_function
|
|
7565
7644
|
|
|
7566
7645
|
taint_opaque_eval(node, current_class, accumulator)
|
|
7567
|
-
return if singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator
|
|
7646
|
+
return if singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator,
|
|
7647
|
+
in_singleton: in_singleton)
|
|
7568
7648
|
|
|
7569
7649
|
kind, _arguments, via_send = mixin_call_view(node)
|
|
7570
7650
|
return if kind.nil?
|
|
@@ -7572,7 +7652,8 @@ module Rigor
|
|
|
7572
7652
|
effects = mixin_effects(node, kind, qualified_prefix, current_class, accumulator, in_singleton: in_singleton)
|
|
7573
7653
|
.select { |_owner, side| side == :extend || in_singleton }
|
|
7574
7654
|
return if effects.empty?
|
|
7575
|
-
return if !via_send && singleton_mixin_recorded?(node, kind, current_class, accumulator,
|
|
7655
|
+
return if !via_send && singleton_mixin_recorded?(node, kind, qualified_prefix, current_class, accumulator,
|
|
7656
|
+
in_singleton)
|
|
7576
7657
|
|
|
7577
7658
|
effects.each { |owner, side| accumulator.taint(owner, side) }
|
|
7578
7659
|
end
|
|
@@ -7587,7 +7668,7 @@ module Rigor
|
|
|
7587
7668
|
# but only where it is a direct statement (a top-level statement, or one of a direct declaration body or of a
|
|
7588
7669
|
# block that runs there once): there the methods walk records those marks under the same owner. In a method
|
|
7589
7670
|
# body, a hook or a conditional it may run on a receiver or at a time the marks cannot place, so it still taints.
|
|
7590
|
-
def singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator)
|
|
7671
|
+
def singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator, in_singleton: false)
|
|
7591
7672
|
receiver = node.receiver
|
|
7592
7673
|
return false unless singleton_class_of_receiver?(receiver) && surface_rewriting_call?(node)
|
|
7593
7674
|
return false if singleton_class_named_call?(node) && accumulator.direct?(node)
|
|
@@ -7595,10 +7676,33 @@ module Rigor
|
|
|
7595
7676
|
owner = singleton_class_owner(node, receiver.receiver, qualified_prefix, current_class)
|
|
7596
7677
|
return false if owner.nil?
|
|
7597
7678
|
|
|
7679
|
+
# Inside `class << self` a bare `singleton_class` is the singleton's own singleton, one level up from the
|
|
7680
|
+
# owner's extend side, so that call stays a taint.
|
|
7681
|
+
up_level = in_singleton && lexical_self_receiver?(receiver.receiver)
|
|
7682
|
+
return true if !up_level && singleton_class_include_recorded?(node, owner, accumulator)
|
|
7683
|
+
|
|
7598
7684
|
accumulator.taint(owner, :extend)
|
|
7599
7685
|
true
|
|
7600
7686
|
end
|
|
7601
7687
|
|
|
7688
|
+
# Issue #2094 — `Float.singleton_class.include(Ext)` (or `prepend`, which the extend table cannot order, so the
|
|
7689
|
+
# edge is unpositioned as every receiver-form edge is) puts `Ext` on the singleton side as `Float.extend(Ext)`
|
|
7690
|
+
# does, the fold #915 gives `class << self; include M`. A `send` spelling or an argument that is not a constant
|
|
7691
|
+
# stays a taint.
|
|
7692
|
+
def singleton_class_include_recorded?(node, owner, accumulator)
|
|
7693
|
+
return false unless %i[include prepend].include?(node.name)
|
|
7694
|
+
|
|
7695
|
+
arguments = node.arguments&.arguments || []
|
|
7696
|
+
return false if arguments.empty? || arguments.any? { |arg| Source::ConstantPath.qualified_name(arg).nil? }
|
|
7697
|
+
|
|
7698
|
+
!record_extend_targets(node, owner, accumulator).empty?
|
|
7699
|
+
end
|
|
7700
|
+
|
|
7701
|
+
# A bare or `self.` receiver: the lexical `self`.
|
|
7702
|
+
def lexical_self_receiver?(receiver)
|
|
7703
|
+
receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
7704
|
+
end
|
|
7705
|
+
|
|
7602
7706
|
# A bare, `self.` or constant receiver's `singleton_class` call.
|
|
7603
7707
|
def singleton_class_of_receiver?(receiver)
|
|
7604
7708
|
receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class && receiver.arguments.nil?
|
|
@@ -7657,13 +7761,29 @@ module Rigor
|
|
|
7657
7761
|
|
|
7658
7762
|
# Records the singleton-side edges of a receiverless `extend` (or, inside `class << self`, `include` /
|
|
7659
7763
|
# `prepend`). False when it recorded nothing.
|
|
7660
|
-
def singleton_mixin_recorded?(node, kind, current_class, accumulator, in_singleton)
|
|
7661
|
-
return
|
|
7764
|
+
def singleton_mixin_recorded?(node, kind, qualified_prefix, current_class, accumulator, in_singleton)
|
|
7765
|
+
return receiver_extend_recorded?(node, kind, qualified_prefix, accumulator) if node.receiver
|
|
7766
|
+
return false unless current_class
|
|
7662
7767
|
return false unless in_singleton ? MIXIN_CALL_NAMES.include?(kind) : kind == :extend
|
|
7663
7768
|
|
|
7664
7769
|
!record_extend_targets(node, current_class, accumulator).empty?
|
|
7665
7770
|
end
|
|
7666
7771
|
|
|
7772
|
+
# Issue #2094 — the receiver form, `Float.extend(Ext)`: the singleton twin of #1722's `Integer.include(Ext)`,
|
|
7773
|
+
# filed under the class the constant receiver names ({#prepend_call_receiver}) and, like that form,
|
|
7774
|
+
# unpositioned wherever it is written ({MixinAccumulator#note}). A `self` argument names the receiver, not the
|
|
7775
|
+
# enclosing class, so a call with one stays a taint. A constant receiver names the same class inside
|
|
7776
|
+
# `class << self`, so the walk's singleton flag does not matter here.
|
|
7777
|
+
def receiver_extend_recorded?(node, kind, qualified_prefix, accumulator)
|
|
7778
|
+
return false if kind != :extend
|
|
7779
|
+
return false if (node.arguments&.arguments || []).any?(Prism::SelfNode)
|
|
7780
|
+
|
|
7781
|
+
owner = prepend_call_receiver(node, qualified_prefix)
|
|
7782
|
+
return false if owner.nil?
|
|
7783
|
+
|
|
7784
|
+
!record_extend_targets(node, owner, accumulator).empty?
|
|
7785
|
+
end
|
|
7786
|
+
|
|
7667
7787
|
# The table stores search order, not call order: a later `extend` statement prepends its module
|
|
7668
7788
|
# (`extend A; extend B` → singleton ancestors `[B, A]`) while the arguments of a single
|
|
7669
7789
|
# `extend A, B` keep call order (`[A, B]`). Prepending each call's argument list preserves both,
|
|
@@ -7895,6 +8015,16 @@ module Rigor
|
|
|
7895
8015
|
end
|
|
7896
8016
|
end
|
|
7897
8017
|
|
|
8018
|
+
# Issue #2013 — contests the instance slot of `name`, recorded or not, and marks the contest as a
|
|
8019
|
+
# replacement's ({ScopeIndexer.replaced_visibility_mark}): a `define_method(:name)` or an alias gave the name
|
|
8020
|
+
# a visibility the walk does not read, and another file may record the slot. A later certain write in this
|
|
8021
|
+
# walk clears the contest ({#wrote}) and leaves the mark, which then only keeps a contest another file puts on
|
|
8022
|
+
# the slot: the fold declines more, never reports more, and file order is not load order there either.
|
|
8023
|
+
def contest_replaced(class_name, name)
|
|
8024
|
+
contest_slot(class_name, name)
|
|
8025
|
+
contest_slot(class_name, ScopeIndexer.replaced_visibility_mark(name))
|
|
8026
|
+
end
|
|
8027
|
+
|
|
7898
8028
|
# Contests the `[class_name, name]` slot, recorded or not: a visibility call changed it in a way the walk
|
|
7899
8029
|
# cannot read (`private def self.x` reaching an instance `x`).
|
|
7900
8030
|
def contest_slot(class_name, name)
|
|
@@ -8231,6 +8361,12 @@ module Rigor
|
|
|
8231
8361
|
# them public (`K.public_class_method(*names)`, `K.singleton_class.class_eval { … }`).
|
|
8232
8362
|
SINGLETON_WILDCARD_SLOT = %i[singleton *].freeze
|
|
8233
8363
|
|
|
8364
|
+
# Issue #2013 — the mark a contested instance slot `name` carries in `contested_discovered_method_visibilities`
|
|
8365
|
+
# when a `define_method(:name)` or an alias replaced the method: `[:replaced, name]`. No reader asks for it; it
|
|
8366
|
+
# keeps the slot's contest when another file's write of the slot folds later ({#fold_visibility_contests}),
|
|
8367
|
+
# since file order is not load order.
|
|
8368
|
+
def self.replaced_visibility_mark(name) = [:replaced, name].freeze
|
|
8369
|
+
|
|
8234
8370
|
# Issue #1984 — a class method visibility call written on a constant (`K.public_class_method(:x)`,
|
|
8235
8371
|
# `K.send(:public_class_method, :x)`), anywhere: each literal name is recorded public on every class the
|
|
8236
8372
|
# constant may name, so a reopening's or another file's `private_class_method` disagrees with it and is
|
|
@@ -8501,19 +8637,21 @@ module Rigor
|
|
|
8501
8637
|
Source::AliasNames.alias_method_call_names(call_node)&.first(1) || []
|
|
8502
8638
|
end
|
|
8503
8639
|
|
|
8504
|
-
# Issue #1989 — a `define_method` or an alias that replaces a method
|
|
8505
|
-
#
|
|
8506
|
-
#
|
|
8507
|
-
#
|
|
8508
|
-
#
|
|
8640
|
+
# Issue #1989 — a `define_method` or an alias that replaces a method gives the name a visibility the walk
|
|
8641
|
+
# does not read (the running default for `define_method`, the old name's for an alias), so the slot is
|
|
8642
|
+
# contested rather than left asserting the replaced `def`'s. Issue #1984 — the slot is contested whether or
|
|
8643
|
+
# not this file recorded it, since another file may have; issue #2013 — on the instance side too, with the
|
|
8644
|
+
# mark that keeps the contest when that file's write folds later ({VisibilityTable#contest_replaced}).
|
|
8509
8645
|
def contest_replaced_visibility(names, qualified_prefix, accumulator, side: :instance)
|
|
8510
8646
|
return if names.empty? || qualified_prefix.empty?
|
|
8511
8647
|
|
|
8512
8648
|
class_name = qualified_prefix.join("::")
|
|
8513
|
-
recorded = accumulator[class_name]
|
|
8514
8649
|
names.each do |name|
|
|
8515
|
-
|
|
8516
|
-
|
|
8650
|
+
if side == :singleton
|
|
8651
|
+
accumulator.contest_slot(class_name, visibility_slot(name, side))
|
|
8652
|
+
else
|
|
8653
|
+
accumulator.contest_replaced(class_name, name)
|
|
8654
|
+
end
|
|
8517
8655
|
end
|
|
8518
8656
|
end
|
|
8519
8657
|
|
|
@@ -8754,48 +8892,76 @@ module Rigor
|
|
|
8754
8892
|
#
|
|
8755
8893
|
# ADR-119 WD3 — the alias is the slot's writer: the new name's slot is contested when the alias is possible
|
|
8756
8894
|
# or the slot it copies is contested, and cleared otherwise.
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
8895
|
+
#
|
|
8896
|
+
# Issue #2006 — in source order. An alias copies what its old name held AT the alias, read from the walk's
|
|
8897
|
+
# definer history ({MethodTables#wrote_definer}) and the copies earlier aliases made: a `def` of the old name
|
|
8898
|
+
# written after the alias (`alias old_x x; def x = 1`) is not what `old_x` runs, and nothing before the alias
|
|
8899
|
+
# copies nothing. The new name's slot then follows its last writer: a later `def` keeps the slot the walk
|
|
8900
|
+
# gave it, and a later `define_method` / `attr_*` leaves the copy contested, as {#supersede_def_nodes} leaves
|
|
8901
|
+
# a replaced `def`.
|
|
8902
|
+
def apply_alias_def_nodes(root, accumulator, tables)
|
|
8903
|
+
alias_map = collect_class_alias_map(root, [], AliasMap.new.track(tables.certainty))
|
|
8904
|
+
history = (tables.definer_history || {}).transform_values(&:dup)
|
|
8905
|
+
alias_map.copy_into(history).each { |key| settle_alias_slot(accumulator, tables, key, history[key]) }
|
|
8906
|
+
end
|
|
8907
|
+
|
|
8908
|
+
# Writes the new name's slot from its last writer `writes.last` ({#apply_alias_def_nodes}): an alias copy or a
|
|
8909
|
+
# later `def` writes its `def` with its certainty (for the `def`, what the walk wrote already). A last writer
|
|
8910
|
+
# with no `def` this file holds — a later `define_method` / `attr_*`, or an alias that copies nothing this file
|
|
8911
|
+
# defines (an inherited or core method, an attr reader, a name it cannot read) — contests the slot, which keeps
|
|
8912
|
+
# what it holds or else the latest copy, so no sibling file's `def` fills it.
|
|
8913
|
+
def settle_alias_slot(accumulator, tables, (class_name, new_name), writes)
|
|
8914
|
+
last = writes.last
|
|
8915
|
+
slot = (accumulator[class_name] ||= {})
|
|
8916
|
+
if last[1]
|
|
8917
|
+
slot[new_name] = last[1]
|
|
8918
|
+
follow_slot_writer(tables, :contested_def_nodes, [class_name, new_name], last[2])
|
|
8919
|
+
else
|
|
8920
|
+
copy = writes.rfind { |write| write[1] }
|
|
8921
|
+
slot[new_name] ||= copy[1] if copy
|
|
8922
|
+
follow_slot_writer(tables, :contested_def_nodes, [class_name, new_name], false)
|
|
8774
8923
|
end
|
|
8775
8924
|
end
|
|
8776
8925
|
|
|
8777
|
-
# The `{class => {new name => old name}}` map {#collect_class_alias_map} fills, which also
|
|
8778
|
-
#
|
|
8926
|
+
# The `{class => {new name => old name}}` map {#collect_class_alias_map} fills, which also lists every alias it
|
|
8927
|
+
# met as an event `[class, new name, old name, start offset, possible?]` ({Certainty}).
|
|
8779
8928
|
class AliasMap < Hash
|
|
8929
|
+
attr_reader :events
|
|
8930
|
+
|
|
8780
8931
|
def track(certainty)
|
|
8781
8932
|
@certainty = certainty || Certainty::EMPTY
|
|
8782
|
-
@
|
|
8933
|
+
@events = []
|
|
8783
8934
|
self
|
|
8784
8935
|
end
|
|
8785
8936
|
|
|
8786
|
-
# Records that `node` wrote the `[class_name, new_name]` entry
|
|
8787
|
-
def wrote(class_name, new_name, node)
|
|
8937
|
+
# Records that `node` wrote the `[class_name, new_name]` entry, aliasing `old_name`.
|
|
8938
|
+
def wrote(class_name, new_name, old_name, node)
|
|
8788
8939
|
return if @certainty.nil?
|
|
8789
8940
|
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
|
|
8941
|
+
@events << [class_name, new_name, old_name, node.location.start_offset, Certainty.possible?(@certainty, node)]
|
|
8942
|
+
end
|
|
8943
|
+
|
|
8944
|
+
# Issue #2006 — adds every alias to `history` (`{[class, name] => [[offset, def node or nil, certain,
|
|
8945
|
+
# (:alias)], ...]}`, ascending), in source order. An alias whose old name held a `def` at the alias copies it,
|
|
8946
|
+
# certain when that write and the alias are. Any other alias — an old name with no `def` of this file before
|
|
8947
|
+
# it (inherited, core, an attr reader, a sibling file's), or one it cannot read — adds a write with no `def`,
|
|
8948
|
+
# which leaves the slot contested ({ScopeIndexer#settle_alias_slot}). An alias whose new name it cannot read
|
|
8949
|
+
# adds one to every name of the class written before it. Returns the `[class, name]` keys it wrote.
|
|
8950
|
+
def copy_into(history)
|
|
8951
|
+
@events.sort_by { |event| event[3] }.flat_map do |class_name, new_name, old_name, at, possible|
|
|
8952
|
+
source = old_name && history[[class_name, old_name]]&.rfind { |write| write.first < at }
|
|
8953
|
+
write = if source&.[](1) then [at, source[1], source[2] && !possible, :alias].freeze
|
|
8954
|
+
else [at, nil, false, :alias].freeze
|
|
8955
|
+
end
|
|
8956
|
+
keys = new_name ? [[class_name, new_name]] : names_before(history, class_name, at)
|
|
8957
|
+
keys.each { |key| (history[key] ||= []).push(write).sort_by!(&:first) }
|
|
8958
|
+
end.uniq
|
|
8795
8959
|
end
|
|
8796
8960
|
|
|
8797
|
-
|
|
8798
|
-
|
|
8961
|
+
private
|
|
8962
|
+
|
|
8963
|
+
def names_before(history, class_name, at)
|
|
8964
|
+
history.filter_map { |key, writes| key if key.first == class_name && writes.first.first < at }
|
|
8799
8965
|
end
|
|
8800
8966
|
end
|
|
8801
8967
|
|
|
@@ -8857,12 +9023,19 @@ module Rigor
|
|
|
8857
9023
|
# `t` alias was the corpus case). Unlike AliasMethodNode a call's children can carry further
|
|
8858
9024
|
# class bodies (`Class.new do … end`), so the walk continues below it either way.
|
|
8859
9025
|
names = Source::AliasNames.alias_method_call_names(node)
|
|
8860
|
-
if names && !rec_prefix.empty? && !(singleton_cref && leaf_owner.nil?)
|
|
9026
|
+
if (names || unread_alias_method?(node)) && !rec_prefix.empty? && !(singleton_cref && leaf_owner.nil?)
|
|
8861
9027
|
record_alias_call_entry(accumulator, rec_prefix.join("::"), names, node)
|
|
8862
9028
|
end
|
|
8863
9029
|
false
|
|
8864
9030
|
end
|
|
8865
9031
|
|
|
9032
|
+
# Issue #2006 — an `alias_method` call on no receiver or `self` whose names {Source::AliasNames} cannot read
|
|
9033
|
+
# (`alias_method n, :to_s`, `self.alias_method :x, :y`, a splat): it still replaces a method, which
|
|
9034
|
+
# {AliasMap#copy_into} contests.
|
|
9035
|
+
def unread_alias_method?(node)
|
|
9036
|
+
node.name == :alias_method && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
9037
|
+
end
|
|
9038
|
+
|
|
8866
9039
|
# The rebinding-block call arm of {#collect_class_alias_map}: an anonymous meta-new call
|
|
8867
9040
|
# (`Class.new { … }` no write names) files its block's aliases nowhere, and an eval-family
|
|
8868
9041
|
# call binds them on the receiver — a nameable receiver re-anchors the owner while a
|
|
@@ -8953,20 +9126,32 @@ module Rigor
|
|
|
8953
9126
|
end
|
|
8954
9127
|
|
|
8955
9128
|
def record_alias_call_entry(accumulator, class_name, names, node)
|
|
9129
|
+
if names.nil?
|
|
9130
|
+
# Issue #2006 — an unread `alias_method` maps nothing, but still replaces a method ({AliasMap#copy_into}).
|
|
9131
|
+
new_name = Source::AliasNames.literal_name(node.arguments&.arguments&.first)
|
|
9132
|
+
accumulator.wrote(class_name, new_name, nil, node) if accumulator.is_a?(AliasMap)
|
|
9133
|
+
return
|
|
9134
|
+
end
|
|
9135
|
+
|
|
8956
9136
|
(accumulator[class_name] ||= {})[names.first] = names.last
|
|
8957
|
-
accumulator.wrote(class_name, names.first, node) if accumulator.is_a?(AliasMap)
|
|
9137
|
+
accumulator.wrote(class_name, names.first, names.last, node) if accumulator.is_a?(AliasMap)
|
|
8958
9138
|
end
|
|
8959
9139
|
|
|
8960
9140
|
def record_alias_map_entry(alias_node, qualified_prefix, accumulator)
|
|
8961
9141
|
return if qualified_prefix.empty?
|
|
8962
9142
|
|
|
8963
9143
|
names = Source::AliasNames.keyword_names(alias_node)
|
|
8964
|
-
return if names.nil?
|
|
8965
|
-
|
|
8966
9144
|
class_name = qualified_prefix.join("::")
|
|
9145
|
+
if names.nil?
|
|
9146
|
+
# Issue #2006 — an `alias` whose names are not plain symbols still replaces a method ({AliasMap#copy_into}).
|
|
9147
|
+
new_name = Source::AliasNames.symbol_name(alias_node.new_name)
|
|
9148
|
+
accumulator.wrote(class_name, new_name, nil, alias_node) if accumulator.is_a?(AliasMap)
|
|
9149
|
+
return
|
|
9150
|
+
end
|
|
9151
|
+
|
|
8967
9152
|
new_name, old_name = names
|
|
8968
9153
|
(accumulator[class_name] ||= {})[new_name] = old_name
|
|
8969
|
-
accumulator.wrote(class_name, new_name, alias_node) if accumulator.is_a?(AliasMap)
|
|
9154
|
+
accumulator.wrote(class_name, new_name, old_name, alias_node) if accumulator.is_a?(AliasMap)
|
|
8970
9155
|
end
|
|
8971
9156
|
|
|
8972
9157
|
def record_define_method(call_node, qualified_prefix, in_singleton_class, accumulator)
|
|
@@ -9153,14 +9338,15 @@ module Rigor
|
|
|
9153
9338
|
# {#discovered_project_index_incremental}'s changed-file branch) yields the live def nodes the signature
|
|
9154
9339
|
# reads their parameter structure from. Issue #1120 — and each file's own refinement table, which the merged
|
|
9155
9340
|
# def-index cannot attribute back to a file, so the session can diff it against the file's seed bundle.
|
|
9156
|
-
# Issue #1459 — and each file's {#defined_method_names}, diffed the same way.
|
|
9157
|
-
#
|
|
9341
|
+
# Issue #1459 — and each file's {#defined_method_names}, diffed the same way. Issue #2134 — and each file's
|
|
9342
|
+
# {#visibility_entries}. Issue #2158 — and each file's `global_write_census`.
|
|
9343
|
+
# @return `{ def_index:, code_fingerprints:, declaration_signatures:, refinements:, defined_method_names:,
|
|
9344
|
+
# visibility_entries:, global_write_census: }`.
|
|
9158
9345
|
def scan_summary_for_paths(paths, buffer: nil)
|
|
9159
9346
|
acc = new_def_index_accumulator
|
|
9160
9347
|
code_fingerprints = {}
|
|
9161
9348
|
declaration_signatures = {}
|
|
9162
|
-
|
|
9163
|
-
defined_method_names = {}
|
|
9349
|
+
per_file = { refinements: {}, defined_method_names: {}, visibility_entries: {}, global_write_census: {} }
|
|
9164
9350
|
paths.each do |path|
|
|
9165
9351
|
physical = buffer ? buffer.resolve(path) : path
|
|
9166
9352
|
source = File.read(physical)
|
|
@@ -9169,16 +9355,21 @@ module Rigor
|
|
|
9169
9355
|
fold_file_index(acc, file_index)
|
|
9170
9356
|
code_fingerprints[path] = code_fingerprint(source, parsed.comments)
|
|
9171
9357
|
declaration_signatures[path] = declaration_signature(file_index)
|
|
9172
|
-
|
|
9173
|
-
defined_method_names[path] = defined_method_names(file_index)
|
|
9358
|
+
file_summary(file_index).each { |key, table| per_file[key][path] = table unless table.nil? }
|
|
9174
9359
|
rescue DeclarationWalk::ContractError
|
|
9175
9360
|
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
9176
9361
|
rescue StandardError
|
|
9177
9362
|
next
|
|
9178
9363
|
end
|
|
9179
9364
|
{ def_index: finalize_def_index(acc), code_fingerprints: code_fingerprints,
|
|
9180
|
-
declaration_signatures: declaration_signatures,
|
|
9181
|
-
|
|
9365
|
+
declaration_signatures: declaration_signatures, **per_file }
|
|
9366
|
+
end
|
|
9367
|
+
|
|
9368
|
+
# One file's tables {#scan_summary_for_paths} hands the session to diff against the file's seed bundle (nil for
|
|
9369
|
+
# a table the file does not have).
|
|
9370
|
+
def file_summary(file_index)
|
|
9371
|
+
{ refinements: file_index[:refinements], defined_method_names: defined_method_names(file_index),
|
|
9372
|
+
visibility_entries: visibility_entries(file_index), global_write_census: file_index[:global_write_census] }
|
|
9182
9373
|
end
|
|
9183
9374
|
|
|
9184
9375
|
# Issue #1459 — every method name one file's index defines, on any class or module and either side: the keys of
|
|
@@ -9192,9 +9383,39 @@ module Rigor
|
|
|
9192
9383
|
%i[methods def_nodes singleton_def_nodes].each do |key|
|
|
9193
9384
|
file_index[key]&.each_value { |table| names.merge(table.keys) }
|
|
9194
9385
|
end
|
|
9386
|
+
# Issue #2184 — a named deferred-range row with no side (a `def` in a block, a `define_singleton_method`) is a
|
|
9387
|
+
# definition no table above holds.
|
|
9388
|
+
file_index[:deferred_ranges]&.each_value { |rows| rows.each { |row| names << row[2] if row[2] && row[3].nil? } }
|
|
9195
9389
|
names.freeze
|
|
9196
9390
|
end
|
|
9197
9391
|
|
|
9392
|
+
# Issue #2134 — one file's contributions to the visibility table and its contested sibling, as a Set of
|
|
9393
|
+
# `[class, slot, visibility]` entries and `[:contested, class, slot]` contests, where a slot is a method name or
|
|
9394
|
+
# `[:singleton, name]`. Takes a live file index or a seed bundle, which hold the same keys.
|
|
9395
|
+
#
|
|
9396
|
+
# Issue #2161 — and a `[:module_function, owner, name]` entry for each of the file's deferred-range rows that
|
|
9397
|
+
# `DefinerResolution.module_function_row?` counts (a named def row whose kind is not `:instance`): a module
|
|
9398
|
+
# function's singleton copy is public whatever its instance slot says, so the row is a visibility fact the
|
|
9399
|
+
# table itself does not hold.
|
|
9400
|
+
def visibility_entries(file_index)
|
|
9401
|
+
entries = Set.new
|
|
9402
|
+
file_index[:method_visibilities]&.each do |class_name, table|
|
|
9403
|
+
table.each { |slot, visibility| entries << [class_name, slot, visibility] }
|
|
9404
|
+
end
|
|
9405
|
+
contested = (file_index[:siblings] || {})[:contested_discovered_method_visibilities]
|
|
9406
|
+
contested&.each { |(class_name, slot)| entries << [:contested, class_name, slot] }
|
|
9407
|
+
add_module_function_entries(file_index[:deferred_ranges], entries)
|
|
9408
|
+
entries.freeze
|
|
9409
|
+
end
|
|
9410
|
+
|
|
9411
|
+
def add_module_function_entries(deferred_ranges, entries)
|
|
9412
|
+
deferred_ranges&.each_value do |rows|
|
|
9413
|
+
rows.each do |(_start, _finish, name, kind, owner)|
|
|
9414
|
+
entries << [:module_function, owner, name] if name && owner && kind != :instance
|
|
9415
|
+
end
|
|
9416
|
+
end
|
|
9417
|
+
end
|
|
9418
|
+
|
|
9198
9419
|
# ADR-89 WD1 — a per-file digest of every cross-file DECLARATION surface an ancestry / file-level
|
|
9199
9420
|
# dependent consumes, EXCLUDING method bodies (which only symbol dependents consume, via the ADR-46
|
|
9200
9421
|
# symbol edges the change-detection already fingerprints per method). A body edit that touches no
|
|
@@ -9412,14 +9633,21 @@ module Rigor
|
|
|
9412
9633
|
# Error degradation is identical to the two independent loops it replaces: a read / parse failure (the
|
|
9413
9634
|
# rescue's real target) contributes nothing to either table. The subset-scoped callers
|
|
9414
9635
|
# ({IncrementalSession}, `coverage --protection`) keep calling the individual methods unchanged.
|
|
9415
|
-
|
|
9636
|
+
#
|
|
9637
|
+
# @param uncertain — issue #2021: the paths whose every method contribution is `possible`
|
|
9638
|
+
# ({Certainty.every_contribution}).
|
|
9639
|
+
def discovered_project_index_for_paths(paths, buffer: nil, uncertain: EMPTY_PATH_SET)
|
|
9416
9640
|
classes = {}
|
|
9417
9641
|
acc = new_def_index_accumulator
|
|
9418
9642
|
paths.each do |path|
|
|
9419
9643
|
physical = buffer ? buffer.resolve(path) : path
|
|
9420
9644
|
root = Prism.parse(File.read(physical), filepath: path).value
|
|
9421
9645
|
collect_class_decls(root, [], classes)
|
|
9422
|
-
|
|
9646
|
+
if uncertain.include?(path)
|
|
9647
|
+
fold_file_index(acc, without_written_slots(build_file_index(path, root, uncertain: true), acc))
|
|
9648
|
+
else
|
|
9649
|
+
accumulate_project_index(acc, path, root)
|
|
9650
|
+
end
|
|
9423
9651
|
rescue DeclarationWalk::ContractError
|
|
9424
9652
|
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
9425
9653
|
rescue StandardError
|
|
@@ -9467,14 +9695,19 @@ module Rigor
|
|
|
9467
9695
|
# @param paths — project file paths, in canonical order.
|
|
9468
9696
|
# @param seed_bundles — the prior run's per-file bundles, keyed by logical path.
|
|
9469
9697
|
# @return `{ classes:, def_index:, bundles: }`.
|
|
9470
|
-
|
|
9698
|
+
# @param uncertain — issue #2021: as {#discovered_project_index_for_paths}. Such a file is always re-walked and
|
|
9699
|
+
# keeps no bundle, since a bundle does not record that its contributions were all possible.
|
|
9700
|
+
def discovered_project_index_incremental(paths, seed_bundles:, buffer: nil, uncertain: EMPTY_PATH_SET) # rubocop:disable Metrics/AbcSize
|
|
9471
9701
|
classes = {}
|
|
9472
9702
|
acc = new_def_index_accumulator
|
|
9473
9703
|
bundles = {}
|
|
9474
9704
|
paths.each do |path|
|
|
9475
9705
|
physical = buffer ? buffer.resolve(path) : path
|
|
9476
9706
|
digest = Cache::FileDigest.hexdigest(physical)
|
|
9477
|
-
|
|
9707
|
+
walked = uncertain.include?(path)
|
|
9708
|
+
# A walked file neither reuses nor stores a bundle. Both are guards no spec separates: no bundle is ever
|
|
9709
|
+
# stored for a walked file, so the reuse guard matters only if a path changes role within one session.
|
|
9710
|
+
cached = walked ? nil : seed_bundles[path]
|
|
9478
9711
|
if cached && cached[:digest] == digest
|
|
9479
9712
|
bundles[path] = cached
|
|
9480
9713
|
classes.merge!(cached[:classes])
|
|
@@ -9483,13 +9716,13 @@ module Rigor
|
|
|
9483
9716
|
source = File.read(physical)
|
|
9484
9717
|
parsed = Prism.parse(source, filepath: path)
|
|
9485
9718
|
root = parsed.value
|
|
9486
|
-
file_index = build_file_index(path, root)
|
|
9719
|
+
file_index = build_file_index(path, root, uncertain: walked)
|
|
9487
9720
|
file_classes = {}
|
|
9488
9721
|
collect_class_decls(root, [], file_classes)
|
|
9489
|
-
|
|
9490
|
-
|
|
9722
|
+
fingerprint = code_fingerprint(source, parsed.comments)
|
|
9723
|
+
bundles[path] = build_seed_bundle(file_index, file_classes, digest, fingerprint) unless walked
|
|
9491
9724
|
classes.merge!(file_classes)
|
|
9492
|
-
fold_file_index(acc, file_index)
|
|
9725
|
+
fold_file_index(acc, walked ? without_written_slots(file_index, acc) : file_index)
|
|
9493
9726
|
end
|
|
9494
9727
|
rescue DeclarationWalk::ContractError
|
|
9495
9728
|
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
@@ -9503,12 +9736,43 @@ module Rigor
|
|
|
9503
9736
|
# Builds ONE file's isolated def-index contribution (live `Prism::DefNode`s) by folding it into a fresh
|
|
9504
9737
|
# accumulator — recon Q2(a)'s "constructible in isolation" property. Shared by the cold-baseline bundle
|
|
9505
9738
|
# build and the incremental changed-file re-walk.
|
|
9506
|
-
def build_file_index(path, root)
|
|
9739
|
+
def build_file_index(path, root, uncertain: false)
|
|
9507
9740
|
file_acc = new_def_index_accumulator
|
|
9508
|
-
accumulate_project_index(file_acc, path, root)
|
|
9741
|
+
accumulate_project_index(file_acc, path, root, uncertain: uncertain)
|
|
9509
9742
|
file_acc
|
|
9510
9743
|
end
|
|
9511
9744
|
|
|
9745
|
+
# Issue #2021 — the single-valued slot tables an `uncertain` file writes, each with its contest sibling.
|
|
9746
|
+
UNCERTAIN_SLOT_TABLES = {
|
|
9747
|
+
def_nodes: :contested_discovered_def_nodes,
|
|
9748
|
+
singleton_def_nodes: :contested_discovered_singleton_def_nodes,
|
|
9749
|
+
method_visibilities: :contested_discovered_method_visibilities,
|
|
9750
|
+
parameter_envelopes: :contested_discovered_parameter_envelopes
|
|
9751
|
+
}.freeze
|
|
9752
|
+
|
|
9753
|
+
# Issue #2021 — an `uncertain` file's isolated index without the slots `acc` already holds. The walked files fold
|
|
9754
|
+
# after every other discovered file, so `acc` holds what the configured (certain) files wrote: a walked reopening
|
|
9755
|
+
# of such a class adds the names it defines (existence) and leaves each written def node, visibility and
|
|
9756
|
+
# envelope, and its contest, as the certain writer left it. Without this, the walked file's possible write was
|
|
9757
|
+
# the slot's last and contested it, and `rigor check lib/user.rb plug/x.rb` withheld what `rigor check
|
|
9758
|
+
# lib/user.rb` reports. A slot no earlier file wrote is taken, contested.
|
|
9759
|
+
def without_written_slots(file_index, acc)
|
|
9760
|
+
siblings = (file_index[:siblings] || {}).dup
|
|
9761
|
+
tables = UNCERTAIN_SLOT_TABLES.to_h do |table, sibling|
|
|
9762
|
+
written = acc[table]
|
|
9763
|
+
kept = {}
|
|
9764
|
+
(file_index[table] || {}).each do |class_name, entries|
|
|
9765
|
+
sitting = written[class_name]
|
|
9766
|
+
entries = entries.reject { |key, _| sitting.key?(key) } if sitting
|
|
9767
|
+
kept[class_name] = entries unless entries.empty?
|
|
9768
|
+
end
|
|
9769
|
+
contests = siblings[sibling]
|
|
9770
|
+
siblings[sibling] = contests.select { |class_name, key| kept[class_name]&.key?(key) }.to_set if contests
|
|
9771
|
+
[table, kept]
|
|
9772
|
+
end
|
|
9773
|
+
file_index.merge(tables).merge(siblings: siblings)
|
|
9774
|
+
end
|
|
9775
|
+
|
|
9512
9776
|
# ADR-85 WD2 — folds a single file's isolated def-index contribution into the cross-file accumulator,
|
|
9513
9777
|
# applying EXACTLY the merge semantics {#accumulate_project_index} applies incrementally (def_nodes
|
|
9514
9778
|
# later-wins, def_sources first-wins, includes / class_sources accumulate, everything else later-wins).
|
|
@@ -9645,9 +9909,14 @@ module Rigor
|
|
|
9645
9909
|
# Issue #1984 — a singleton slot (`[:singleton, name]`) does not follow its writer: file order is not load
|
|
9646
9910
|
# order, so two files that give it different visibilities (`private_class_method :x` here,
|
|
9647
9911
|
# `class << self; public :x; end` in a reopening) leave it contested, and a contest on it stays whichever
|
|
9648
|
-
# file folds later. The instance slots fold as before
|
|
9912
|
+
# file folds later. The instance slots fold as before, except (issue #2013) one whose contest carries a
|
|
9913
|
+
# replacement's mark ({ScopeIndexer.replaced_visibility_mark}): a `define_method` or an alias in one file and a
|
|
9914
|
+
# `private def` in another leave the visibility to load order, so that contest stays too.
|
|
9649
9915
|
def fold_visibility_contests(sitting, recorded, file_slots, file_contested)
|
|
9650
|
-
stale = sitting.select
|
|
9916
|
+
stale = sitting.select do |class_name, name|
|
|
9917
|
+
!name.is_a?(Array) && file_slots[class_name]&.key?(name) &&
|
|
9918
|
+
!sitting.include?([class_name, ScopeIndexer.replaced_visibility_mark(name)])
|
|
9919
|
+
end
|
|
9651
9920
|
disagreeing = singleton_visibility_disagreements(recorded, file_slots)
|
|
9652
9921
|
return sitting if stale.empty? && file_contested.empty? && disagreeing.empty?
|
|
9653
9922
|
|
|
@@ -10070,12 +10339,12 @@ module Rigor
|
|
|
10070
10339
|
# Folds one file's class-keyed indexes into the cross-file accumulator. `method_visibilities` (ADR-35) is
|
|
10071
10340
|
# collected here so the override-visibility-reduced rule can read an ancestor's visibility declared in a sibling
|
|
10072
10341
|
# file.
|
|
10073
|
-
def accumulate_project_index(acc, path, root)
|
|
10342
|
+
def accumulate_project_index(acc, path, root, uncertain: false)
|
|
10074
10343
|
# One combined descent yields both the methods existence table and the def-node table; the latter is also
|
|
10075
10344
|
# consumed by `record_class_sources`, so a def-dense file is walked once here instead of three times (methods +
|
|
10076
10345
|
# def-nodes ×2). See {#build_methods_and_def_nodes}.
|
|
10077
10346
|
file_methods, file_def_nodes, file_envelopes, file_refinements, file_singleton_def_nodes, certainty =
|
|
10078
|
-
walk_def_contributions(acc, path, root)
|
|
10347
|
+
walk_def_contributions(acc, path, root, uncertain: uncertain)
|
|
10079
10348
|
merge_discovered_defs(acc[:def_nodes], acc[:def_sources], path, file_def_nodes)
|
|
10080
10349
|
fold_parameter_envelopes(acc, file_envelopes)
|
|
10081
10350
|
fold_refinements(acc, file_refinements)
|
|
@@ -10102,8 +10371,8 @@ module Rigor
|
|
|
10102
10371
|
# file's siblings folded into the accumulator BEFORE its members join it (ADR-119 WD3: the `possible` rule
|
|
10103
10372
|
# reads the accumulator's member as it stood). Returns `[methods, def_nodes, envelopes, refinements,
|
|
10104
10373
|
# singleton_def_nodes, certainty]`, the classification passed on for the visibility walk.
|
|
10105
|
-
def walk_def_contributions(acc, path, root)
|
|
10106
|
-
certainty = Certainty.possible_nodes(root)
|
|
10374
|
+
def walk_def_contributions(acc, path, root, uncertain: false)
|
|
10375
|
+
certainty = uncertain ? Certainty.every_contribution(root) : Certainty.possible_nodes(root)
|
|
10107
10376
|
file_methods, file_def_nodes, file_envelopes, file_refinements, file_siblings =
|
|
10108
10377
|
build_methods_and_def_nodes(root, path, certainty: certainty)
|
|
10109
10378
|
file_singleton_def_nodes, file_singleton_contested = build_singleton_def_tables(root, certainty: certainty)
|
|
@@ -10149,7 +10418,15 @@ module Rigor
|
|
|
10149
10418
|
census.writes.each do |name, descriptor|
|
|
10150
10419
|
(acc[:constant_writes][name] ||= {})[path] = descriptor
|
|
10151
10420
|
end
|
|
10152
|
-
acc[:global_write_census].merge(census.write_census.census)
|
|
10421
|
+
acc[:global_write_census].merge(attribute_own_writes(census.write_census.census, path))
|
|
10422
|
+
end
|
|
10423
|
+
|
|
10424
|
+
# #1154 — tags each `[:own_write, class, ivar]` entry with the file that records it, so a reader of the merged
|
|
10425
|
+
# census can tell a second file writing the same ivar of the same class (`UnseenIvarWriters`).
|
|
10426
|
+
def attribute_own_writes(census, path)
|
|
10427
|
+
return census unless census.any? { |entry| entry.first == :own_write }
|
|
10428
|
+
|
|
10429
|
+
census.to_set { |entry| entry.first == :own_write ? [*entry, path].freeze : entry }
|
|
10153
10430
|
end
|
|
10154
10431
|
|
|
10155
10432
|
# Folds one file's Data + Struct member-layout tables into the cross-file accumulator (kept out of
|
|
@@ -10272,8 +10549,12 @@ module Rigor
|
|
|
10272
10549
|
singleton_cref: false)
|
|
10273
10550
|
return unless node.is_a?(Prism::Node)
|
|
10274
10551
|
|
|
10552
|
+
# #1154 — the class whose instance `def`s the ivar-writer census credits; none under `class << self`.
|
|
10553
|
+
ivar_owner = census_ivar_owner(qualified_prefix, self_owner, singleton_cref)
|
|
10275
10554
|
case node
|
|
10276
10555
|
when Prism::SingletonClassNode
|
|
10556
|
+
# #1154 — a `class << self` inside an instance `def` opens the instance's singleton class.
|
|
10557
|
+
tables.write_census.visit(node, top_level: qualified_prefix.empty?, owner: ivar_owner)
|
|
10277
10558
|
return walk_census_singleton_class(node, qualified_prefix, tables, self_owner,
|
|
10278
10559
|
meta_owner, singleton_cref)
|
|
10279
10560
|
when Prism::ClassNode, Prism::ModuleNode
|
|
@@ -10281,7 +10562,7 @@ module Rigor
|
|
|
10281
10562
|
singleton_cref)
|
|
10282
10563
|
else
|
|
10283
10564
|
census_constant_write(node, qualified_prefix, tables, self_owner, singleton_cref: singleton_cref)
|
|
10284
|
-
tables.write_census.visit(node, top_level: qualified_prefix.empty
|
|
10565
|
+
tables.write_census.visit(node, top_level: qualified_prefix.empty?, owner: ivar_owner)
|
|
10285
10566
|
end
|
|
10286
10567
|
|
|
10287
10568
|
rebound = rebound_block_self(node, qualified_prefix, nil, meta_owner, self_owner)
|
|
@@ -10313,6 +10594,16 @@ module Rigor
|
|
|
10313
10594
|
end
|
|
10314
10595
|
end
|
|
10315
10596
|
|
|
10597
|
+
# #1154 — the class whose instance `def`s the ivar-writer census credits: the class a `Struct.new` /
|
|
10598
|
+
# `Data.define` / `Class.new` constant or a `K.class_eval` block opens (`self_owner`), else the lexical class;
|
|
10599
|
+
# none under `class << self` or an opaque block. The prefix Array is joined only when an entry is recorded.
|
|
10600
|
+
def census_ivar_owner(qualified_prefix, self_owner, singleton_cref)
|
|
10601
|
+
return self_owner if self_owner.is_a?(String)
|
|
10602
|
+
return nil if self_owner || singleton_cref || qualified_prefix.empty?
|
|
10603
|
+
|
|
10604
|
+
qualified_prefix
|
|
10605
|
+
end
|
|
10606
|
+
|
|
10316
10607
|
# Under an unnameable cref a bare/`self::` header pushes `#<singleton>::Name` — still
|
|
10317
10608
|
# unnameable — so the body keeps the OPAQUE self and the cref flag; nameable
|
|
10318
10609
|
# headers re-anchor at a real cref.
|
|
@@ -11400,6 +11691,7 @@ module Rigor
|
|
|
11400
11691
|
table[node] = parent_scope
|
|
11401
11692
|
parent_scope
|
|
11402
11693
|
end
|
|
11694
|
+
evaluate_unentered_flow(node, table, current_scope)
|
|
11403
11695
|
|
|
11404
11696
|
case node
|
|
11405
11697
|
when Prism::IfNode
|
|
@@ -11415,7 +11707,7 @@ module Rigor
|
|
|
11415
11707
|
child_scope = entered ? current_scope : closure_scope(node, current_scope)
|
|
11416
11708
|
node.rigor_each_child { |child| propagate(child, table, child_scope) }
|
|
11417
11709
|
when Prism::CallNode
|
|
11418
|
-
propagate_call(node, table, current_scope)
|
|
11710
|
+
propagate_call(node, table, current_scope, recorded: recorded)
|
|
11419
11711
|
when Prism::RescueNode, Prism::EnsureNode
|
|
11420
11712
|
# Issue #1360 — a clause the evaluator did not enter (a `begin` in a value position) reads `$!`, `$@` and
|
|
11421
11713
|
# `$?` unbound, not the enclosing clause's; one it entered keeps what it recorded.
|
|
@@ -11429,6 +11721,84 @@ module Rigor
|
|
|
11429
11721
|
end
|
|
11430
11722
|
end
|
|
11431
11723
|
|
|
11724
|
+
# Issues #2196 and #2056 — the constructs whose parts run under a narrowing their own condition or left operand
|
|
11725
|
+
# makes, keyed to the part {StatementEvaluator} records first when it enters one. Issue #2214 — a `&&=` on a
|
|
11726
|
+
# variable runs its value under the variable's truthy narrowing (`[c &&= c + 1]`).
|
|
11727
|
+
UNENTERED_FLOW_ENTRY = {
|
|
11728
|
+
Prism::AndNode => :left, Prism::OrNode => :left,
|
|
11729
|
+
Prism::IfNode => :predicate, Prism::UnlessNode => :predicate,
|
|
11730
|
+
Prism::WhileNode => :predicate, Prism::UntilNode => :predicate,
|
|
11731
|
+
Prism::LocalVariableAndWriteNode => :value, Prism::InstanceVariableAndWriteNode => :value,
|
|
11732
|
+
Prism::ClassVariableAndWriteNode => :value, Prism::GlobalVariableAndWriteNode => :value,
|
|
11733
|
+
Prism::CaseNode => :predicate
|
|
11734
|
+
}.freeze
|
|
11735
|
+
private_constant :UNENTERED_FLOW_ENTRY
|
|
11736
|
+
|
|
11737
|
+
UNENTERED_EVALUATOR_KEY = :rigor_scope_index_unentered_evaluator
|
|
11738
|
+
private_constant :UNENTERED_EVALUATOR_KEY
|
|
11739
|
+
|
|
11740
|
+
# Issues #2196 and #2056 — an `&&` / `||` / `and` / `or`, an `if` / `unless` / ternary or a `while` / `until`
|
|
11741
|
+
# in a value position (an array element, a hash value, a call argument, an interpolation, a multi-value
|
|
11742
|
+
# `return`) is typed by `ExpressionTyper` and never entered, so no part of it was recorded and this walk handed
|
|
11743
|
+
# every part one scope. A walk that narrows by the condition's edges cannot follow a write inside the narrowed
|
|
11744
|
+
# part (`[c && (c = c.size; c.even?)]`, `[x || (set!; @x.upcase)]`) nor one inside a block in an earlier
|
|
11745
|
+
# conjunct (`[x.nil? && [1].each { x = "d" } && x.upcase]`). Such a construct is evaluated here instead, from
|
|
11746
|
+
# the scope this walk reached it with, under the index's own recorder, so each part is recorded with the scope
|
|
11747
|
+
# the statement form records it with. What it does not visit (a dead arm) is walked as before.
|
|
11748
|
+
def evaluate_unentered_flow(node, table, current_scope)
|
|
11749
|
+
entry = UNENTERED_FLOW_ENTRY[node.class]
|
|
11750
|
+
return if entry.nil?
|
|
11751
|
+
|
|
11752
|
+
# An entered construct is already recorded; skipping it is a performance guard, not a behaviour. A `case`
|
|
11753
|
+
# without a subject records its first `when` test first.
|
|
11754
|
+
part = node.public_send(entry) || (node.conditions.first&.conditions&.first if node.is_a?(Prism::CaseNode))
|
|
11755
|
+
return if part.nil? || table.key?(part)
|
|
11756
|
+
|
|
11757
|
+
evaluate = Thread.current[UNENTERED_EVALUATOR_KEY]
|
|
11758
|
+
evaluate&.call(node, current_scope, code_body_nodes(node, current_scope.source_path))
|
|
11759
|
+
end
|
|
11760
|
+
|
|
11761
|
+
# The evaluation of an unentered construct starts with no class frame and no lexical nesting, which only a code
|
|
11762
|
+
# body observes: a `def`, a `class` / `module` / `class << …` body, and the block of a class-creating call
|
|
11763
|
+
# (`Class.new { … }`, `Struct.new`, `Data.define`, `Module.new`) or of a `refine`, whose `def`s and constants
|
|
11764
|
+
# resolve against the class the walk tracks and the evaluation does not. Every node in one is left out of the
|
|
11765
|
+
# recording, so the walk reaches it unrecorded and handles it as it did before #2196 ({#unentered_block_entry},
|
|
11766
|
+
# {#record_class_body_defs}).
|
|
11767
|
+
# Answers nil, allocating nothing, for a construct holding no code body: the common case.
|
|
11768
|
+
def code_body_nodes(node, source_path, acc = nil, inside: false)
|
|
11769
|
+
inside ||= CODE_BODY_NODES.include?(node.class)
|
|
11770
|
+
(acc ||= Set.new.compare_by_identity) << node if inside
|
|
11771
|
+
if !inside && node.is_a?(Prism::CallNode) && class_body_block_call?(node, source_path)
|
|
11772
|
+
acc = code_body_nodes(node.block, source_path, acc, inside: true)
|
|
11773
|
+
node.rigor_each_child do |child|
|
|
11774
|
+
acc = code_body_nodes(child, source_path, acc) unless child.equal?(node.block)
|
|
11775
|
+
end
|
|
11776
|
+
return acc
|
|
11777
|
+
end
|
|
11778
|
+
node.rigor_each_child { |child| acc = code_body_nodes(child, source_path, acc, inside: inside) }
|
|
11779
|
+
acc
|
|
11780
|
+
end
|
|
11781
|
+
|
|
11782
|
+
CODE_BODY_NODES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
11783
|
+
private_constant :CODE_BODY_NODES
|
|
11784
|
+
|
|
11785
|
+
def class_body_block_call?(node, source_path)
|
|
11786
|
+
node.block.is_a?(Prism::BlockNode) &&
|
|
11787
|
+
(!AnonymousMetaClass.name_for(node, source_path).nil? || !refine_target(node).nil?)
|
|
11788
|
+
end
|
|
11789
|
+
|
|
11790
|
+
# The evaluation {#evaluate_unentered_flow} runs for the walk of one {#index}, installed for that walk only.
|
|
11791
|
+
# Index walks nest (a rule may re-index a file while another file's walk is suspended), so the outer one is
|
|
11792
|
+
# restored on exit. A `propagate` called directly while an outer index walk is running would record into the
|
|
11793
|
+
# OUTER index's table through it; nothing outside {#index} calls `propagate`.
|
|
11794
|
+
def with_unentered_evaluator(evaluate)
|
|
11795
|
+
outer = Thread.current[UNENTERED_EVALUATOR_KEY]
|
|
11796
|
+
Thread.current[UNENTERED_EVALUATOR_KEY] = evaluate
|
|
11797
|
+
yield
|
|
11798
|
+
ensure
|
|
11799
|
+
Thread.current[UNENTERED_EVALUATOR_KEY] = outer
|
|
11800
|
+
end
|
|
11801
|
+
|
|
11432
11802
|
SELF_OPENING_BODIES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
11433
11803
|
private_constant :SELF_OPENING_BODIES
|
|
11434
11804
|
|
|
@@ -11463,11 +11833,11 @@ module Rigor
|
|
|
11463
11833
|
# argument one scope and threads none, so arguments that write keep the call's: an earlier one may rebind the
|
|
11464
11834
|
# receiver a later one reads (`return y&.concat((y = nil).to_s, y.upcase)`, where the evaluator threads no
|
|
11465
11835
|
# operand).
|
|
11466
|
-
def propagate_call(node, table, current_scope)
|
|
11836
|
+
def propagate_call(node, table, current_scope, recorded: false)
|
|
11467
11837
|
block = node.block
|
|
11468
11838
|
operands =
|
|
11469
11839
|
OperandEffects.any?(node.arguments) ? current_scope : Narrowing.safe_navigation_scope(node, current_scope)
|
|
11470
|
-
entry = unentered_block_entry(node, block, table, current_scope)
|
|
11840
|
+
entry = unentered_block_entry(node, block, table, current_scope, call_recorded: recorded)
|
|
11471
11841
|
if entry.equal?(current_scope) && operands.equal?(current_scope)
|
|
11472
11842
|
node.rigor_each_child { |child| propagate(child, table, current_scope) }
|
|
11473
11843
|
return
|
|
@@ -11491,7 +11861,7 @@ module Rigor
|
|
|
11491
11861
|
node.rigor_each_child { |child| propagate(child, table, child_scope) }
|
|
11492
11862
|
end
|
|
11493
11863
|
|
|
11494
|
-
def unentered_block_entry(node, block, table, current_scope)
|
|
11864
|
+
def unentered_block_entry(node, block, table, current_scope, call_recorded: false)
|
|
11495
11865
|
return current_scope unless block.is_a?(Prism::BlockNode) && !table.key?(block)
|
|
11496
11866
|
|
|
11497
11867
|
narrowing = unentered_block_narrowing(node, current_scope)
|
|
@@ -11504,6 +11874,12 @@ module Rigor
|
|
|
11504
11874
|
else
|
|
11505
11875
|
LastLine.block_entry(FreshFrameBlocks.closure_entry(current_scope, block, node), block, node)
|
|
11506
11876
|
end
|
|
11877
|
+
# Issue #2196 — a class-body block whose call the evaluator recorded but did not enter (one inside a construct
|
|
11878
|
+
# {#evaluate_unentered_flow} evaluated, or a later operand) starts from the scope the call was evaluated in,
|
|
11879
|
+
# which may narrow a captured local the body writes; this walk runs the body's statements from one scope, so
|
|
11880
|
+
# such a local reads `Dynamic[top]` in it: `[c && Class.new { c = c.size; c.even? }]`. A block whose call was
|
|
11881
|
+
# not recorded keeps the enclosing scope as before.
|
|
11882
|
+
entry = widen_condition_writes(block, entry) if call_recorded && class_body_block_call?(node, entry.source_path)
|
|
11507
11883
|
anonymous = AnonymousMetaClass.name_for(node, current_scope.source_path)
|
|
11508
11884
|
return unentered_meta_class_body_entry(block, entry, anonymous, table) if anonymous
|
|
11509
11885
|
|
|
@@ -11690,7 +12066,8 @@ module Rigor
|
|
|
11690
12066
|
# A test that calls a method may rebind an instance-variable or global subject before the body reads it
|
|
11691
12067
|
# (`when String then 1 when reset! then @x.upcase`, where `reset!` writes `@x`), and the walk cannot tell. From
|
|
11692
12068
|
# the first such test on, the subject reads `Dynamic[top]` on both edges ({#forget_called_subject}). The
|
|
11693
|
-
# evaluator
|
|
12069
|
+
# evaluator forgets a global subject there too (issue #2211). A `case` / `when` the evaluator did not enter is
|
|
12070
|
+
# evaluated ({#evaluate_unentered_flow}); this walk handles only a clause that evaluation does not visit.
|
|
11694
12071
|
def propagate_subject_case(node, table, current_scope)
|
|
11695
12072
|
subject = node.predicate
|
|
11696
12073
|
propagate(subject, table, current_scope)
|