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,354 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "anonymous_meta_class"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
# Issue #2490 — a block, inside an instance body of a class `K`, whose `self` is provably not an instance of `K`, so
|
|
10
|
+
# an `@x` write directly in it writes some other object's ivar and the class-ivar censuses must not credit it to
|
|
11
|
+
# `K`. Read syntactically, before any type is known, by both the per-file census ({ScopeIndexer}) and the cross-file
|
|
12
|
+
# {IvarWriterCensus}. False positives come first, so a receiver that may hold a `K` instance keeps the credit:
|
|
13
|
+
#
|
|
14
|
+
# - `instance_eval` / `instance_exec` on a receiver that is provably no `K` instance ({.provably_other?}): a
|
|
15
|
+
# constant or `self.class` (a class object), `X.new` / `X.allocate` on a constant `X` not spelling `K`, a
|
|
16
|
+
# literal. Any other receiver (a parameter, a local, `dup`, `itself`) may be a `K` and still counts;
|
|
17
|
+
# - `class_eval` / `module_eval` / `class_exec` / `module_exec` on an explicit receiver: `self` is the receiver
|
|
18
|
+
# module, so `@x` directly in the block is that module object's own ivar;
|
|
19
|
+
# - `define_method` on a constant not spelling `K` (its block runs on that class's instances), and
|
|
20
|
+
# `define_singleton_method` on a class object or a literal;
|
|
21
|
+
# - a `Class.new` / `Module.new` / `Struct.new` / `Data.define` block: `self` is the new class.
|
|
22
|
+
#
|
|
23
|
+
# `class << obj` inside an instance body is one more barred body; the callers bar it themselves, since it is no
|
|
24
|
+
# call. Only a barred body's own statements are dropped: a `define_method` block nested in a `class_eval` or
|
|
25
|
+
# `class << obj` body whose module may be `K` or an instance's singleton class runs on a `K` instance and still
|
|
26
|
+
# counts, and so does a receiver-less `def` nested in either (#2941, {.reentry_blocks}).
|
|
27
|
+
# `def.ivar-write-mismatch`'s collector reads the same rule (#2943, {.census_children}).
|
|
28
|
+
module CensusForeignBlock
|
|
29
|
+
INSTANCE_EVALS = %i[instance_eval instance_exec].to_set.freeze
|
|
30
|
+
MODULE_EVALS = %i[class_eval module_eval class_exec module_exec].to_set.freeze
|
|
31
|
+
DEFINERS = %i[define_method define_singleton_method].to_set.freeze
|
|
32
|
+
SEND_CALLS = %i[send public_send __send__].to_set.freeze
|
|
33
|
+
# The constructors whose receiver names the class of the object they answer.
|
|
34
|
+
CONSTRUCTORS = %i[new allocate].to_set.freeze
|
|
35
|
+
# The literal receivers, none of them an instance of a project class.
|
|
36
|
+
LITERALS = [Prism::StringNode, Prism::InterpolatedStringNode, Prism::SymbolNode, Prism::IntegerNode,
|
|
37
|
+
Prism::FloatNode, Prism::ArrayNode, Prism::HashNode, Prism::RangeNode, Prism::RegularExpressionNode,
|
|
38
|
+
Prism::NilNode, Prism::TrueNode, Prism::FalseNode, Prism::LambdaNode].freeze
|
|
39
|
+
# The calls that answer a module of their receiver.
|
|
40
|
+
MODULE_OF_INSTANCE = %i[class singleton_class].to_set.freeze
|
|
41
|
+
# The constants naming a module every class has among its ancestors, whose methods an instance runs.
|
|
42
|
+
ANCESTOR_ROOTS = %w[Object BasicObject Kernel].to_set.freeze
|
|
43
|
+
# The calls whose answer is foreign when their receiver is: the object itself, a copy, a new instance, its class.
|
|
44
|
+
FOREIGN_PRESERVING = %i[new allocate dup clone itself class singleton_class].to_set.freeze
|
|
45
|
+
# Issue #2983 — the scalar literals, whose blockless calls answer no instance of a project class, unlike a
|
|
46
|
+
# container's (`[k].first`) or a lambda's.
|
|
47
|
+
SCALAR_LITERALS = (LITERALS - [Prism::ArrayNode, Prism::HashNode, Prism::LambdaNode]).freeze
|
|
48
|
+
# A String or Symbol literal that spells a constant path, which a call may resolve to the class
|
|
49
|
+
# (`"K".constantize`).
|
|
50
|
+
CONSTANT_SPELLING = /\A(?:::)?[A-Z]\w*(?:::[A-Z]\w*)*\z/
|
|
51
|
+
# Separates the qualifications of a {.candidate_key}; no constant name holds it.
|
|
52
|
+
CANDIDATE_SEPARATOR = "|"
|
|
53
|
+
# Marks a candidate key whose code runs on the constant's own value's module ({.root_constant}).
|
|
54
|
+
OBJECT_MARK = "@"
|
|
55
|
+
COPIES = %i[dup clone itself].to_set.freeze
|
|
56
|
+
BARRIERS = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
57
|
+
NONE = [].freeze
|
|
58
|
+
# Every name {.foreign?} can answer true for, so any other call returns at one Set probe.
|
|
59
|
+
CALLS = (INSTANCE_EVALS | MODULE_EVALS | DEFINERS | %i[new define]).freeze
|
|
60
|
+
|
|
61
|
+
module_function
|
|
62
|
+
|
|
63
|
+
# Whether `node`'s literal block runs with a `self` that is provably not an instance of `class_name`.
|
|
64
|
+
def foreign?(node, class_name)
|
|
65
|
+
unless node.is_a?(Prism::CallNode) && CALLS.include?(node.name) && node.block.is_a?(Prism::BlockNode)
|
|
66
|
+
return false
|
|
67
|
+
end
|
|
68
|
+
return true if AnonymousMetaClass.block_form_receiver(node)
|
|
69
|
+
|
|
70
|
+
name = node.name
|
|
71
|
+
receiver = node.receiver
|
|
72
|
+
return false if self_node?(receiver)
|
|
73
|
+
return true if MODULE_EVALS.include?(name)
|
|
74
|
+
return provably_other?(receiver, class_name) if INSTANCE_EVALS.include?(name)
|
|
75
|
+
return provably_other?(receiver, class_name) if name == :define_singleton_method
|
|
76
|
+
|
|
77
|
+
foreign_constant?(receiver, class_name)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# A receiver that is provably no instance of `class_name`: a constant or `self.class` (a class object, `K`
|
|
81
|
+
# itself included), `X.new` / `X.allocate` on a constant `X` not spelling `class_name`, or a literal.
|
|
82
|
+
def provably_other?(receiver, class_name)
|
|
83
|
+
case receiver
|
|
84
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode then true
|
|
85
|
+
when Prism::CallNode
|
|
86
|
+
return receiver.arguments.nil? if receiver.name == :class
|
|
87
|
+
|
|
88
|
+
CONSTRUCTORS.include?(receiver.name) && foreign_constant?(receiver.receiver, class_name)
|
|
89
|
+
else LITERALS.any? { |klass| receiver.is_a?(klass) }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# A constant that does not spell `class_name`.
|
|
94
|
+
def foreign_constant?(node, class_name)
|
|
95
|
+
(node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)) && !own_class?(node, class_name)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The children of `node` a census walk for `class_name` still descends into when `node` runs code whose `self`
|
|
99
|
+
# is another object, or nil for an ordinary node: a {.foreign?} call keeps its receiver and arguments, and a
|
|
100
|
+
# `class << obj` body its expression; both keep the code nested in them that runs on a `class_name` instance
|
|
101
|
+
# again ({.reentry_blocks}) unless `reentry` is false, as it is in a singleton-side `def`, whose `self.class`
|
|
102
|
+
# and `singleton_class` are no module of an instance. A reentered `def` comes back as the {Prism::DefNode},
|
|
103
|
+
# which a walk that stops at a `def` skips and the class-ivar census walks into (#2941).
|
|
104
|
+
def census_children(node, class_name, reentry: true)
|
|
105
|
+
case node
|
|
106
|
+
when Prism::SingletonClassNode
|
|
107
|
+
return [node.expression] unless reentry
|
|
108
|
+
|
|
109
|
+
[node.expression, *reentry_blocks(node, class_name)]
|
|
110
|
+
when Prism::CallNode
|
|
111
|
+
return nil unless foreign?(node, class_name)
|
|
112
|
+
return [node.receiver, node.arguments].compact unless reentry
|
|
113
|
+
|
|
114
|
+
[node.receiver, node.arguments, *reentry_blocks(node, class_name)].compact
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# The code inside barred code whose `self` may again be an instance of `class_name`, in a `class_eval`-family
|
|
119
|
+
# block whose module may be `class_name` or the singleton class of one of its instances ({.foreign_module?}) or in
|
|
120
|
+
# a `class << obj` body on an operand {.provably_other?} does not name:
|
|
121
|
+
#
|
|
122
|
+
# - a `define_method` block on the implicit or `self` receiver, called directly or through `send` /
|
|
123
|
+
# `public_send` / `__send__` with the literal name;
|
|
124
|
+
# - a receiver-less `def` (#2941), which defines an instance method of that module, or a singleton method of
|
|
125
|
+
# the `class << obj` operand, just as the `define_method` does.
|
|
126
|
+
#
|
|
127
|
+
# Ruby runs each on a `K` instance (a `K` method, or a singleton method of the instance), so its writes are
|
|
128
|
+
# `K`'s; an untyped receiver keeps the credit, as {.foreign?} keeps it for `instance_eval`.
|
|
129
|
+
def reentry_blocks(node, class_name)
|
|
130
|
+
body =
|
|
131
|
+
case node
|
|
132
|
+
when Prism::SingletonClassNode then node.body unless provably_other?(node.expression, class_name)
|
|
133
|
+
when Prism::CallNode
|
|
134
|
+
node.block.body if MODULE_EVALS.include?(node.name) && !foreign_module?(node.receiver, class_name)
|
|
135
|
+
end
|
|
136
|
+
return NONE if body.nil?
|
|
137
|
+
|
|
138
|
+
found = []
|
|
139
|
+
collect_define_method_blocks(body, found)
|
|
140
|
+
found
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# A `class_eval` receiver whose module is provably neither `class_name`, an ancestor of it nor an instance's
|
|
144
|
+
# singleton class, read from inside an instance body. A receiver chain is foreign only when it is:
|
|
145
|
+
#
|
|
146
|
+
# - rooted at a constant other than `class_name`'s own spelling and {ANCESTOR_ROOTS} (`Other.class_eval`,
|
|
147
|
+
# `Other.dup`), a `X.new` or `Class.new` on such a constant among them, or rooted at a literal, through
|
|
148
|
+
# {FOREIGN_PRESERVING} calls only;
|
|
149
|
+
# - `.class` / `.singleton_class` on a receiver {.provably_other?} names (`Other.new.class`, `"s".class`,
|
|
150
|
+
# `self.class.class`);
|
|
151
|
+
# - rooted at a scalar literal through blockless calls of any name (`"s".upcase.class`, #2983), unless the
|
|
152
|
+
# literal is a String or Symbol that spells a constant (`"K".constantize`).
|
|
153
|
+
#
|
|
154
|
+
# A `superclass` call may answer an ancestor (`Other.superclass` is `Object`), a `const_get` or registry call may
|
|
155
|
+
# answer the class (`Other.const_get(:K)`), and every other receiver
|
|
156
|
+
# (`self.class`, `singleton_class`, a parameter's `o.class`, `self.class.base_class`) may name `class_name`, so
|
|
157
|
+
# they keep the credit. Parentheses are looked through. The rule is syntactic: a constant naming another
|
|
158
|
+
# ancestor of the class (`Base.class_eval` with `K < Base`), an alias of it or one of its instances reads as
|
|
159
|
+
# foreign here, and the cross-file census files the barred block's code under that constant instead
|
|
160
|
+
# ({.root_constant}), whose entries the class's census joins (#2983).
|
|
161
|
+
def foreign_module?(receiver, class_name)
|
|
162
|
+
receiver = unwrap(receiver)
|
|
163
|
+
return true if scalar_literal_rooted?(receiver)
|
|
164
|
+
|
|
165
|
+
case receiver
|
|
166
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
167
|
+
foreign_constant?(receiver, class_name) && !ANCESTOR_ROOTS.include?(receiver.slice.delete_prefix("::"))
|
|
168
|
+
when Prism::CallNode then foreign_module_call?(receiver, class_name)
|
|
169
|
+
else LITERALS.any? { |klass| receiver.is_a?(klass) }
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# A call in the chain keeps it foreign only when it answers its receiver's own object, a copy, a new instance
|
|
174
|
+
# or its class ({FOREIGN_PRESERVING}); any other call (`const_get`, a registry's `fetch`, `superclass`) may
|
|
175
|
+
# answer the class, so the receiver may be it.
|
|
176
|
+
def foreign_module_call?(call, class_name)
|
|
177
|
+
if MODULE_OF_INSTANCE.include?(call.name) && call.arguments.nil? &&
|
|
178
|
+
provably_other?(unwrap(call.receiver), class_name)
|
|
179
|
+
return true
|
|
180
|
+
end
|
|
181
|
+
return false unless FOREIGN_PRESERVING.include?(call.name)
|
|
182
|
+
|
|
183
|
+
foreign_module?(call.receiver, class_name)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Issue #2983 — whether `receiver` is a chain of blockless calls rooted at a scalar literal that spells no
|
|
187
|
+
# constant: whatever the calls are, Ruby's methods on such a literal answer no project class or its instance.
|
|
188
|
+
def scalar_literal_rooted?(receiver)
|
|
189
|
+
node = receiver
|
|
190
|
+
while node.is_a?(Prism::CallNode)
|
|
191
|
+
return false if node.block
|
|
192
|
+
|
|
193
|
+
node = unwrap(node.receiver)
|
|
194
|
+
end
|
|
195
|
+
return false if node.equal?(receiver) || SCALAR_LITERALS.none? { |klass| node.is_a?(klass) }
|
|
196
|
+
|
|
197
|
+
!((node.is_a?(Prism::StringNode) || node.is_a?(Prism::SymbolNode)) && CONSTANT_SPELLING.match?(node.unescaped))
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Issue #2983 — the constant a `class_eval` receiver chain is rooted at through {FOREIGN_PRESERVING} calls
|
|
201
|
+
# (`Anc`, `INST.class`, `AliasK.new.class`, `Other.singleton_class`), as its candidate key under `nesting`
|
|
202
|
+
# ({.candidate_key}), or nil. The module such a chain names is the constant's, its class's or its singleton's,
|
|
203
|
+
# so the code a barred block reenters runs on the constant's instances or on the object it holds; the
|
|
204
|
+
# cross-file census files it under the key, and a class whose census joins the constant the key resolves to (an
|
|
205
|
+
# ancestor, an alias, an instance constant of it) sees the write.
|
|
206
|
+
#
|
|
207
|
+
# Review round 2 of #3001 — a `.class` or `.singleton_class` taken of the constant's own value (`INST.class`,
|
|
208
|
+
# `X.singleton_class`, through `dup` / `clone` / `itself`) names the module of that object, which is a class
|
|
209
|
+
# instance only when the constant binds one; the key then carries {OBJECT_MARK}, and the join credits it only
|
|
210
|
+
# when the constant resolves to an instance binding (`INST = K.new`). One taken of a class object (`X.class` of a
|
|
211
|
+
# `.class`, `X.new.class.singleton_class`) names `Class` or a metaclass, whose methods no instance runs: nil.
|
|
212
|
+
def root_constant(receiver, nesting)
|
|
213
|
+
node = unwrap(receiver)
|
|
214
|
+
object = false
|
|
215
|
+
while node.is_a?(Prism::CallNode) && FOREIGN_PRESERVING.include?(node.name)
|
|
216
|
+
inner = unwrap(node.receiver)
|
|
217
|
+
if MODULE_OF_INSTANCE.include?(node.name)
|
|
218
|
+
value = through_copies(inner)
|
|
219
|
+
return if value.is_a?(Prism::CallNode) && MODULE_OF_INSTANCE.include?(value.name)
|
|
220
|
+
|
|
221
|
+
object = true if constant_node?(value)
|
|
222
|
+
end
|
|
223
|
+
node = inner
|
|
224
|
+
end
|
|
225
|
+
return unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
|
226
|
+
|
|
227
|
+
key = candidate_key(node.slice, nesting)
|
|
228
|
+
object ? "#{OBJECT_MARK}#{key}" : key
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# `node` with `dup`, `clone` and `itself` calls looked through: the object they answer has the same class.
|
|
232
|
+
def through_copies(node)
|
|
233
|
+
node = unwrap(node.receiver) while node.is_a?(Prism::CallNode) && COPIES.include?(node.name)
|
|
234
|
+
node
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def constant_node?(node) = node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
|
238
|
+
|
|
239
|
+
# Review round 1 of #3001 — the qualifications a constant spelled `spelling` may have under the lexical
|
|
240
|
+
# `nesting` (the enclosing class and module names), nearest first as Ruby's lookup tries them, joined by
|
|
241
|
+
# {CANDIDATE_SEPARATOR}: `Anc` under `N::K` is `"N::K::Anc|N::Anc|Anc"`. A rooted `::Anc`, or one at the top
|
|
242
|
+
# level, has the one candidate `"Anc"`. `ScopeIndexer::IvarContributions` resolves the key to the first
|
|
243
|
+
# candidate the project declares.
|
|
244
|
+
def candidate_key(spelling, nesting)
|
|
245
|
+
return spelling.delete_prefix("::") if spelling.start_with?("::") || nesting.empty?
|
|
246
|
+
|
|
247
|
+
nesting.size.downto(0).map { |depth| [*nesting.first(depth), spelling].join("::") }.join(CANDIDATE_SEPARATOR)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Review round 3 of #3001 — whether `call` is `Class.new`, `Module.new`, `Struct.new` or `Data.define` on the
|
|
251
|
+
# bare constant, whose answer is a class.
|
|
252
|
+
def meta_new?(call)
|
|
253
|
+
!AnonymousMetaClass::META_NEW_SELECTORS[AnonymousMetaClass.literal_constant_name(call.receiver)].nil?
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Issue #2983 — for a constant write `C = K` or `C = K.new` / `K.allocate` under the lexical `nesting`, the
|
|
257
|
+
# census entry `[:binds_class, key, name, kind]`: `K`'s {.candidate_key}, `C`'s qualified name (a path write's
|
|
258
|
+
# as written) and `:instance` for a constructor or `:alias`, else nil. `C` is an alias or an instance of the
|
|
259
|
+
# class `K` resolves to.
|
|
260
|
+
def binding_entry(node, nesting)
|
|
261
|
+
value = unwrap(node.value)
|
|
262
|
+
instance = value.is_a?(Prism::CallNode) && CONSTRUCTORS.include?(value.name) && value.block.nil?
|
|
263
|
+
# Review round 3 of #3001: `Struct.new(:a)`, `Class.new(Base)` and `Module.new` answer a class, not an instance.
|
|
264
|
+
# A guard beside `IvarContributions.module?`, which drops the same credit once the constant is a declared class.
|
|
265
|
+
return if instance && meta_new?(value)
|
|
266
|
+
|
|
267
|
+
value = value.receiver if instance
|
|
268
|
+
return unless value.is_a?(Prism::ConstantReadNode) || value.is_a?(Prism::ConstantPathNode)
|
|
269
|
+
|
|
270
|
+
name = node.is_a?(Prism::ConstantWriteNode) ? [*nesting, node.name].join("::") : node.target.slice
|
|
271
|
+
[:binds_class, candidate_key(value.slice, nesting), name.delete_prefix("::"),
|
|
272
|
+
instance ? :instance : :alias].freeze
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Issue #2983 — the module a String `class_eval` / `module_eval` may define methods on, for
|
|
276
|
+
# `IvarWriterCensus#record_string_eval`: `owner`, the class owning a class body, for an implicit or `self`
|
|
277
|
+
# receiver there (one in an instance body raises); `instance`, the instance body's class, for a receiver
|
|
278
|
+
# {.foreign_module?} does not bar; otherwise the receiver chain's root constant ({.root_constant}); or nil.
|
|
279
|
+
def string_eval_target(node, instance, owner)
|
|
280
|
+
return (owner unless instance) if self_node?(node.receiver)
|
|
281
|
+
return instance if instance && !foreign_module?(node.receiver, instance)
|
|
282
|
+
|
|
283
|
+
nesting = instance || owner
|
|
284
|
+
root_constant(node.receiver, nesting ? nesting.split("::") : NONE)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# For a barred `class_eval`-family block whose receiver {.foreign_module?} takes as another module and whose
|
|
288
|
+
# chain is rooted at a constant ({.root_constant}), that constant's candidate key and the code in the block that
|
|
289
|
+
# runs on its module's instances, as {.reentry_blocks} collects it; else nil.
|
|
290
|
+
def rooted_reentry(node, class_name)
|
|
291
|
+
return unless MODULE_EVALS.include?(node.name) && foreign_module?(node.receiver, class_name)
|
|
292
|
+
|
|
293
|
+
root = root_constant(node.receiver, class_name.split("::"))
|
|
294
|
+
return if root.nil?
|
|
295
|
+
|
|
296
|
+
found = []
|
|
297
|
+
collect_define_method_blocks(node.block.body, found)
|
|
298
|
+
[root, found]
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# `node` with any parentheses around a single expression looked through.
|
|
302
|
+
def unwrap(node)
|
|
303
|
+
while node.is_a?(Prism::ParenthesesNode)
|
|
304
|
+
body = node.body
|
|
305
|
+
body = body.body.first if body.is_a?(Prism::StatementsNode) && body.body.size == 1
|
|
306
|
+
break if body.nil? || body.is_a?(Prism::StatementsNode)
|
|
307
|
+
|
|
308
|
+
node = body
|
|
309
|
+
end
|
|
310
|
+
node
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def collect_define_method_blocks(node, found)
|
|
314
|
+
return unless node.is_a?(Prism::Node)
|
|
315
|
+
return found << node if node.is_a?(Prism::DefNode) && node.receiver.nil?
|
|
316
|
+
return if BARRIERS.any? { |klass| node.is_a?(klass) }
|
|
317
|
+
|
|
318
|
+
if node.is_a?(Prism::CallNode)
|
|
319
|
+
return found << node.block if define_method_call?(node)
|
|
320
|
+
return if node.block.is_a?(Prism::BlockNode) && CALLS.include?(node.name)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
node.rigor_each_child { |child| collect_define_method_blocks(child, found) }
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# A `define_method` with a literal block on the implicit or `self` receiver, called directly or through a `send`
|
|
327
|
+
# whose first argument is the literal name.
|
|
328
|
+
def define_method_call?(node)
|
|
329
|
+
return false unless node.is_a?(Prism::CallNode) && self_node?(node.receiver)
|
|
330
|
+
return false unless node.block.is_a?(Prism::BlockNode)
|
|
331
|
+
return true if node.name == :define_method
|
|
332
|
+
return false unless SEND_CALLS.include?(node.name)
|
|
333
|
+
|
|
334
|
+
first = node.arguments&.arguments&.first
|
|
335
|
+
(first.is_a?(Prism::SymbolNode) || first.is_a?(Prism::StringNode)) && first.unescaped == "define_method"
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# `self.class`, or a constant spelling `class_name`.
|
|
339
|
+
def own_class?(node, class_name)
|
|
340
|
+
case node
|
|
341
|
+
when Prism::CallNode
|
|
342
|
+
node.name == :class && self_node?(node.receiver) && node.arguments.nil?
|
|
343
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
344
|
+
spelled = node.slice.delete_prefix("::")
|
|
345
|
+
spelled == class_name || spelled == class_name.split("::").last
|
|
346
|
+
else
|
|
347
|
+
false
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def self_node?(node) = node.nil? || node.is_a?(Prism::SelfNode)
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
@@ -57,7 +57,7 @@ module Rigor
|
|
|
57
57
|
widened = names.reduce(scope) do |acc, name|
|
|
58
58
|
ivar = name.start_with?("@")
|
|
59
59
|
current = ivar ? scope.ivar(name) : scope.local(name)
|
|
60
|
-
next acc if current.nil? || current
|
|
60
|
+
next acc if current.nil? || Type::Combinator.untyped?(current)
|
|
61
61
|
|
|
62
62
|
ivar ? acc.with_ivar(name, untyped).without_ivar_guard(name) : acc.with_local(name, untyped)
|
|
63
63
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../type"
|
|
4
4
|
require_relative "../reflection"
|
|
5
5
|
require_relative "external_ancestor_resolution"
|
|
6
|
+
require_relative "../memo_audit"
|
|
6
7
|
require_relative "project_method_ownership"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
@@ -185,9 +186,15 @@ module Rigor
|
|
|
185
186
|
return false unless scope.known_user_class?(class_name)
|
|
186
187
|
|
|
187
188
|
by_method = (ProjectMethodOwnership.memo(scope)[:ancestry][class_name] ||= {})
|
|
188
|
-
|
|
189
|
+
if by_method.key?(method_sym)
|
|
190
|
+
return MemoAudit.check("ancestry non-escaping", by_method[method_sym], class_name, method_sym) do
|
|
191
|
+
compute_ancestry_non_escaping?(class_name, method_sym, scope)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
189
194
|
|
|
190
|
-
by_method[method_sym] =
|
|
195
|
+
by_method[method_sym] = MemoAudit.key_audit("ancestry non-escaping") do
|
|
196
|
+
compute_ancestry_non_escaping?(class_name, method_sym, scope)
|
|
197
|
+
end
|
|
191
198
|
end
|
|
192
199
|
|
|
193
200
|
def compute_ancestry_non_escaping?(class_name, method_sym, scope)
|
|
@@ -236,7 +243,7 @@ module Rigor
|
|
|
236
243
|
OBJECT_NON_ESCAPING = %i[tap then yield_self].freeze
|
|
237
244
|
|
|
238
245
|
ENUMERABLE_NON_ESCAPING = %i[
|
|
239
|
-
each map collect flat_map collect_concat
|
|
246
|
+
each reverse_each map collect flat_map collect_concat
|
|
240
247
|
select filter reject filter_map
|
|
241
248
|
find detect find_index find_all
|
|
242
249
|
any? all? none? one? count tally sum
|
|
@@ -248,7 +255,7 @@ module Rigor
|
|
|
248
255
|
zip
|
|
249
256
|
].freeze
|
|
250
257
|
|
|
251
|
-
ARRAY_EXTRA = %i[each_index].freeze
|
|
258
|
+
ARRAY_EXTRA = %i[each_index rfind index rindex].freeze
|
|
252
259
|
HASH_EXTRA = %i[
|
|
253
260
|
each_pair each_key each_value
|
|
254
261
|
transform_keys transform_values
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
require_relative "../source/node_walker"
|
|
5
5
|
require_relative "../source/node_children"
|
|
6
|
-
require_relative "../
|
|
6
|
+
require_relative "../source/literals"
|
|
7
|
+
require_relative "../typing_state"
|
|
8
|
+
require_relative "../memo_audit"
|
|
7
9
|
|
|
8
10
|
module Rigor
|
|
9
11
|
module Inference
|
|
@@ -26,8 +28,10 @@ module Rigor
|
|
|
26
28
|
# A listed name the project defines anywhere (a reopen, a prepended or included module, a refinement, any
|
|
27
29
|
# class at all) escapes all the same ({.project_defines?}). Every argument position escapes, and so does
|
|
28
30
|
# any reference inside a block or lambda, a write whose value is used (`a = b = …`), and every local of a
|
|
29
|
-
# body that
|
|
30
|
-
# `instance_eval` / `class_eval` /
|
|
31
|
+
# body that reaches the frame reflectively: a call of `binding`, `eval`, `local_variable_get`,
|
|
32
|
+
# `local_variable_set`, `instance_variable_set` or a string-form `instance_eval` / `class_eval` /
|
|
33
|
+
# `module_eval`, a `send` / `__send__` / `public_send` dispatch whose first argument is one of those names
|
|
34
|
+
# as a literal Symbol or String, or a `TOPLEVEL_BINDING` reference.
|
|
31
35
|
# The scan is flow insensitive and runs once per body, so an escape anywhere drops the fact for the whole body:
|
|
32
36
|
# the conservative direction, since without the fact the seams answer as before. A nested `def` or class body is
|
|
33
37
|
# scanned too, though its locals are its own; a same-named local there can only over-report an escape.
|
|
@@ -59,8 +63,11 @@ module Rigor
|
|
|
59
63
|
)
|
|
60
64
|
|
|
61
65
|
# Calls that read or rebind the frame's locals by name: one anywhere in a body escapes every local in it.
|
|
62
|
-
FRAME_CALLS = %i[binding eval local_variable_set instance_variable_set].to_set.freeze
|
|
66
|
+
FRAME_CALLS = %i[binding eval local_variable_get local_variable_set instance_variable_set].to_set.freeze
|
|
63
67
|
STRING_EVAL_CALLS = %i[instance_eval class_eval module_eval].to_set.freeze
|
|
68
|
+
# The spellings that reach a {FRAME_CALLS} or {STRING_EVAL_CALLS} name through a literal Symbol or String
|
|
69
|
+
# argument (`send(:eval, …)`, `send("eval", …)`).
|
|
70
|
+
SEND_CALLS = %i[send __send__ public_send].to_set.freeze
|
|
64
71
|
|
|
65
72
|
CLOSURES = [Prism::BlockNode, Prism::LambdaNode].freeze
|
|
66
73
|
LOCAL_REFERENCES = [
|
|
@@ -73,11 +80,15 @@ module Rigor
|
|
|
73
80
|
].freeze
|
|
74
81
|
ALL = :all
|
|
75
82
|
EMPTY_CALLS = Set.new.freeze
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
# ADR-129 WD1 — a run-scoped Memo, emptied when a run ends so a long-lived process never serves a verdict from
|
|
84
|
+
# a body an earlier run parsed: keyed by body node, the only input of the scan.
|
|
85
|
+
CACHE = TypingState.memo(
|
|
86
|
+
:construction_escape,
|
|
87
|
+
run_scoped: true, audit: "construction escape",
|
|
88
|
+
reason: "keyed by the body node; the escape scan is a pure function of the body"
|
|
89
|
+
)
|
|
90
|
+
private_constant :CLOSURES, :LOCAL_REFERENCES, :LOCAL_WRITES, :ALL, :EMPTY_CALLS, :CACHE,
|
|
91
|
+
:STRING_EVAL_CALLS, :SEND_CALLS
|
|
81
92
|
|
|
82
93
|
# One body's answer: per name, ALL or the classes it escapes as, and the calls the scan admitted on it.
|
|
83
94
|
Result = Data.define(:escapes, :calls)
|
|
@@ -91,8 +102,7 @@ module Rigor
|
|
|
91
102
|
def escaped?(body, name, class_name, scope)
|
|
92
103
|
return true if body.nil? || class_name.nil?
|
|
93
104
|
|
|
94
|
-
|
|
95
|
-
result = table[body] ||= scan(body)
|
|
105
|
+
result = escape_scan(body)
|
|
96
106
|
return true if result.equal?(ALL)
|
|
97
107
|
|
|
98
108
|
reason = result.escapes[name]
|
|
@@ -122,11 +132,22 @@ module Rigor
|
|
|
122
132
|
end
|
|
123
133
|
end
|
|
124
134
|
|
|
135
|
+
# {.scan} of `body`, memoised for the run.
|
|
136
|
+
def escape_scan(body)
|
|
137
|
+
return scan(body) if MemoAudit.enabled? && MemoAudit.suspended?
|
|
138
|
+
|
|
139
|
+
table = TypingState.current.weak_key_map(CACHE)
|
|
140
|
+
stored = table[body]
|
|
141
|
+
return table[body] = MemoAudit.key_audit("construction escape") { scan(body) } if stored.nil?
|
|
142
|
+
|
|
143
|
+
MemoAudit.check("construction escape", stored, body) { scan(body) }
|
|
144
|
+
end
|
|
145
|
+
|
|
125
146
|
def scan(body)
|
|
126
147
|
escapes = {}
|
|
127
148
|
calls = {}
|
|
128
149
|
Source::NodeWalker.each_with_ancestors(body) do |node, ancestors|
|
|
129
|
-
return ALL if
|
|
150
|
+
return ALL if frame_access?(node)
|
|
130
151
|
next unless LOCAL_REFERENCES.any? { |k| node.is_a?(k) }
|
|
131
152
|
|
|
132
153
|
record(escapes, calls, node, ancestors)
|
|
@@ -148,10 +169,30 @@ module Rigor
|
|
|
148
169
|
escapes[name] = unsafe.size == CLASSES.size ? ALL : (escapes[name] || Set.new).merge(unsafe)
|
|
149
170
|
end
|
|
150
171
|
|
|
151
|
-
|
|
152
|
-
|
|
172
|
+
# A node that reaches the frame's locals by name: a {FRAME_CALLS} or string-eval call, a `send`-family
|
|
173
|
+
# dispatch on a literal frame-call name (`send(:eval, …)`), or a `TOPLEVEL_BINDING` reference under any
|
|
174
|
+
# qualification (a project's own `X::TOPLEVEL_BINDING` escapes too, costing only precision), whose
|
|
175
|
+
# `local_variable_get` / `local_variable_set` reads and rebinds the top frame.
|
|
176
|
+
def frame_access?(node)
|
|
177
|
+
case node
|
|
178
|
+
when Prism::CallNode then frame_call_name?(node)
|
|
179
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode then node.name == :TOPLEVEL_BINDING
|
|
180
|
+
else false
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def frame_call_name?(node)
|
|
185
|
+
name = node.name
|
|
186
|
+
return true if FRAME_CALLS.include?(name)
|
|
187
|
+
return true if STRING_EVAL_CALLS.include?(name) && !node.arguments.nil?
|
|
188
|
+
return false unless SEND_CALLS.include?(name)
|
|
189
|
+
|
|
190
|
+
arguments = node.arguments&.arguments
|
|
191
|
+
return false if arguments.nil? || arguments.empty?
|
|
153
192
|
|
|
154
|
-
|
|
193
|
+
dispatched = Source::Literals.symbol_or_string(arguments.first)
|
|
194
|
+
FRAME_CALLS.include?(dispatched) ||
|
|
195
|
+
(STRING_EVAL_CALLS.include?(dispatched) && arguments.size > 1)
|
|
155
196
|
end
|
|
156
197
|
|
|
157
198
|
# The call a read outside any closure is the receiver of, or nil.
|
|
@@ -5,3 +5,7 @@
|
|
|
5
5
|
# the indexer, which loads the walk. `ScopeIndexer` itself requires the traversal file rather than this one, which
|
|
6
6
|
# keeps the two free of a require cycle.
|
|
7
7
|
require_relative "scope_indexer"
|
|
8
|
+
|
|
9
|
+
# {Rigor::Reflection.class_declarations_in}'s collectors: the plugin-facing walk lives with `Reflection` but needs the
|
|
10
|
+
# walk above, so it is loaded here rather than by `reflection/declarations`, which the engine-free paths load.
|
|
11
|
+
require_relative "../reflection/declaration_collectors"
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
4
|
require_relative "def_handle"
|
|
5
|
+
require_relative "../typing_state"
|
|
5
6
|
require_relative "../source/node_children"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
@@ -20,23 +21,35 @@ module Rigor
|
|
|
20
21
|
# file, and finally nil — conservative (the accessor then reports no user def, losing precision but never
|
|
21
22
|
# firing a wrong diagnostic), never silent.
|
|
22
23
|
#
|
|
23
|
-
# The memo is a
|
|
24
|
-
#
|
|
24
|
+
# The memo is a {TypingState} Frame installed for the duration of a run ({.with_run}) and restored on the way
|
|
25
|
+
# out: per thread, and a fork child inherits whatever is installed at the fork. Outside a run scope every
|
|
25
26
|
# resolution parses directly — a correct if un-memoised fallback for runner-less probes.
|
|
26
27
|
module DefNodeResolver
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
# ADR-129 WD1 — the run's memo is a Frame: {.with_run} installs it for the run and restores the previous one, and
|
|
29
|
+
# whether one is installed changes answers (outside a run every resolution parses afresh and no `Module.nesting`
|
|
30
|
+
# is recorded). The installed hash is frozen; the four tables in it are extensional caches its owner fills in
|
|
31
|
+
# place without a touch (Memos by kind, whose entry is fixed once written): `nodes` is keyed on
|
|
32
|
+
# `(path, node_id)`, `indexes` and `refinement_queries` on the file's path, and `nestings` on the identity of
|
|
33
|
+
# the node this module minted, holding the nesting the handle's bundle row (or the file's `refine` bodies)
|
|
34
|
+
# recorded for it.
|
|
35
|
+
RUN_MEMO = TypingState.frame(
|
|
36
|
+
:def_node_resolver_run,
|
|
37
|
+
memos: %i[nodes indexes nestings refinement_queries],
|
|
38
|
+
reason: "installed per run by with_run; its tables are extensional caches, keyed on (path, node_id), on the " \
|
|
39
|
+
"path, and (nestings) on the minted node's identity, filled in place",
|
|
40
|
+
# G1′ witness: an incremental recheck types a file against handles of the unchanged files, and
|
|
41
|
+
# {.resolve} reads the run's tables from inside `type_of` (#2874).
|
|
42
|
+
witness: "spec/fixtures/typing_state_witnesses/def_node_resolver_run.rb"
|
|
43
|
+
)
|
|
44
|
+
private_constant :RUN_MEMO
|
|
29
45
|
|
|
30
46
|
# Installs a fresh per-run resolution memo, restoring the previous one on exit (always, even on a raise).
|
|
31
47
|
# `nodes` caches the resolved node per `(path, node_id)`; `indexes` caches each file's parse index;
|
|
32
48
|
# `nestings` re-attaches each handle's recorded `Module.nesting` to the node this module minted for it
|
|
33
49
|
# ({.rehydrated_nesting}).
|
|
34
|
-
def self.with_run
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
yield
|
|
38
|
-
ensure
|
|
39
|
-
Thread.current[MEMO_KEY] = previous
|
|
50
|
+
def self.with_run(&)
|
|
51
|
+
memo = { nodes: {}, indexes: {}, nestings: {}.compare_by_identity, refinement_queries: {} }.freeze
|
|
52
|
+
TypingState.current.with(RUN_MEMO, memo, &)
|
|
40
53
|
end
|
|
41
54
|
|
|
42
55
|
# Whether a per-run memo scope is in force. The rehydrated `Module.nesting` ({.rehydrated_nesting}) is
|
|
@@ -44,7 +57,12 @@ module Rigor
|
|
|
44
57
|
# per-run memo scope" clause states — and lets a spec assert that no handle-consuming entry point
|
|
45
58
|
# resolves outside it, which is the difference between the peel fallback being a documented edge and
|
|
46
59
|
# being silently reachable from production.
|
|
47
|
-
def self.run_scope? = !
|
|
60
|
+
def self.run_scope? = !TypingState.current[RUN_MEMO].nil?
|
|
61
|
+
|
|
62
|
+
# Issue #3160 — the nodes this run's memo has minted so far, keyed `[path, node_id]` (nil outside a run scope).
|
|
63
|
+
# The return memo keys on them, so the runner keeps the table to find an unchanged file's def in the memo once
|
|
64
|
+
# the run scope has closed.
|
|
65
|
+
def self.resolved_nodes = TypingState.current[RUN_MEMO]&.fetch(:nodes)
|
|
48
66
|
|
|
49
67
|
# Resolves `handle` to a `Prism::DefNode` (the same object across the run for a given (path, node_id)), or
|
|
50
68
|
# nil. A non-handle argument is returned unchanged, so callers can pass a table value that is either a live
|
|
@@ -52,7 +70,7 @@ module Rigor
|
|
|
52
70
|
def self.resolve(handle)
|
|
53
71
|
return handle unless handle.is_a?(DefHandle)
|
|
54
72
|
|
|
55
|
-
memo =
|
|
73
|
+
memo = TypingState.current[RUN_MEMO]
|
|
56
74
|
return locate(handle, {}) if memo.nil?
|
|
57
75
|
|
|
58
76
|
key = [handle.path, handle.node_id].freeze
|
|
@@ -96,7 +114,7 @@ module Rigor
|
|
|
96
114
|
# def_node_resolver_spec.rb` pins that, so a new one added outside a run scope fails there rather than
|
|
97
115
|
# silently reverting to the peel.
|
|
98
116
|
def self.rehydrated_nesting(node)
|
|
99
|
-
memo =
|
|
117
|
+
memo = TypingState.current[RUN_MEMO]
|
|
100
118
|
memo && memo[:nestings][node]
|
|
101
119
|
end
|
|
102
120
|
|
|
@@ -117,7 +135,7 @@ module Rigor
|
|
|
117
135
|
# read: the typed refined arm reads another file's refine body through it. The bodies' `Module.nesting`
|
|
118
136
|
# is recorded as a handle's is, so the re-typed body reads its constants where it is written.
|
|
119
137
|
def self.refinement_query(path)
|
|
120
|
-
memo =
|
|
138
|
+
memo = TypingState.current[RUN_MEMO]
|
|
121
139
|
return build_refinement_query(path, nil) if memo.nil?
|
|
122
140
|
|
|
123
141
|
queries = memo[:refinement_queries]
|