rigortype 0.4.7 → 0.4.8
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 +3 -1
- data/data/core_overlay/bigdecimal.rbs +33 -0
- data/data/core_overlay/resolv.rbs +30 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
- data/data/gem_overlay/addressable/addressable.rbs +40 -0
- data/docs/handbook/01-getting-started.md +1 -1
- data/docs/handbook/11-sig-gen.md +35 -0
- data/docs/install.md +20 -0
- data/docs/manual/01-installation.md +67 -26
- data/docs/manual/02-cli-reference.md +307 -91
- data/docs/manual/03-configuration.md +41 -3
- data/docs/manual/04-diagnostics.md +5 -4
- data/docs/manual/08-skills.md +11 -0
- data/docs/manual/09-editor-integration.md +8 -0
- data/docs/manual/14-rails-quickstart.md +1 -1
- data/docs/manual/19-effect-labels.md +3 -4
- data/docs/manual/plugins/rigor-actionpack.md +15 -6
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
- data/docs/manual/plugins/rigor-sorbet.md +17 -3
- data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
- data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
- data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
- data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules.rb +598 -99
- data/lib/rigor/analysis/crash_signature.rb +43 -1
- data/lib/rigor/analysis/dependency_recorder.rb +233 -76
- data/lib/rigor/analysis/incremental.rb +11 -1
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +279 -37
- data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
- data/lib/rigor/analysis/rule_catalog.rb +37 -1
- data/lib/rigor/analysis/run_cache_key.rb +27 -3
- data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
- data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
- data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
- data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
- data/lib/rigor/analysis/runner.rb +222 -39
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
- data/lib/rigor/analysis/worker_session.rb +42 -41
- data/lib/rigor/bleeding_edge.rb +19 -4
- data/lib/rigor/cache/file_digest.rb +39 -31
- data/lib/rigor/cache/incremental_snapshot.rb +66 -7
- data/lib/rigor/cache/rbs_descriptor.rb +37 -2
- data/lib/rigor/cache/rbs_environment.rb +5 -1
- data/lib/rigor/cli/baseline_command.rb +15 -2
- data/lib/rigor/cli/check_command.rb +4 -2
- data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
- data/lib/rigor/cli/lens_command.rb +17 -2
- data/lib/rigor/cli/sig_gen_command.rb +8 -0
- data/lib/rigor/cli/skill_describe.rb +1 -0
- data/lib/rigor/cli.rb +5 -2
- data/lib/rigor/config_audit.rb +32 -18
- data/lib/rigor/configuration/severity_profile.rb +9 -0
- data/lib/rigor/configuration.rb +32 -5
- data/lib/rigor/effects/collector.rb +53 -21
- data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
- data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
- data/lib/rigor/environment/default_libraries.rb +17 -0
- data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
- data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
- data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
- data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
- data/lib/rigor/environment/rbs_loader.rb +312 -63
- data/lib/rigor/environment.rb +131 -35
- data/lib/rigor/inference/acceptance.rb +50 -15
- data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
- data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
- data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
- data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
- data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
- data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
- data/lib/rigor/inference/block_auto_splat.rb +1 -1
- data/lib/rigor/inference/block_run_count.rb +9 -2
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
- data/lib/rigor/inference/captured_locals.rb +2 -0
- data/lib/rigor/inference/census_foreign_block.rb +354 -0
- data/lib/rigor/inference/clause_widening.rb +1 -1
- data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
- data/lib/rigor/inference/construction_escape.rb +56 -15
- data/lib/rigor/inference/declaration_walk.rb +4 -0
- data/lib/rigor/inference/def_node_resolver.rb +32 -14
- data/lib/rigor/inference/def_return_typer.rb +1 -1
- data/lib/rigor/inference/definer_resolution.rb +15 -7
- data/lib/rigor/inference/definers_index.rb +72 -16
- data/lib/rigor/inference/error_info.rb +16 -0
- data/lib/rigor/inference/expression_typer.rb +1459 -287
- data/lib/rigor/inference/flow_tracer.rb +31 -14
- data/lib/rigor/inference/fork_map.rb +3 -0
- data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
- data/lib/rigor/inference/global_write_census.rb +8 -6
- data/lib/rigor/inference/in_effect_refinements.rb +7 -0
- data/lib/rigor/inference/indexed_narrowing.rb +57 -8
- data/lib/rigor/inference/ivar_writer_census.rb +106 -11
- data/lib/rigor/inference/key_presence_guard.rb +51 -25
- data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
- data/lib/rigor/inference/last_line.rb +5 -2
- data/lib/rigor/inference/loop_value_record.rb +63 -15
- data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
- data/lib/rigor/inference/match_rebinding.rb +21 -8
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
- data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
- data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
- data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
- data/lib/rigor/inference/method_dispatcher.rb +66 -18
- data/lib/rigor/inference/method_parameter_binder.rb +136 -56
- data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
- data/lib/rigor/inference/module_function_state.rb +13 -2
- data/lib/rigor/inference/multi_target_binder.rb +1 -1
- data/lib/rigor/inference/mutation_widening.rb +44 -4
- data/lib/rigor/inference/narrowing.rb +201 -27
- data/lib/rigor/inference/operand_effects.rb +43 -2
- data/lib/rigor/inference/optimistic_origin.rb +133 -8
- data/lib/rigor/inference/origin_lookup.rb +1 -1
- data/lib/rigor/inference/precision_scanner.rb +1 -1
- data/lib/rigor/inference/project_method_ownership.rb +28 -10
- data/lib/rigor/inference/project_patched_methods.rb +65 -13
- data/lib/rigor/inference/project_patched_scanner.rb +120 -25
- data/lib/rigor/inference/project_patched_visibility.rb +39 -3
- data/lib/rigor/inference/rbs_type_translator.rb +68 -15
- data/lib/rigor/inference/receiver_writer_call.rb +67 -0
- data/lib/rigor/inference/recursion_rebinding.rb +157 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
- data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
- data/lib/rigor/inference/refined_dispatch.rb +13 -0
- data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
- data/lib/rigor/inference/remover_holder_scan.rb +76 -22
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
- data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
- data/lib/rigor/inference/scope_indexer.rb +2079 -183
- data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
- data/lib/rigor/inference/self_preserving_block.rb +87 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
- data/lib/rigor/inference/singleton_object_constant.rb +32 -2
- data/lib/rigor/inference/statement_evaluator.rb +970 -250
- data/lib/rigor/inference/string_mutation.rb +14 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
- data/lib/rigor/inference/toplevel_def_census.rb +14 -6
- data/lib/rigor/inference/unknown_store_widening.rb +1 -1
- data/lib/rigor/inference/untyped_rejoin.rb +1 -1
- data/lib/rigor/inference/void_tail_summary.rb +29 -10
- data/lib/rigor/lens/claims/comparison.rb +78 -0
- data/lib/rigor/lens/claims/request.rb +131 -0
- data/lib/rigor/lens/claims/type_reader.rb +216 -0
- data/lib/rigor/lens/claims.rb +208 -0
- data/lib/rigor/lens/contract_reader.rb +5 -0
- data/lib/rigor/lens/document.rb +1 -1
- data/lib/rigor/lens/file_reader.rb +21 -8
- data/lib/rigor/lens/run.rb +8 -0
- data/lib/rigor/lens.rb +1 -1
- data/lib/rigor/library_probe.rb +68 -0
- data/lib/rigor/memo_audit.rb +282 -0
- data/lib/rigor/plugin/base.rb +15 -2
- data/lib/rigor/plugin/io_boundary.rb +25 -3
- data/lib/rigor/plugin/manifest.rb +7 -3
- data/lib/rigor/plugin/registry.rb +13 -2
- data/lib/rigor/plugin/trust_policy.rb +46 -0
- data/lib/rigor/protection/mutation_cache.rb +4 -1
- data/lib/rigor/reflection/constant_ancestors.rb +79 -22
- data/lib/rigor/reflection/declaration_collectors.rb +55 -0
- data/lib/rigor/reflection/declarations.rb +58 -0
- data/lib/rigor/reflection/project_method.rb +177 -0
- data/lib/rigor/reflection.rb +2 -0
- data/lib/rigor/rooted_glob.rb +18 -1
- data/lib/rigor/run_scoped_slots.rb +20 -20
- data/lib/rigor/scope/discovery_index.rb +149 -2
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
- data/lib/rigor/scope/resolution_chain.rb +900 -55
- data/lib/rigor/scope.rb +307 -34
- data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
- data/lib/rigor/sig_gen/classification.rb +14 -1
- data/lib/rigor/sig_gen/generator.rb +222 -13
- data/lib/rigor/sig_gen/method_candidate.rb +14 -0
- data/lib/rigor/sig_gen/observation_collector.rb +11 -2
- data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
- data/lib/rigor/sig_gen/writer.rb +21 -0
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/source/forwardable_delegators.rb +122 -0
- data/lib/rigor/source/node_children.rb +13 -0
- data/lib/rigor/type/combinator.rb +164 -8
- data/lib/rigor/type/interface.rb +60 -0
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_of_memo/serving.rb +99 -0
- data/lib/rigor/type_of_memo/stats.rb +43 -0
- data/lib/rigor/type_of_memo.rb +337 -0
- data/lib/rigor/typing_state.rb +1094 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
- data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
- data/sig/rigor/analysis/fact_store.rbs +2 -3
- data/sig/rigor/cache/descriptor.rbs +14 -0
- data/sig/rigor/environment.rbs +4 -1
- data/sig/rigor/plugin/base.rbs +53 -19
- data/sig/rigor/plugin/interfaces.rbs +51 -0
- data/sig/rigor/plugin/io_boundary.rbs +2 -3
- data/sig/rigor/plugin/node_context.rbs +21 -0
- data/sig/rigor/reflection.rbs +37 -0
- data/sig/rigor/scope.rbs +22 -3
- data/sig/rigor/trinary.rbs +1 -0
- data/sig/rigor/type.rbs +9 -3
- data/sig/rigor.rbs +3 -2
- data/skills/rigor-ask/SKILL.md +1 -0
- data/skills/rigor-project-init/references/01-detect.md +1 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
- data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
- data/skills/rigor-rbs-contract/SKILL.md +147 -0
- data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
- data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
- data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
- data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
- data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
- data/skills/rigor-rbs-setup/SKILL.md +3 -1
- data/skills/rigor-type-oracle/SKILL.md +26 -1
- data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
- metadata +48 -2
- data/lib/rigor/inference/memo_audit.rb +0 -55
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../receiver_alias"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
# Issue #2335 — which locals a write through a receiver may reach ({AliasMutationFloor}), read off a frame body's
|
|
10
|
+
# {AliasMutationFloor::Facts}.
|
|
11
|
+
module AliasMutationFloor
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# The locals aliased by the time `call_node`'s message runs, as `{ name => depth }`: those a site has handed on,
|
|
15
|
+
# and those linked to one ({.reach}). A call inside a closure that may run later sees every site.
|
|
16
|
+
def aliased_at(facts, call_node)
|
|
17
|
+
return ALL if facts.held.equal?(ALL)
|
|
18
|
+
|
|
19
|
+
offset = offset_of(facts, call_node)
|
|
20
|
+
seeds = {}
|
|
21
|
+
facts.held.each { |name, key| seeds[name] = FULL if key <= offset }
|
|
22
|
+
facts.held_part.each { |name, key| seeds[name] ||= PART if key <= offset }
|
|
23
|
+
reach(seeds, facts.links, offset)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def offset_of(facts, call_node)
|
|
27
|
+
offset = (call_node.message_loc || call_node.location).start_offset
|
|
28
|
+
facts.deferred.any? { |from, to| offset >= from && offset < to } ? Float::INFINITY : offset
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# `seeds` closed over every link whose site may have run by `offset`. A depth says how far inside a local's object
|
|
32
|
+
# the mutated object may sit: 0 ({FULL}) is the object itself, 1 ({PART}) one of its elements, and so on. A copy
|
|
33
|
+
# keeps the depth; across an element link (`x = a[i]`, `x = a[i][j]`) the parent sits as many levels further out
|
|
34
|
+
# than the child as the read goes, and the child as many further in, so a mutation of the parent object itself
|
|
35
|
+
# (depth 0) does not reach the child.
|
|
36
|
+
# The least depth wins.
|
|
37
|
+
def reach(seeds, links, offset)
|
|
38
|
+
changed = !seeds.empty?
|
|
39
|
+
while changed
|
|
40
|
+
changed = false
|
|
41
|
+
links.each do |child, parent, key, levels|
|
|
42
|
+
next if key > offset
|
|
43
|
+
|
|
44
|
+
changed = true if spread(seeds, child, parent, levels) | spread(seeds, parent, child, -levels)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
seeds
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
DEPTH_CAP = 8
|
|
51
|
+
private_constant :DEPTH_CAP
|
|
52
|
+
|
|
53
|
+
# Spreads the depth `from` reached across one link to `to`, `step` levels further out; answers whether `to`
|
|
54
|
+
# changed.
|
|
55
|
+
def spread(seeds, from, to, step) # rubocop:disable Naming/PredicateMethod
|
|
56
|
+
depth = seeds[from]
|
|
57
|
+
return false if depth.nil?
|
|
58
|
+
|
|
59
|
+
depth += step
|
|
60
|
+
return false if depth.negative? || depth > DEPTH_CAP
|
|
61
|
+
return false if seeds[to] && seeds[to] <= depth
|
|
62
|
+
|
|
63
|
+
seeds[to] = depth
|
|
64
|
+
true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The locals a write through `receiver` may reach, as {.aliased_at} answers them: through each local it may
|
|
68
|
+
# evaluate to ({ReceiverAlias.candidates}: `(c ? a : b)`), through an element read's root ({.element_targets}), or
|
|
69
|
+
# as through any other receiver.
|
|
70
|
+
def receiver_targets(receiver, facts, call_node)
|
|
71
|
+
reads = ReceiverAlias.candidates(receiver)
|
|
72
|
+
aliased = aliased_at(facts, call_node)
|
|
73
|
+
return element_targets(receiver, facts, aliased, call_node) if reads.empty?
|
|
74
|
+
return targets(nil, facts, aliased, call_node) unless reads.all?(Prism::LocalVariableReadNode)
|
|
75
|
+
|
|
76
|
+
reads.map { |read| targets(read.name, facts, aliased, call_node) }.reduce { |acc, set| union(acc, set) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Through an element read rooted at a local (`c[0] << x`, `h[:k].fetch(0) << x`), the root's own links as far
|
|
80
|
+
# inside it as the read goes (`a = c[0]` is the mutated object itself), beside what any other receiver reaches.
|
|
81
|
+
def element_targets(receiver, facts, aliased, call_node)
|
|
82
|
+
others = targets(nil, facts, aliased, call_node)
|
|
83
|
+
root, depth = element_root(receiver)
|
|
84
|
+
return others if root.nil? || others.equal?(ALL)
|
|
85
|
+
|
|
86
|
+
union(others, reach({ root.name => depth }, facts.links, offset_of(facts, call_node)))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def union(left, right)
|
|
90
|
+
return ALL if left.equal?(ALL) || right.equal?(ALL)
|
|
91
|
+
|
|
92
|
+
merged = left.dup
|
|
93
|
+
right.each { |name, depth| merged[name] = [merged[name], depth].compact.min }
|
|
94
|
+
merged
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The locals a write through the local `name` (nil for any other receiver) may reach, as `{ name => depth }`:
|
|
98
|
+
#
|
|
99
|
+
# - the locals linked to it ({.reach}): a copy of it, or what it copied (`b = a; b << x`), or the local it is an
|
|
100
|
+
# element of (`x = a[i]`);
|
|
101
|
+
# - and every aliased local, through a receiver that may hold any object: an ivar, an index read, a call result,
|
|
102
|
+
# a block parameter, or a local some write binds to an object from elsewhere;
|
|
103
|
+
# - or only the aliased locals that are foreign, through a local only ever written a new object or a method
|
|
104
|
+
# parameter the body never rebinds, once the body has handed it on: either is an object no new-object local
|
|
105
|
+
# can be, and only a holder it reached can hand it to another local.
|
|
106
|
+
def targets(name, facts, aliased, call_node)
|
|
107
|
+
return ALL if aliased.equal?(ALL)
|
|
108
|
+
return aliased if name.nil? || NUMBERED_PARAMETER.match?(name)
|
|
109
|
+
|
|
110
|
+
group = reach({ name => FULL }, facts.links, offset_of(facts, call_node))
|
|
111
|
+
group.delete(name)
|
|
112
|
+
return union(aliased, group) if facts.foreign.include?(name) && !facts.entry.include?(name)
|
|
113
|
+
return group unless aliased.key?(name)
|
|
114
|
+
|
|
115
|
+
union(group, aliased.slice(*facts.foreign))
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../element_read_widening"
|
|
6
|
+
require_relative "../mutation_widening"
|
|
7
|
+
require_relative "../receiver_alias"
|
|
8
|
+
require_relative "../remover_holder_scan"
|
|
9
|
+
require_relative "../set_mutation"
|
|
10
|
+
require_relative "foreign_locals"
|
|
11
|
+
|
|
12
|
+
module Rigor
|
|
13
|
+
module Inference
|
|
14
|
+
module AliasMutationFloor
|
|
15
|
+
ALWAYS = -1
|
|
16
|
+
LOOPS = [Prism::WhileNode, Prism::UntilNode, Prism::ForNode, Prism::BlockNode, Prism::LambdaNode].freeze
|
|
17
|
+
KEEPING_CALLS = (MutationWidening::SHAPE_MUTATORS | SetMutation::MUTATORS |
|
|
18
|
+
%i[new add add? send public_send __send__ instance_variable_set instance_exec instance_eval
|
|
19
|
+
class_exec module_exec define_method define_singleton_method]).freeze
|
|
20
|
+
private_constant :ALWAYS, :LOOPS, :KEEPING_CALLS
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
IMMUTABLE_CLASSES = %w[Integer Float Rational Complex Symbol NilClass TrueClass FalseClass].to_set.freeze
|
|
25
|
+
private_constant :IMMUTABLE_CLASSES
|
|
26
|
+
|
|
27
|
+
# Whether `type` is a container that may hold a mutable element, which a write through an element read (`x =
|
|
28
|
+
# a[0]; x << 1`) can change: `[[1]]` and `Array[String]` may, `[1, :a]` may not.
|
|
29
|
+
def mutable_parts?(type)
|
|
30
|
+
case type
|
|
31
|
+
when Type::Tuple then type.elements.any? { |element| mutable_value?(element) }
|
|
32
|
+
when Type::HashShape then type.pairs.values.any? { |value| mutable_value?(value) }
|
|
33
|
+
when Type::Nominal then type.type_args.any? { |arg| mutable_value?(arg) }
|
|
34
|
+
when Type::Difference then mutable_parts?(type.base)
|
|
35
|
+
when Type::Union then type.members.any? { |member| mutable_parts?(member) }
|
|
36
|
+
else true
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def mutable_value?(type)
|
|
41
|
+
case type
|
|
42
|
+
when Type::Constant then type.value.is_a?(String) || type.value.is_a?(::Set)
|
|
43
|
+
when Type::Nominal then !IMMUTABLE_CLASSES.include?(type.class_name)
|
|
44
|
+
when Type::Union then type.members.any? { |member| mutable_value?(member) }
|
|
45
|
+
else true
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Each local or ivar read `node` may evaluate to: through the branches of an `if` / `unless` / ternary, `case`,
|
|
50
|
+
# `case`/`in`, `begin` (its body, rescue and else clauses), `rescue` modifier and `||` / `&&`, the last statement
|
|
51
|
+
# of a body, and a call that answers its receiver (`a.tap { }`, `a.itself`, `a.freeze`, `a.to_a`); anything else
|
|
52
|
+
# as {ReceiverAlias.candidates} reads it. A nest deeper than {SELECTION_DEPTH} reads as no local.
|
|
53
|
+
def each_may_be(node, depth = 0, &)
|
|
54
|
+
node = RemoverHolderScan.unparenthesised(node)
|
|
55
|
+
return if node.nil? || depth > SELECTION_DEPTH
|
|
56
|
+
return each_may_be(node.receiver, depth + 1, &) if self_returning_call?(node)
|
|
57
|
+
|
|
58
|
+
branches = selection_branches(node)
|
|
59
|
+
return ReceiverAlias.candidates(node).each(&) if branches.nil?
|
|
60
|
+
|
|
61
|
+
branches.each { |branch| each_may_be(branch, depth + 1, &) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
SELECTION_DEPTH = 6
|
|
65
|
+
|
|
66
|
+
def self_returning_call?(node) = node.is_a?(Prism::CallNode) && SELF_RETURNERS.include?(node.name)
|
|
67
|
+
|
|
68
|
+
# The sub-expressions a selection's value is one of, or nil for a node that selects nothing.
|
|
69
|
+
def selection_branches(node)
|
|
70
|
+
case node
|
|
71
|
+
when Prism::StatementsNode then [node.body.last]
|
|
72
|
+
when Prism::ParenthesesNode, Prism::ElseNode then [node.respond_to?(:statements) ? node.statements : node.body]
|
|
73
|
+
when Prism::IfNode then [node.statements, node.subsequent]
|
|
74
|
+
when Prism::UnlessNode then [node.statements, node.else_clause]
|
|
75
|
+
when Prism::OrNode, Prism::AndNode then [node.left, node.right]
|
|
76
|
+
when Prism::CaseNode, Prism::CaseMatchNode
|
|
77
|
+
node.conditions.map(&:statements) << node.else_clause
|
|
78
|
+
when Prism::BeginNode then begin_branches(node)
|
|
79
|
+
when Prism::RescueModifierNode then [node.expression, node.rescue_expression]
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def begin_branches(node)
|
|
84
|
+
branches = [node.statements, node.else_clause]
|
|
85
|
+
clause = node.rescue_clause
|
|
86
|
+
while clause
|
|
87
|
+
branches << clause.statements
|
|
88
|
+
clause = clause.subsequent
|
|
89
|
+
end
|
|
90
|
+
branches
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def local_name(read) = ReceiverAlias.read_name(read)
|
|
94
|
+
|
|
95
|
+
# `[base, depth]` for an element read (`a[i]`, `a.first`, `h.fetch(:k)`, `a.dig(:k, 0)`, a chain of them): what
|
|
96
|
+
# it reads from and how many levels inside it, `[node, 0]` for anything else.
|
|
97
|
+
def element_path(node)
|
|
98
|
+
node = RemoverHolderScan.unparenthesised(node)
|
|
99
|
+
depth = 0
|
|
100
|
+
while node.is_a?(Prism::CallNode) && node.receiver && node.block.nil? && ELEMENT_READS.include?(node.name)
|
|
101
|
+
depth += node.name == :dig ? [node.arguments&.arguments&.size.to_i, 1].max : 1
|
|
102
|
+
node = RemoverHolderScan.unparenthesised(node.receiver)
|
|
103
|
+
end
|
|
104
|
+
[node, depth]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# `[root, depth]` for an element read rooted at a local, or nil.
|
|
108
|
+
def element_root(node)
|
|
109
|
+
root, depth = element_path(node)
|
|
110
|
+
[root, depth] if depth.positive? && root.is_a?(Prism::LocalVariableReadNode)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The {RemoverHolderScan} collector, which reads every fact {.scan} needs in that one walk: for each local the
|
|
114
|
+
# least key of the sites that hand it on ({.handed_by}), the ranges of the closures that may run later, and the
|
|
115
|
+
# body's foreign and written locals ({ForeignLocals.visit}).
|
|
116
|
+
class SiteCollector
|
|
117
|
+
# Non-local reads: a local bound to one, or to an element read rooted at one (`a = @rows[0]`), is held by that
|
|
118
|
+
# variable. ({ReceiverAlias.candidates} answers a bare ivar read itself.)
|
|
119
|
+
NON_LOCAL_READS = [Prism::InstanceVariableReadNode, Prism::GlobalVariableReadNode,
|
|
120
|
+
Prism::ClassVariableReadNode].to_set.freeze
|
|
121
|
+
LOCAL_BINDS = [Prism::LocalVariableWriteNode, Prism::LocalVariableOrWriteNode,
|
|
122
|
+
Prism::LocalVariableAndWriteNode].to_set.freeze
|
|
123
|
+
|
|
124
|
+
attr_writer :site
|
|
125
|
+
attr_reader :held, :held_part, :links, :deferred, :foreign, :written
|
|
126
|
+
|
|
127
|
+
def initialize
|
|
128
|
+
@held = {}
|
|
129
|
+
@held_part = {}
|
|
130
|
+
@links = []
|
|
131
|
+
@site = nil
|
|
132
|
+
@outer = nil
|
|
133
|
+
@outer_owner = nil
|
|
134
|
+
@deferred = []
|
|
135
|
+
@foreign = Set.new
|
|
136
|
+
@written = Set.new
|
|
137
|
+
@literal_blocks = Set.new.compare_by_identity
|
|
138
|
+
@def_depth = 0
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# A local handed on whole: a capture by a closure that may run later, a reflective receiver, a zero-argument
|
|
142
|
+
# `super`'s parameter.
|
|
143
|
+
def <<(name)
|
|
144
|
+
note(@held, name)
|
|
145
|
+
self
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# A value position another holder keeps (an element, an argument of a keeping call, an index or attribute
|
|
149
|
+
# write's value): each local the value may be is handed on, and the root of an element read hands on a part.
|
|
150
|
+
def value_held(node)
|
|
151
|
+
AliasMutationFloor.each_may_be(node) do |read|
|
|
152
|
+
note(@held, AliasMutationFloor.local_name(read)) if local?(read)
|
|
153
|
+
end
|
|
154
|
+
root, = AliasMutationFloor.element_root(node)
|
|
155
|
+
note(@held_part, root.name) if root
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# A write: a local bound to a value that may be another local's object links the two (`b = a`), an element read
|
|
159
|
+
# links them as a part (`x = a[i]`), and a local bound to a non-local holder's object is held by that holder
|
|
160
|
+
# (`a = @q`). A non-local write hands the value on as {#value_held} does.
|
|
161
|
+
def value_written(node)
|
|
162
|
+
return if node.is_a?(Prism::LocalVariableOperatorWriteNode)
|
|
163
|
+
return value_held(node.value) unless LOCAL_BINDS.include?(node.class)
|
|
164
|
+
|
|
165
|
+
bind(node.name, node.value)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# A multiple assignment or a pattern: each local target is bound as {#value_written} binds one, to each element
|
|
169
|
+
# of an Array literal value or to the value whole.
|
|
170
|
+
def value_bound(value, target_nodes)
|
|
171
|
+
names = []
|
|
172
|
+
RemoverHolderScan.local_targets(target_nodes, names)
|
|
173
|
+
return if names.empty?
|
|
174
|
+
|
|
175
|
+
values = value.is_a?(Prism::ArrayNode) ? value.elements : [value]
|
|
176
|
+
names.each { |name| values.each { |part| bind(name, part) } }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def enter(node, parent)
|
|
180
|
+
if @outer.nil? && LOOPS.any? { |klass| node.is_a?(klass) }
|
|
181
|
+
@outer = node.location.start_offset
|
|
182
|
+
@outer_owner = node
|
|
183
|
+
end
|
|
184
|
+
note_deferred(node, parent)
|
|
185
|
+
if node.is_a?(Prism::DefNode)
|
|
186
|
+
@def_depth += 1
|
|
187
|
+
elsif @def_depth.zero?
|
|
188
|
+
ForeignLocals.visit(node, @foreign, @written, @literal_blocks)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def leave(node)
|
|
193
|
+
if node.equal?(@outer_owner)
|
|
194
|
+
@outer = nil
|
|
195
|
+
@outer_owner = nil
|
|
196
|
+
end
|
|
197
|
+
@def_depth -= 1 if node.is_a?(Prism::DefNode)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Only a call that may keep its argument hands it to another holder: a constructor, an in-place mutator (a
|
|
201
|
+
# container adder), and a reflective or `define_method` call. Any other call's arguments are passed, not kept.
|
|
202
|
+
def keeps_arguments?(call_node) = KEEPING_CALLS.include?(call_node.name)
|
|
203
|
+
|
|
204
|
+
private
|
|
205
|
+
|
|
206
|
+
def local?(read) = read.is_a?(Prism::LocalVariableReadNode) || read.is_a?(Prism::ItLocalVariableReadNode)
|
|
207
|
+
|
|
208
|
+
def key = @site ? @outer || AliasMutationFloor.handed_by(@site) : ALWAYS
|
|
209
|
+
|
|
210
|
+
def note(table, name)
|
|
211
|
+
site_key = key
|
|
212
|
+
current = table[name]
|
|
213
|
+
table[name] = site_key if current.nil? || site_key < current
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def bind(name, value)
|
|
217
|
+
AliasMutationFloor.each_may_be(value) do |read|
|
|
218
|
+
next unless local?(read)
|
|
219
|
+
|
|
220
|
+
other = AliasMutationFloor.local_name(read)
|
|
221
|
+
@links << [name, other, key, 0] unless other == name
|
|
222
|
+
end
|
|
223
|
+
note(@held, name) if NON_LOCAL_READS.include?(AliasMutationFloor.element_path(value).first.class)
|
|
224
|
+
root, depth = AliasMutationFloor.element_root(value)
|
|
225
|
+
@links << [name, root.name, key, depth] if root && root.name != name
|
|
226
|
+
link_elements(name, value)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# A container literal's elements are the local's elements (`c = [inner]`): each local an element may be is
|
|
230
|
+
# linked to it as a part.
|
|
231
|
+
def link_elements(name, value)
|
|
232
|
+
value = RemoverHolderScan.unparenthesised(value)
|
|
233
|
+
elements = value.elements if value.is_a?(Prism::ArrayNode)
|
|
234
|
+
elements = value.elements.grep(Prism::AssocNode).map(&:value) if value.is_a?(Prism::HashNode)
|
|
235
|
+
elements&.each do |element|
|
|
236
|
+
AliasMutationFloor.each_may_be(element) do |read|
|
|
237
|
+
@links << [AliasMutationFloor.local_name(read), name, key, 1] if local?(read)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def note_deferred(node, parent)
|
|
243
|
+
return unless node.is_a?(Prism::LambdaNode) ||
|
|
244
|
+
(node.is_a?(Prism::BlockNode) && RemoverHolderScan.deferred_block?(parent))
|
|
245
|
+
|
|
246
|
+
start = node.location.start_offset
|
|
247
|
+
@deferred << [start, start + node.location.length]
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|