rigortype 0.3.9 → 0.4.0
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 +2 -2
- data/data/core_overlay/enumerable.rbs +51 -0
- data/data/core_overlay/enumerator.rbs +84 -0
- data/data/core_overlay/hash_rbs3.rbs +41 -0
- data/data/core_overlay/process.rbs +40 -0
- data/data/core_overlay/string_io.rbs +33 -0
- data/data/effects/core.yml +3 -3
- data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
- data/docs/handbook/03-narrowing.md +95 -10
- data/docs/handbook/04-tuples-and-shapes.md +8 -6
- data/docs/handbook/07-rbs-and-extended.md +16 -11
- data/docs/handbook/10-sorbet.md +9 -10
- data/docs/handbook/11-sig-gen.md +454 -13
- data/docs/manual/02-cli-reference.md +63 -2
- data/docs/manual/03-configuration.md +7 -0
- data/docs/manual/04-diagnostics.md +4 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/10-mcp-server.md +3 -2
- data/docs/manual/16-rbs-extended-annotations.md +40 -7
- data/docs/manual/19-effect-labels.md +10 -0
- data/docs/manual/plugins/README.md +7 -0
- data/docs/manual/plugins/rigor-actioncable.md +8 -1
- data/docs/manual/plugins/rigor-actionmailer.md +7 -0
- data/docs/manual/plugins/rigor-actionpack.md +243 -0
- data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
- data/docs/manual/plugins/rigor-activejob.md +7 -0
- data/docs/manual/plugins/rigor-activerecord.md +110 -4
- data/docs/manual/plugins/rigor-activestorage.md +8 -1
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
- data/docs/manual/plugins/rigor-grape.md +106 -0
- data/docs/manual/plugins/rigor-graphql.md +23 -2
- data/docs/manual/plugins/rigor-pundit.md +8 -1
- data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
- data/docs/manual/plugins/rigor-rails-routes.md +7 -0
- data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
- data/docs/manual/plugins/rigor-sidekiq.md +8 -1
- data/docs/manual/plugins/rigor-sorbet.md +21 -5
- data/exe/rigor +19 -4
- data/lib/rigor/analysis/baseline.rb +1 -1
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
- data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
- data/lib/rigor/analysis/check_rules.rb +520 -49
- data/lib/rigor/analysis/dependency_recorder.rb +23 -0
- data/lib/rigor/analysis/fact_store.rb +9 -0
- data/lib/rigor/analysis/incremental.rb +26 -0
- data/lib/rigor/analysis/incremental_session.rb +36 -4
- data/lib/rigor/analysis/project_scan.rb +11 -1
- data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
- data/lib/rigor/analysis/reachability/scan_cache.rb +1 -0
- data/lib/rigor/analysis/rule_catalog.rb +127 -0
- data/lib/rigor/analysis/run_cache_key.rb +20 -11
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +167 -11
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
- data/lib/rigor/analysis/runner/pool_coordinator.rb +107 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +26 -7
- data/lib/rigor/analysis/runner.rb +278 -20
- data/lib/rigor/analysis/template_unit_collector.rb +303 -0
- data/lib/rigor/analysis/template_unit_paths.rb +91 -0
- data/lib/rigor/analysis/template_unit_positions.rb +293 -0
- data/lib/rigor/analysis/template_units.rb +399 -0
- data/lib/rigor/analysis/worker_session.rb +53 -10
- data/lib/rigor/bleeding_edge.rb +0 -2
- data/lib/rigor/builtins/hkt_builtins.rb +1 -0
- data/lib/rigor/builtins/imported_refinements.rb +4 -0
- data/lib/rigor/builtins/regex_refinement.rb +17 -9
- data/lib/rigor/builtins/static_return_refinements.rb +2 -0
- data/lib/rigor/cache/descriptor.rb +6 -1
- data/lib/rigor/cache/engine_source.rb +29 -1
- data/lib/rigor/cache/incremental_snapshot.rb +33 -1
- data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
- data/lib/rigor/cache/rbs_descriptor.rb +37 -7
- data/lib/rigor/cache/store.rb +0 -1
- data/lib/rigor/ci_detector.rb +1 -0
- data/lib/rigor/cli/doc_links.rb +1 -1
- data/lib/rigor/cli/docs_command.rb +4 -4
- data/lib/rigor/cli/plugin_command.rb +3 -3
- data/lib/rigor/cli/prism_colorizer.rb +0 -1
- data/lib/rigor/cli/sig_gen_command.rb +210 -29
- data/lib/rigor/cli/skill_command.rb +1 -1
- data/lib/rigor/cli/skill_describe.rb +0 -1
- data/lib/rigor/cli/type_of_command.rb +19 -5
- data/lib/rigor/cli/type_of_renderer.rb +20 -9
- data/lib/rigor/cli/type_of_template_probe.rb +189 -0
- data/lib/rigor/cli.rb +21 -3
- data/lib/rigor/configuration/severity_profile.rb +19 -3
- data/lib/rigor/configuration.rb +66 -3
- data/lib/rigor/effects/ancestry_recorder.rb +191 -0
- data/lib/rigor/effects/attribution.rb +11 -2
- data/lib/rigor/effects/callee_rule.rb +368 -0
- data/lib/rigor/effects/catalog.rb +7 -4
- data/lib/rigor/effects/collector.rb +11 -5
- data/lib/rigor/effects/config_envelopes.rb +9 -2
- data/lib/rigor/effects/definition_context.rb +179 -0
- data/lib/rigor/effects/effect_table.rb +11 -3
- data/lib/rigor/effects/envelope_check.rb +1 -1
- data/lib/rigor/effects/envelope_index.rb +15 -0
- data/lib/rigor/effects/file_collection.rb +59 -5
- data/lib/rigor/effects/framework_units.rb +1 -1
- data/lib/rigor/effects/identity.rb +16 -0
- data/lib/rigor/effects/local_ownership.rb +38 -12
- data/lib/rigor/effects/method_key.rb +21 -0
- data/lib/rigor/effects/mutation_classifier.rb +23 -12
- data/lib/rigor/effects/plugin_facts.rb +43 -31
- data/lib/rigor/effects/propagator.rb +295 -16
- data/lib/rigor/effects/registry.rb +1 -1
- data/lib/rigor/effects/scanner.rb +121 -72
- data/lib/rigor/effects/signature_sources.rb +1 -1
- data/lib/rigor/effects/snapshot.rb +2 -1
- data/lib/rigor/effects/summary.rb +27 -4
- data/lib/rigor/effects/unit_scan.rb +385 -30
- data/lib/rigor/effects/visibility.rb +101 -0
- data/lib/rigor/environment/lockfile_resolver.rb +17 -0
- data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
- data/lib/rigor/environment/member_consistency.rb +298 -0
- data/lib/rigor/environment/rbs_loader.rb +399 -133
- data/lib/rigor/environment.rb +103 -38
- data/lib/rigor/hashing/xxh3.rb +264 -0
- data/lib/rigor/inference/acceptance.rb +139 -9
- data/lib/rigor/inference/block_auto_splat.rb +216 -0
- data/lib/rigor/inference/block_call_timing.rb +338 -0
- data/lib/rigor/inference/block_parameter_binder.rb +73 -27
- data/lib/rigor/inference/block_repetition.rb +71 -0
- data/lib/rigor/inference/body_fixpoint.rb +2 -1
- data/lib/rigor/inference/budget_trace.rb +2 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
- data/lib/rigor/inference/captured_locals.rb +387 -15
- data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
- data/lib/rigor/inference/content_join.rb +200 -27
- data/lib/rigor/inference/def_return_typer.rb +11 -7
- data/lib/rigor/inference/define_method_block_self.rb +64 -0
- data/lib/rigor/inference/element_read_widening.rb +22 -10
- data/lib/rigor/inference/error_info.rb +196 -0
- data/lib/rigor/inference/expression_typer.rb +1532 -496
- data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
- data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
- data/lib/rigor/inference/global_write_census.rb +239 -0
- data/lib/rigor/inference/guard_rebinding.rb +447 -0
- data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
- data/lib/rigor/inference/index_write_widening.rb +16 -3
- data/lib/rigor/inference/indexed_narrowing.rb +61 -9
- data/lib/rigor/inference/jump_targets.rb +82 -0
- data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
- data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
- data/lib/rigor/inference/last_line.rb +340 -0
- data/lib/rigor/inference/last_status.rb +144 -0
- data/lib/rigor/inference/macro_block_self_type.rb +167 -17
- data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
- data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
- data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
- data/lib/rigor/inference/match_rebinding.rb +392 -0
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
- data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
- data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
- data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +881 -38
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +214 -13
- data/lib/rigor/inference/method_parameter_binder.rb +8 -3
- data/lib/rigor/inference/multi_target_binder.rb +340 -52
- data/lib/rigor/inference/mutation_rejoin.rb +4 -1
- data/lib/rigor/inference/mutation_widening.rb +70 -48
- data/lib/rigor/inference/narrowing.rb +540 -120
- data/lib/rigor/inference/operand_effects.rb +167 -0
- data/lib/rigor/inference/operand_walk.rb +88 -0
- data/lib/rigor/inference/optimistic_origin.rb +152 -9
- data/lib/rigor/inference/parameter_inference_collector.rb +3 -2
- data/lib/rigor/inference/project_method_ownership.rb +136 -0
- data/lib/rigor/inference/project_patched_methods.rb +7 -2
- data/lib/rigor/inference/project_patched_scanner.rb +7 -3
- data/lib/rigor/inference/receiver_alias.rb +90 -1
- data/lib/rigor/inference/receiver_blind_block.rb +219 -0
- data/lib/rigor/inference/refinement_mutation.rb +15 -11
- data/lib/rigor/inference/repeated_or_writes.rb +463 -0
- data/lib/rigor/inference/return_barrier.rb +54 -0
- data/lib/rigor/inference/rewrite_mutation.rb +120 -0
- data/lib/rigor/inference/scope_indexer.rb +4615 -551
- data/lib/rigor/inference/statement_evaluator.rb +3176 -490
- data/lib/rigor/inference/stored_block_call.rb +54 -0
- data/lib/rigor/inference/string_mutation.rb +44 -7
- data/lib/rigor/inference/unknown_store_widening.rb +200 -0
- data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
- data/lib/rigor/language_server/debouncer.rb +0 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
- data/lib/rigor/language_server/hover_renderer.rb +3 -3
- data/lib/rigor/language_server/project_context.rb +5 -3
- data/lib/rigor/mcp/server.rb +2 -1
- data/lib/rigor/plugin/base.rb +168 -5
- data/lib/rigor/plugin/box_probe.rb +91 -0
- data/lib/rigor/plugin/bundled_catalog.rb +1 -1
- data/lib/rigor/plugin/effect_attribution.rb +58 -4
- data/lib/rigor/plugin/loader.rb +2 -1
- data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
- data/lib/rigor/plugin/manifest.rb +71 -10
- data/lib/rigor/plugin/registry.rb +35 -1
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
- data/lib/rigor/plugin/template_unit.rb +196 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/protection/discovery_seed.rb +3 -1
- data/lib/rigor/protection/kill_signature.rb +0 -1
- data/lib/rigor/protection/mutation_cache.rb +1 -2
- data/lib/rigor/rbs_extended.rb +27 -0
- data/lib/rigor/reflection/constant_ancestors.rb +97 -0
- data/lib/rigor/reflection/constant_path.rb +19 -6
- data/lib/rigor/reflection.rb +72 -105
- data/lib/rigor/scope/discovery_index.rb +135 -2
- data/lib/rigor/scope.rb +859 -49
- data/lib/rigor/sig_gen/alias_index.rb +289 -0
- data/lib/rigor/sig_gen/classification.rb +22 -5
- data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
- data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
- data/lib/rigor/sig_gen/generator.rb +558 -31
- data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
- data/lib/rigor/sig_gen/method_candidate.rb +47 -3
- data/lib/rigor/sig_gen/observation_collector.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +124 -9
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
- data/lib/rigor/sig_gen/write_result.rb +19 -3
- data/lib/rigor/sig_gen/writer.rb +166 -35
- data/lib/rigor/sig_gen.rb +3 -0
- data/lib/rigor/signature_path_audit.rb +1 -1
- data/lib/rigor/source/node_walker.rb +0 -3
- data/lib/rigor/source/parameter_envelope.rb +72 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/combinator.rb +88 -12
- data/lib/rigor/type/difference.rb +1 -0
- data/lib/rigor/type/hash_shape.rb +1 -1
- data/lib/rigor/type/refined.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
- data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
- data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
- data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
- data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
- data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
- data/plugins/rigor-grape/sig/grape.rbs +263 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
- data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
- data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
- data/sig/rigor/analysis/baseline.rbs +69 -7
- data/sig/rigor/analysis/fact_store.rbs +1 -1
- data/sig/rigor/analysis/project_scan.rbs +74 -0
- data/sig/rigor/effects/config_envelopes.rbs +100 -0
- data/sig/rigor/effects/effect_table.rbs +60 -0
- data/sig/rigor/effects/envelope.rbs +97 -0
- data/sig/rigor/effects/envelope_index.rbs +33 -0
- data/sig/rigor/effects/file_collection.rbs +81 -0
- data/sig/rigor/effects/label.rbs +26 -0
- data/sig/rigor/effects/label_set.rbs +46 -0
- data/sig/rigor/effects/method_key.rbs +24 -0
- data/sig/rigor/effects/origin.rbs +50 -0
- data/sig/rigor/effects/plugin_facts.rbs +141 -0
- data/sig/rigor/effects/registry.rbs +68 -0
- data/sig/rigor/effects/summary.rbs +50 -0
- data/sig/rigor/effects/taint_cause.rbs +12 -0
- data/sig/rigor/environment.rbs +12 -10
- data/sig/rigor/inference/optimistic_origin.rbs +10 -0
- data/sig/rigor/inference.rbs +6 -4
- data/sig/rigor/plugin/additional_initializer.rbs +36 -0
- data/sig/rigor/plugin/base.rbs +30 -7
- data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
- data/sig/rigor/plugin/effect_attribution.rbs +66 -0
- data/sig/rigor/plugin/effect_edge.rbs +33 -0
- data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
- data/sig/rigor/plugin/io_boundary.rbs +1 -1
- data/sig/rigor/plugin/loader.rbs +3 -3
- data/sig/rigor/plugin/manifest.rbs +50 -11
- data/sig/rigor/plugin/protocol_contract.rbs +68 -0
- data/sig/rigor/plugin/registry.rbs +62 -1
- data/sig/rigor/plugin.rbs +1 -1
- data/sig/rigor/rbs_extended.rbs +1 -1
- data/sig/rigor/reflection.rbs +7 -6
- data/sig/rigor/scope.rbs +135 -20
- data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
- data/sig/rigor/source.rbs +4 -4
- data/sig/rigor/testing.rbs +10 -4
- data/sig/rigor/type.rbs +6 -0
- data/sig/rigor.rbs +38 -20
- data/skills/rigor-ask/SKILL.md +8 -5
- data/skills/rigor-baseline-reduce/SKILL.md +4 -6
- data/skills/rigor-ci-setup/SKILL.md +17 -21
- data/skills/rigor-doctor/SKILL.md +24 -22
- data/skills/rigor-doctor/references/01-checks.md +97 -33
- data/skills/rigor-editor-setup/SKILL.md +6 -4
- data/skills/rigor-mcp-setup/SKILL.md +5 -4
- data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
- data/skills/rigor-next-steps/SKILL.md +4 -2
- data/skills/rigor-plugin-author/SKILL.md +19 -23
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
- data/skills/rigor-plugin-review/SKILL.md +6 -4
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
- data/skills/rigor-plugin-tune/SKILL.md +4 -2
- data/skills/rigor-project-init/SKILL.md +9 -7
- data/skills/rigor-project-init/references/01-detect.md +13 -9
- data/skills/rigor-project-init/references/02-configure.md +33 -8
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
- data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
- data/skills/rigor-protection-uplift/SKILL.md +4 -6
- data/skills/rigor-rbs-setup/SKILL.md +4 -2
- data/skills/rigor-type-oracle/SKILL.md +4 -6
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
- data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
- data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
- data/skills/rigor-upgrade/SKILL.md +13 -8
- metadata +108 -1
|
@@ -4,6 +4,12 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../source/node_walker"
|
|
6
6
|
require_relative "block_parameter_binder"
|
|
7
|
+
require_relative "element_read_widening"
|
|
8
|
+
require_relative "index_write_widening"
|
|
9
|
+
require_relative "mutation_widening"
|
|
10
|
+
require_relative "optimistic_origin"
|
|
11
|
+
require_relative "receiver_alias"
|
|
12
|
+
require_relative "unknown_store_widening"
|
|
7
13
|
|
|
8
14
|
module Rigor
|
|
9
15
|
module Inference
|
|
@@ -16,11 +22,21 @@ module Rigor
|
|
|
16
22
|
# A write counts across every local-write form — plain `=` (`LocalVariableWriteNode`), the operator /
|
|
17
23
|
# `||=` / `&&=` compound forms, and a multi-assign target (`x, y = …` → `LocalVariableTargetNode` under
|
|
18
24
|
# a `MultiWriteNode`) — at ANY depth: a block is a closure, so a write inside a nested block binds the
|
|
19
|
-
# same outer variable. Block-introduced names (parameters, numbered parameters, `;`-locals)
|
|
20
|
-
# not bound in the outer scope
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
# same outer variable. Block-introduced names (parameters, numbered parameters, `;`-locals), names
|
|
26
|
+
# not bound in the outer scope, a write a nested block's own parameter or block-local shadows, and a
|
|
27
|
+
# write inside a nested `def` or class body ({.outer_local?}) are excluded; a write to any of them is
|
|
28
|
+
# not a captured rebind of an outer variable.
|
|
29
|
+
#
|
|
30
|
+
# All three also ask for the instance variables the body rebinds (`ivars: true`). The per-element fold asks
|
|
31
|
+
# for every other binding that outlives an iteration as well (`non_locals: true`): the class variables and
|
|
32
|
+
# globals the body rebinds, and the instance variable behind an attribute setter it calls on `self`. Names
|
|
33
|
+
# keep their sigil, so a map over every kind never collides, and {.bound_type} / {.bind} reach each name
|
|
34
|
+
# through its own kind of binding.
|
|
35
|
+
#
|
|
36
|
+
# {.content_mutations} is the sibling set on the same terms: the captured outer locals the body mutates
|
|
37
|
+
# IN PLACE rather than rebinds, which the rebind set cannot see and the per-element fold needs as well.
|
|
38
|
+
module CapturedLocals # rubocop:disable Metrics/ModuleLength
|
|
39
|
+
LOCAL_WRITE_NODES = Set[
|
|
24
40
|
Prism::LocalVariableWriteNode,
|
|
25
41
|
Prism::LocalVariableOperatorWriteNode,
|
|
26
42
|
Prism::LocalVariableOrWriteNode,
|
|
@@ -28,24 +44,380 @@ module Rigor
|
|
|
28
44
|
Prism::LocalVariableTargetNode
|
|
29
45
|
].freeze
|
|
30
46
|
|
|
47
|
+
# The write forms of the bindings other than locals that outlive an iteration: instance variables, class
|
|
48
|
+
# variables and globals.
|
|
49
|
+
NON_LOCAL_WRITE_NODES = Set[
|
|
50
|
+
Prism::InstanceVariableWriteNode,
|
|
51
|
+
Prism::InstanceVariableOperatorWriteNode,
|
|
52
|
+
Prism::InstanceVariableOrWriteNode,
|
|
53
|
+
Prism::InstanceVariableAndWriteNode,
|
|
54
|
+
Prism::InstanceVariableTargetNode,
|
|
55
|
+
Prism::ClassVariableWriteNode,
|
|
56
|
+
Prism::ClassVariableOperatorWriteNode,
|
|
57
|
+
Prism::ClassVariableOrWriteNode,
|
|
58
|
+
Prism::ClassVariableAndWriteNode,
|
|
59
|
+
Prism::ClassVariableTargetNode,
|
|
60
|
+
Prism::GlobalVariableWriteNode,
|
|
61
|
+
Prism::GlobalVariableOperatorWriteNode,
|
|
62
|
+
Prism::GlobalVariableOrWriteNode,
|
|
63
|
+
Prism::GlobalVariableAndWriteNode,
|
|
64
|
+
Prism::GlobalVariableTargetNode
|
|
65
|
+
].freeze
|
|
66
|
+
|
|
67
|
+
# The call forms that invoke an attribute setter: `self.w = v`, the compound `self.w += v` / `||=` / `&&=`,
|
|
68
|
+
# and a multi-assign target `self.w, x = …`.
|
|
69
|
+
SETTER_CALL_NODES = Set[
|
|
70
|
+
Prism::CallNode, Prism::CallOperatorWriteNode, Prism::CallOrWriteNode, Prism::CallAndWriteNode,
|
|
71
|
+
Prism::CallTargetNode
|
|
72
|
+
].freeze
|
|
73
|
+
private_constant :SETTER_CALL_NODES
|
|
74
|
+
|
|
75
|
+
# An attribute writer's name: an identifier followed by `=`. `==`, `!=`, `<=`, `>=`, `===` and `[]=` are
|
|
76
|
+
# not setters.
|
|
77
|
+
SETTER_NAME = /\A[A-Za-z_]\w*=\z/
|
|
78
|
+
private_constant :SETTER_NAME
|
|
79
|
+
|
|
80
|
+
# The binding the per-element fold lays under a block's parameters: each name's type across iterations, and
|
|
81
|
+
# the optimistic nil-freeness mark an iteration's own rebind gave it, which {.bind} adds to the one the
|
|
82
|
+
# call site already holds — as `Scope#join` unions the marks of the scopes it joins. `repeated` is the
|
|
83
|
+
# `RepeatedOrWrites::Marks` of the index `||=` sites whose slot an earlier run may have filled, marked on the
|
|
84
|
+
# same scope: those of the per-element fold's `position`, or every site when no position is given.
|
|
85
|
+
Bindings = Data.define(:types, :marks, :repeated) do
|
|
86
|
+
def names = types.keys
|
|
87
|
+
|
|
88
|
+
def lay(scope, position: nil)
|
|
89
|
+
bound = types.reduce(scope) do |acc, (name, type)|
|
|
90
|
+
CapturedLocals.bind(acc, name, type, optimistic: marks[name])
|
|
91
|
+
end
|
|
92
|
+
bound.with_repeated_or_writes(repeated.at(position))
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The empty answers, shared: every block-bearing call's return pass asks both questions, and nearly every
|
|
97
|
+
# body answers nothing to either.
|
|
98
|
+
NO_NAMES = [].freeze
|
|
99
|
+
NO_SITES = {}.freeze
|
|
100
|
+
private_constant :NO_NAMES, :NO_SITES
|
|
101
|
+
|
|
31
102
|
module_function
|
|
32
103
|
|
|
33
104
|
# @param base_scope — the call-site scope the block closes over.
|
|
105
|
+
# @param ivars — also collect the instance variables the body rebinds. An ivar is not captured — the
|
|
106
|
+
# block shares the caller's `self` — but it outlives an iteration, and the call, exactly as a captured
|
|
107
|
+
# local does. It counts on the same terms as a local (every write form, any depth, bound in
|
|
108
|
+
# `base_scope`), except that one still on its class-wide binding does not: ADR-58's declaration seed is
|
|
109
|
+
# the union of every write in the class, this body's included, so nothing the body stores can move it,
|
|
110
|
+
# and rebinding it would only drop the declaration mark that keeps its nil from being diagnostic fuel.
|
|
111
|
+
# A nested block that rebinds `self` (`o.instance_eval`) writes another object's ivar, and a nested
|
|
112
|
+
# `def` runs only when called; both still count, because an `instance_eval` without a receiver, or a
|
|
113
|
+
# call to that `def` inside the body, does write this one.
|
|
114
|
+
# @param non_locals — the per-element fold's superset of `ivars`: the class variables and globals the body
|
|
115
|
+
# rebinds count too, on the same terms, and so does an attribute setter called on `self` (`self.w = v`)
|
|
116
|
+
# as a rebind of the instance variable it is named after, `@w`: the `attr_writer` / `attr_accessor`
|
|
117
|
+
# convention stores there, and no write node shows it. A hand-written setter storing elsewhere is not
|
|
118
|
+
# seen.
|
|
34
119
|
# @return the captured names the body writes, each once, in first-write order.
|
|
35
|
-
def writes(block_node, base_scope)
|
|
120
|
+
def writes(block_node, base_scope, ivars: false, non_locals: false)
|
|
121
|
+
body = block_node.body
|
|
122
|
+
return NO_NAMES if body.nil?
|
|
123
|
+
|
|
124
|
+
introduced = nil
|
|
125
|
+
names = nil
|
|
126
|
+
outliving = non_locals || ivars
|
|
127
|
+
Source::NodeWalker.each_with_ancestors(body) do |descendant, ancestors|
|
|
128
|
+
name =
|
|
129
|
+
if LOCAL_WRITE_NODES.include?(descendant.class)
|
|
130
|
+
captured_local_write(descendant, ancestors, base_scope) { introduced ||= introduced_locals(block_node) }
|
|
131
|
+
elsif outliving
|
|
132
|
+
rebindable_outliving_write(descendant, base_scope, non_locals)
|
|
133
|
+
end
|
|
134
|
+
(names ||= []) << name if name
|
|
135
|
+
end
|
|
136
|
+
names ? names.uniq : NO_NAMES
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def rebindable_outliving_write(node, base_scope, non_locals)
|
|
140
|
+
name = outliving_write_name(node, non_locals)
|
|
141
|
+
name if name && rebindable_non_local?(base_scope, name)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# The outer local a local-write node rebinds, or nil when the call site does not bind it, the write resolves
|
|
145
|
+
# inside a nested block or `def` ({.outer_local?}), or the block introduces it (the block yields the
|
|
146
|
+
# introduced set, computed only when needed).
|
|
147
|
+
def captured_local_write(node, ancestors, base_scope)
|
|
148
|
+
name = node.name
|
|
149
|
+
return nil unless base_scope.locals.key?(name) && outer_local?(node, ancestors)
|
|
150
|
+
|
|
151
|
+
yield.include?(name) ? nil : name
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# The non-local `node` rebinds under `non_locals:` — or, without it, the instance variable only.
|
|
155
|
+
def outliving_write_name(node, non_locals)
|
|
156
|
+
name = non_local_write_name(node)
|
|
157
|
+
non_locals || (name && ivar_name?(name) && NON_LOCAL_WRITE_NODES.include?(node.class)) ? name : nil
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# The instance, class or global variable `node` rebinds, or nil when it rebinds none of them.
|
|
161
|
+
def non_local_write_name(node)
|
|
162
|
+
return node.name if NON_LOCAL_WRITE_NODES.include?(node.class)
|
|
163
|
+
|
|
164
|
+
setter_ivar_name(node)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# `@w` for an attribute setter called on `self` (`self.w = …` and its compound and target forms), else nil.
|
|
168
|
+
def setter_ivar_name(node)
|
|
169
|
+
return nil unless SETTER_CALL_NODES.include?(node.class) && node.receiver.is_a?(Prism::SelfNode)
|
|
170
|
+
|
|
171
|
+
setter = node.respond_to?(:write_name) ? node.write_name : node.name
|
|
172
|
+
return nil unless SETTER_NAME.match?(setter)
|
|
173
|
+
|
|
174
|
+
:"@#{setter.to_s.delete_suffix('=')}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def rebindable_non_local?(scope, name)
|
|
178
|
+
variable_kind(name) == :ivar ? rebindable_ivar?(scope, name) : !bound_type(scope, name).nil?
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def rebindable_ivar?(scope, name)
|
|
182
|
+
!scope.ivar(name).nil? && !scope.declaration_sourced?(:ivar, name)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# The binding `scope` holds for a name from {.writes}.
|
|
186
|
+
def bound_type(scope, name)
|
|
187
|
+
case variable_kind(name)
|
|
188
|
+
when :ivar then scope.ivar(name)
|
|
189
|
+
when :cvar then scope.cvar(name)
|
|
190
|
+
when :global then scope.global(name)
|
|
191
|
+
else scope.local(name)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# `scope` with a name from {.writes} bound to `type`, keeping the name's optimistic nil-freeness mark
|
|
196
|
+
# (issue #286). `Scope#with_local` / `#with_ivar` drop it as a fresh write should, but here `type`
|
|
197
|
+
# stands for the binding across iterations, and a value that was nil-free only optimistically still is:
|
|
198
|
+
# without the mark `x.nil?` folds to `false` where the runtime answers `true`. `optimistic` is a mark the
|
|
199
|
+
# binding carries besides the one `scope` already holds — one an iteration's own rebind made. Class
|
|
200
|
+
# variables and globals carry no mark. {OptimisticOrigin.carry_local_mark} owns the re-marking, and with
|
|
201
|
+
# it the miss answer the mark records (issue #1302).
|
|
202
|
+
def bind(scope, name, type, optimistic: nil)
|
|
203
|
+
case variable_kind(name)
|
|
204
|
+
when :ivar then OptimisticOrigin.carry_ivar_mark(scope, scope.with_ivar(name, type), name, optimistic)
|
|
205
|
+
when :cvar then scope.with_cvar(name, type)
|
|
206
|
+
when :global then scope.with_global(name, type)
|
|
207
|
+
else OptimisticOrigin.carry_local_mark(scope, scope.with_local(name, type), name, optimistic)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# The optimistic nil-freeness mark `scope` holds for a name from {.writes}, or nil.
|
|
212
|
+
def optimistic_mark(scope, name)
|
|
213
|
+
case variable_kind(name)
|
|
214
|
+
when :ivar then scope.optimistic_ivar(name)
|
|
215
|
+
when :local then scope.optimistic_local(name)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def ivar_name?(name) = variable_kind(name) == :ivar
|
|
220
|
+
|
|
221
|
+
# Ruby spells a global with a leading `$`, a class variable with `@@`, an instance variable with a single
|
|
222
|
+
# `@`, and a local with none of them.
|
|
223
|
+
def variable_kind(name)
|
|
224
|
+
if name.start_with?("$") then :global
|
|
225
|
+
elsif name.start_with?("@@") then :cvar
|
|
226
|
+
elsif name.start_with?("@") then :ivar
|
|
227
|
+
else :local
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# The nodes that change a receiver's CONTENT without rebinding it: a call to a name the straight-line
|
|
232
|
+
# widening responds to ({MutationWidening::SHAPE_MUTATORS}), and the index writes that store through
|
|
233
|
+
# `[]=` without being a `[]=` call ({IndexWriteWidening::CONTENT_WRITE_NODE_CLASSES}, the one list the
|
|
234
|
+
# block-return threading gate and the captured-local write-back read too).
|
|
235
|
+
INDEX_STORE_NODES = IndexWriteWidening::CONTENT_WRITE_NODE_CLASSES
|
|
236
|
+
private_constant :INDEX_STORE_NODES
|
|
237
|
+
|
|
238
|
+
# The captured outer locals the body mutates in place, each mapped to its mutation sites (the nodes
|
|
239
|
+
# above) in source order. A site counts through every variable its receiver can evaluate to
|
|
240
|
+
# ({ReceiverAlias.candidates}), at any nesting depth as long as a local's read does not resolve inside a nested
|
|
241
|
+
# block ({.outer_local?}), and a local is excluded on exactly the terms {.writes} excludes it.
|
|
242
|
+
#
|
|
243
|
+
# Under `non_locals: true` the instance variables, class variables and globals the body mutates in place
|
|
244
|
+
# count too, on the terms {.writes} takes a rebound one ({.rebindable_non_local?}). Since the block-return
|
|
245
|
+
# threading gate threads an index write, `@cache[:first] ||= e; @cache[:first] == 2` would otherwise type
|
|
246
|
+
# every position from the empty entry hash, store THAT position's `e`, and fold `find` to `2` where Ruby,
|
|
247
|
+
# keeping the first iteration's `1`, answers `nil`; `$seen << x; n == 1` after `n = $seen.size` folded
|
|
248
|
+
# `find` to `nil` the same way. A class variable or global counts only as the receiver itself (parentheses
|
|
249
|
+
# aside), not through a branch that selects it.
|
|
250
|
+
#
|
|
251
|
+
# A local also counts through the two routes straight-line code widens it by without reading it as the
|
|
252
|
+
# receiver. A mutator on an element read rooted at it (`a[0] << e`, the local {ElementReadWidening}
|
|
253
|
+
# widens) is a site of that local. A self-call whose callee content-mutates the parameter a local is passed
|
|
254
|
+
# to (`add_to(a, e)`, the local `StatementEvaluator#content_mutated_arguments` reports) is a
|
|
255
|
+
# {UnknownStoreWidening::CalleeStore} site of it. Both were invisible here, so every position of the fold
|
|
256
|
+
# read the local at its entry contents: `a = [[1]]; [1, 2].map { |e| v = a[0].size; a[0] << e; v }`
|
|
257
|
+
# folded to `[1, 1]` where Ruby answers `[1, 2]`.
|
|
258
|
+
#
|
|
259
|
+
# @param base_scope — the call-site scope the block closes over.
|
|
260
|
+
# @param non_locals — also collect the instance variables, class variables and globals the body mutates
|
|
261
|
+
# in place.
|
|
262
|
+
# @return `{ name => [site, ...] }`, empty for the overwhelmingly common body that mutates nothing
|
|
263
|
+
# captured.
|
|
264
|
+
def content_mutations(block_node, base_scope, non_locals: false)
|
|
36
265
|
body = block_node.body
|
|
37
|
-
return
|
|
266
|
+
return NO_SITES if body.nil?
|
|
267
|
+
|
|
268
|
+
introduced = nil
|
|
269
|
+
body_mutation_sites(body, base_scope, non_locals) { introduced ||= introduced_locals(block_node) }
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Issue #1412 — false when `node` certainly holds nothing {.writes} (with `ivars: true`) or
|
|
273
|
+
# {.content_mutations} could collect against `base_scope`: no write to a local it binds, no instance, class or
|
|
274
|
+
# global variable write, no mutator or index store, and no self-call passing a local. It over-approximates
|
|
275
|
+
# (a write a block parameter shadows still answers true) and allocates nothing, so a caller can skip both
|
|
276
|
+
# scans, and whatever it would compute only to use their answers, for the common body that touches no capture.
|
|
277
|
+
def may_touch_capture?(node, base_scope)
|
|
278
|
+
return true if capture_touch?(node, base_scope)
|
|
279
|
+
|
|
280
|
+
node.rigor_each_child { |child| return true if may_touch_capture?(child, base_scope) }
|
|
281
|
+
false
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def capture_touch?(node, base_scope)
|
|
285
|
+
klass = node.class
|
|
286
|
+
return base_scope.locals.key?(node.name) if LOCAL_WRITE_NODES.include?(klass)
|
|
287
|
+
return true if NON_LOCAL_WRITE_NODES.include?(klass) || INDEX_STORE_NODES.include?(klass)
|
|
288
|
+
return false unless klass == Prism::CallNode
|
|
289
|
+
|
|
290
|
+
receiver = node.receiver
|
|
291
|
+
return true if receiver && MutationWidening::SHAPE_MUTATORS.include?(node.name)
|
|
292
|
+
|
|
293
|
+
callee_call?(node)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Issue #1412 — {.content_mutations} for a `while` / `until` / `for` body: the locals bound in `base_scope`
|
|
297
|
+
# (the scope the loop body first enters from) that the body mutates in place. A loop body opens no scope and
|
|
298
|
+
# introduces no name, so every such local counts on the same depth terms a block's capture does — a read
|
|
299
|
+
# that resolves inside a nested block or lambda, or inside a nested `def` or class body, is not the loop's.
|
|
300
|
+
#
|
|
301
|
+
# @return `{ name => [site, ...] }`, empty for the common body that mutates nothing.
|
|
302
|
+
def loop_content_mutations(statements, base_scope)
|
|
303
|
+
return NO_SITES if statements.nil?
|
|
304
|
+
|
|
305
|
+
body_mutation_sites(statements, base_scope, false) { NO_INTRODUCED }
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
NO_INTRODUCED = Set.new.freeze
|
|
309
|
+
private_constant :NO_INTRODUCED
|
|
310
|
+
|
|
311
|
+
# The mutation sites under `body` whose reads name a variable {.captured_target?} accepts, keyed by name.
|
|
312
|
+
# The block yields the names the enclosing construct introduces, asked only when a local site is found.
|
|
313
|
+
def body_mutation_sites(body, base_scope, non_locals, &)
|
|
314
|
+
sites = nil
|
|
315
|
+
evaluator = nil
|
|
316
|
+
Source::NodeWalker.each_with_ancestors(body) do |descendant, ancestors|
|
|
317
|
+
# A callee is resolved in the call-site scope, as the straight-line callee floor resolves it.
|
|
318
|
+
site = mutation_site(descendant) { evaluator ||= StatementEvaluator.new(scope: base_scope) }
|
|
319
|
+
next if site.nil?
|
|
320
|
+
|
|
321
|
+
site_reads(site).each do |read|
|
|
322
|
+
next unless captured_target?(read, ancestors, base_scope, non_locals, &)
|
|
323
|
+
|
|
324
|
+
((sites ||= {})[read.name] ||= []) << site
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
sites || NO_SITES
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# The mutation site `node` is — the node itself, or a {UnknownStoreWidening::CalleeStore} for a self-call
|
|
331
|
+
# whose callee content-mutates a local it is passed — or nil. A self-call is resolved by the
|
|
332
|
+
# `StatementEvaluator` the block yields, which the caller builds only when one is needed. A mutator name
|
|
333
|
+
# called on `self` (`self.store(h, k)`, `self.push(a, x)`) names no variable as its receiver, so it is asked
|
|
334
|
+
# as a self-call, as the straight-line callee floor asks it.
|
|
335
|
+
def mutation_site(node)
|
|
336
|
+
receiver = mutated_receiver(node)
|
|
337
|
+
return node if receiver && !receiver.is_a?(Prism::SelfNode)
|
|
338
|
+
return nil unless callee_call?(node)
|
|
339
|
+
|
|
340
|
+
arguments = yield.content_mutated_arguments(node)
|
|
341
|
+
arguments.empty? ? nil : UnknownStoreWidening::CalleeStore.new(node, arguments)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# The variable reads a mutation site reaches: a callee store's arguments, the local an element read is
|
|
345
|
+
# rooted at, or else {ReceiverAlias.mutated_reads} of the receiver.
|
|
346
|
+
def site_reads(site)
|
|
347
|
+
return site.arguments if site.is_a?(UnknownStoreWidening::CalleeStore)
|
|
348
|
+
|
|
349
|
+
receiver = mutated_receiver(site)
|
|
350
|
+
path = ElementReadWidening.element_read_path(receiver)
|
|
351
|
+
path ? [path.first] : ReceiverAlias.mutated_reads(receiver)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# A call to a method on `self` (implicit or explicit) that passes a local as an argument — the only kind
|
|
355
|
+
# the callee floor can report a site for, so the only kind worth resolving.
|
|
356
|
+
def callee_call?(node)
|
|
357
|
+
return false unless node.is_a?(Prism::CallNode)
|
|
358
|
+
return false unless node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
359
|
+
|
|
360
|
+
arguments = node.arguments&.arguments
|
|
361
|
+
!arguments.nil? && arguments.any?(Prism::LocalVariableReadNode)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# True when `read` names a variable {.content_mutations} collects: a {.content_target?} which, when it is a
|
|
365
|
+
# local, does not resolve inside a nested block and is not one the block introduces (the block yields that set,
|
|
366
|
+
# computed only when needed). An `it` read is never one: it is the parameter of the innermost block around
|
|
367
|
+
# it, so the body's own `it` is introduced and a nested block's `it` is that block's.
|
|
368
|
+
def captured_target?(read, ancestors, base_scope, non_locals)
|
|
369
|
+
return false if read.is_a?(Prism::ItLocalVariableReadNode)
|
|
370
|
+
return false unless content_target?(read, base_scope, non_locals)
|
|
371
|
+
return true unless read.is_a?(Prism::LocalVariableReadNode)
|
|
372
|
+
|
|
373
|
+
outer_local?(read, ancestors) && !yield.include?(read.name)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# A local bound at the call site (the block's own names are excluded by the caller), or — under
|
|
377
|
+
# `non_locals:` — an instance variable, class variable or global {.rebindable_non_local?} accepts.
|
|
378
|
+
def content_target?(read, base_scope, non_locals)
|
|
379
|
+
return base_scope.locals.key?(read.name) if read.is_a?(Prism::LocalVariableReadNode)
|
|
380
|
+
|
|
381
|
+
non_locals && rebindable_non_local?(base_scope, read.name)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# False when a local read or write resolves inside a scope nested between the body and `node`. Prism resolves a
|
|
385
|
+
# name in a nested block's own scope first, so `[[9]].each { |a| a << x }` mutates, and `[[9]].each { |a| a =
|
|
386
|
+
# x }` rebinds, that block's parameter, not the outer local that happens to share its name: its `depth` stops
|
|
387
|
+
# short of the body's own scope. A `def`, `class`, `module` or `class << self` body opens a scope of its own
|
|
388
|
+
# that sees no outer local at all, so `def helper; z = 5; end` writes the method's `z` — but only its body,
|
|
389
|
+
# and a `def`'s parameters, are that scope: a `def (o = x).m` receiver, a `class Foo < (s = x)` superclass or
|
|
390
|
+
# a `class << (t = x)` target runs in the enclosing one and is left to the depth test. {.writes} and
|
|
391
|
+
# {.content_mutations} both ask it, so the two sets cannot disagree about which `a` a site names. A name
|
|
392
|
+
# resolving in the body's own scope is left to the block-introduced exclusion: Prism puts an outer local there
|
|
393
|
+
# only when the parse never saw it declared, which a synthetic call-site scope can still bind.
|
|
394
|
+
def outer_local?(node, ancestors)
|
|
395
|
+
nesting = 0
|
|
396
|
+
ancestors.each_with_index do |ancestor, index|
|
|
397
|
+
return false if hard_scope_entered?(ancestor, ancestors[index + 1] || node)
|
|
398
|
+
|
|
399
|
+
nesting += 1 if NESTED_SCOPE_NODES.include?(ancestor.class)
|
|
400
|
+
end
|
|
401
|
+
node.depth >= nesting
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# True when `child`, the next node on the path, is inside the scope `ancestor` opens.
|
|
405
|
+
def hard_scope_entered?(ancestor, child)
|
|
406
|
+
case ancestor
|
|
407
|
+
when Prism::DefNode then child.equal?(ancestor.body) || child.equal?(ancestor.parameters)
|
|
408
|
+
when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode then child.equal?(ancestor.body)
|
|
409
|
+
else false
|
|
410
|
+
end
|
|
411
|
+
end
|
|
38
412
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
Source::NodeWalker.each(body) do |descendant|
|
|
42
|
-
next unless LOCAL_WRITE_NODES.any? { |klass| descendant.is_a?(klass) }
|
|
43
|
-
next if introduced.include?(descendant.name)
|
|
44
|
-
next unless base_scope.locals.key?(descendant.name)
|
|
413
|
+
NESTED_SCOPE_NODES = Set[Prism::BlockNode, Prism::LambdaNode].freeze
|
|
414
|
+
private_constant :NESTED_SCOPE_NODES
|
|
45
415
|
|
|
46
|
-
|
|
416
|
+
def mutated_receiver(node)
|
|
417
|
+
case node
|
|
418
|
+
when Prism::CallNode then node.receiver if MutationWidening::SHAPE_MUTATORS.include?(node.name)
|
|
419
|
+
when *INDEX_STORE_NODES then node.receiver
|
|
47
420
|
end
|
|
48
|
-
outer_writes.uniq
|
|
49
421
|
end
|
|
50
422
|
|
|
51
423
|
# Names the block itself introduces: parameters (numbered parameters included, via
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../type"
|
|
4
|
+
require_relative "../reflection"
|
|
5
|
+
require_relative "external_ancestor_resolution"
|
|
6
|
+
require_relative "project_method_ownership"
|
|
4
7
|
|
|
5
8
|
module Rigor
|
|
6
9
|
module Inference
|
|
@@ -30,14 +33,19 @@ module Rigor
|
|
|
30
33
|
# therefore covers ONLY the core-and-stdlib surface where immediate invocation is part of the documented
|
|
31
34
|
# contract:
|
|
32
35
|
#
|
|
33
|
-
# - `Array`, `Hash`, `Range`, `
|
|
34
|
-
# `reject`, `flat_map`, `find`/`detect`, `any?`, `all?`, `none?`, `one?`, `count`,
|
|
35
|
-
# `each_with_index`, `each_with_object`, `min_by`, `max_by`, `sort_by`, `partition`,
|
|
36
|
-
# `tally`, `sum`, `take_while`, `drop_while`, `chunk_while`, `slice_when`, `zip`, `collect`,
|
|
36
|
+
# - `Array`, `Hash`, `Range`, `Set`, `Enumerator`, `Enumerator::Lazy` iteration methods (`each`, `map`,
|
|
37
|
+
# `select`, `reject`, `flat_map`, `find`/`detect`, `any?`, `all?`, `none?`, `one?`, `count`,
|
|
38
|
+
# `inject`/`reduce`, `each_with_index`, `each_with_object`, `min_by`, `max_by`, `sort_by`, `partition`,
|
|
39
|
+
# `group_by`, `tally`, `sum`, `take_while`, `drop_while`, `chunk_while`, `slice_when`, `zip`, `collect`,
|
|
37
40
|
# `collect_concat`, `filter`, `filter_map`).
|
|
38
41
|
# - `Hash`-only iteration: `each_pair`, `each_key`, `each_value`, `transform_keys`, `transform_values`.
|
|
39
42
|
# - `Integer#times`, `Integer#upto`, `Integer#downto`, `Range#each`, `Range#step`.
|
|
40
|
-
# - `
|
|
43
|
+
# - `IO` / `File` / `StringIO` line iteration (`each`, `each_line`, `each_byte`, `each_char`,
|
|
44
|
+
# `each_codepoint`, the singleton `foreach`) and the same Enumerable methods minus
|
|
45
|
+
# {DEFERRED_ENUMERATOR_METHODS}.
|
|
46
|
+
# - `Object#tap`, `Object#then`, `Object#yield_self`. The stronger "yields exactly once, before returning"
|
|
47
|
+
# fact for these three lives in {BlockCallTiming}; that table is expected to move to the same
|
|
48
|
+
# `RBS::Extended` call-timing effect as this one.
|
|
41
49
|
# - Tuple/HashShape carriers map to Array/Hash for catalogue lookup so a literal `[1, 2, 3].each { ... }`
|
|
42
50
|
# is recognised.
|
|
43
51
|
#
|
|
@@ -46,6 +54,14 @@ module Rigor
|
|
|
46
54
|
# retained. False positives in this catalogue would silently weaken the soundness of fact retention in
|
|
47
55
|
# later sub-phases.
|
|
48
56
|
#
|
|
57
|
+
# Issue #1234 — a project class is outside the catalogue by name, yet `class Shelf; include Enumerable`
|
|
58
|
+
# answers `find` with `Enumerable#find` all the same. Given the `scope:` whose discovery tables know the
|
|
59
|
+
# project, a `Nominal` receiver of a project class classifies through its ancestry
|
|
60
|
+
# ({.ancestry_non_escaping?}): the method must be one the project does not define anywhere in that
|
|
61
|
+
# ancestry, and the first ancestor outside the project that declares it must be a catalogued class or
|
|
62
|
+
# `Enumerable` ({MIXIN_NON_ESCAPING}). An ancestor the RBS environment does not know could declare
|
|
63
|
+
# anything, so meeting one first declines.
|
|
64
|
+
#
|
|
49
65
|
# The analyzer is a pure query. It MUST NOT mutate the receiver type or scope, MUST NOT raise on
|
|
50
66
|
# unrecognised inputs, and MUST be deterministic for a given input.
|
|
51
67
|
module ClosureEscapeAnalyzer
|
|
@@ -53,23 +69,65 @@ module Rigor
|
|
|
53
69
|
|
|
54
70
|
# @param environment — reserved for the future sub-phase that consults
|
|
55
71
|
# `RBS::Extended` call-timing effects; sub-phase 3a ignores it.
|
|
72
|
+
# @param scope — the project's discovery tables, for the ancestry step; without it a project class
|
|
73
|
+
# stays `:unknown`.
|
|
56
74
|
# @return one of `:non_escaping`, `:escaping`, `:unknown`.
|
|
57
|
-
def classify(receiver_type:, method_name:, environment: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
75
|
+
def classify(receiver_type:, method_name:, environment: nil, scope: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
58
76
|
return :unknown if receiver_type.nil?
|
|
59
77
|
|
|
78
|
+
method_sym = method_name.to_sym
|
|
60
79
|
class_name = receiver_class_name(receiver_type)
|
|
61
|
-
|
|
80
|
+
if class_name
|
|
81
|
+
return :non_escaping if non_escaping?(class_name, method_sym)
|
|
82
|
+
return :escaping if escaping?(class_name, method_sym)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
instance_class = instance_carrier_class_name(receiver_type)
|
|
86
|
+
return :unknown if instance_class.nil?
|
|
87
|
+
|
|
88
|
+
ancestry_non_escaping?(instance_class, method_sym, scope) ? :non_escaping : :unknown
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Issue #1234 — whether some catalogue entry lists `method_name` as an iteration method: a name that runs
|
|
92
|
+
# its block once per element wherever the catalogue knows the receiver. `tap` / `then` / `yield_self` are
|
|
93
|
+
# left out, as they run the block exactly once. This is a fact about the NAME, for the one consumer that
|
|
94
|
+
# asks it of an `:unknown` receiver ({BlockRepetition.may_repeat?}); it proves nothing about escape.
|
|
95
|
+
def iterator_name?(method_name)
|
|
96
|
+
ITERATOR_NAMES.include?(method_name)
|
|
97
|
+
end
|
|
62
98
|
|
|
99
|
+
# Issue #1234 — whether the NAME of a catalogued iterator is the only thing Rigor knows about the method
|
|
100
|
+
# `receiver_type` answers `method_name` with, so a pass may read it as repetition
|
|
101
|
+
# ({BlockRepetition.may_repeat?}, for an `:unknown` receiver). It holds for a receiver Rigor cannot
|
|
102
|
+
# see at all (`Dynamic`, `Top`), and for a class it can see whose method the project does not define.
|
|
103
|
+
#
|
|
104
|
+
# A method the project defines under a catalogued name is the project's, not the iterator, so the name
|
|
105
|
+
# says nothing about how often it yields: `class Vault; def select(key) = yield(key.to_s); end` runs its
|
|
106
|
+
# block once. "Defines" is a `def`, `define_method` or `attr_*` anywhere in a project class's ancestry
|
|
107
|
+
# (instance side, or singleton side for a class-object receiver), or a signature whose declaring owner
|
|
108
|
+
# is a project class or module. A carrier this does not recognise, or one whose class it cannot name
|
|
109
|
+
# (an anonymous `Struct.new` value), answers false: when Rigor cannot tell whether the project owns the
|
|
110
|
+
# method, it does not assume repetition. {ProjectMethodOwnership.targets} carries the carrier audit.
|
|
111
|
+
def repeats_by_name?(receiver_type:, method_name:, scope:)
|
|
63
112
|
method_sym = method_name.to_sym
|
|
64
|
-
return
|
|
65
|
-
return :escaping if escaping?(class_name, method_sym)
|
|
113
|
+
return false unless ITERATOR_NAMES.include?(method_sym)
|
|
66
114
|
|
|
67
|
-
|
|
115
|
+
targets = ProjectMethodOwnership.targets(receiver_type)
|
|
116
|
+
return false if targets.nil?
|
|
117
|
+
|
|
118
|
+
targets.none? { |class_name, kind| ProjectMethodOwnership.defines?(class_name, method_sym, kind, scope) }
|
|
68
119
|
end
|
|
69
120
|
|
|
70
121
|
class << self
|
|
71
122
|
private
|
|
72
123
|
|
|
124
|
+
# The instance-side class a `Nominal` or an ADR-48 member carrier names, for the ancestry step.
|
|
125
|
+
def instance_carrier_class_name(receiver_type)
|
|
126
|
+
case receiver_type
|
|
127
|
+
when Type::Nominal, Type::StructInstance, Type::DataInstance then receiver_type.class_name&.to_s
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
73
131
|
# Resolve a single concrete class name for catalogue lookup. Returns `nil` when the receiver carrier
|
|
74
132
|
# does not name a single class (e.g. `Top`, `Dynamic[Top]`, `Union[...]`, `Bot`). `Tuple` projects to
|
|
75
133
|
# `Array`; `HashShape` to `Hash`; `Singleton[C]` to `C` (so `Integer.times` would resolve as a
|
|
@@ -115,6 +173,64 @@ module Rigor
|
|
|
115
173
|
methods = ESCAPING[class_name]
|
|
116
174
|
methods ? methods.include?(method_sym) : false
|
|
117
175
|
end
|
|
176
|
+
|
|
177
|
+
# Issue #1234 — a project class answers `method_sym` through the catalogued ancestor Ruby dispatches it
|
|
178
|
+
# to. The cheap gates come first: the name must be a catalogued iteration method, and the receiver a
|
|
179
|
+
# class the project declares. A definition anywhere in the project ancestry — the class's own `def
|
|
180
|
+
# find`, a project module's, a reopened `Enumerable`'s — is not the catalogued method and declines.
|
|
181
|
+
# A class the project's `sig/` declares asks its RBS definition where the method comes from; one
|
|
182
|
+
# without RBS walks the ancestors outside the project in method-resolution order.
|
|
183
|
+
def ancestry_non_escaping?(class_name, method_sym, scope)
|
|
184
|
+
return false if scope.nil? || !ITERATOR_NAMES.include?(method_sym)
|
|
185
|
+
return false unless scope.known_user_class?(class_name)
|
|
186
|
+
|
|
187
|
+
by_method = (ProjectMethodOwnership.memo(scope)[:ancestry][class_name] ||= {})
|
|
188
|
+
return by_method[method_sym] if by_method.key?(method_sym)
|
|
189
|
+
|
|
190
|
+
by_method[method_sym] = compute_ancestry_non_escaping?(class_name, method_sym, scope)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def compute_ancestry_non_escaping?(class_name, method_sym, scope)
|
|
194
|
+
return false if ProjectMethodOwnership.defines?(class_name, method_sym, :instance, scope)
|
|
195
|
+
|
|
196
|
+
if Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
|
|
197
|
+
return catalogued_declaration?(method_definition(class_name, method_sym, :instance, scope), method_sym)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
external_ancestry_non_escaping?(class_name, method_sym, scope)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# The RBS definition, or nil — a malformed signature is a gap, and
|
|
204
|
+
# {ExternalAncestorResolution.method_definition} is the one place that rescues it.
|
|
205
|
+
def method_definition(class_name, method_sym, kind, scope)
|
|
206
|
+
ExternalAncestorResolution.method_definition(class_name, method_sym, kind, scope: scope)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# The first external ancestor that declares the method decides. One the environment does not know may
|
|
210
|
+
# declare it, so it declines rather than being skipped; one that does not declare it is skipped.
|
|
211
|
+
def external_ancestry_non_escaping?(class_name, method_sym, scope)
|
|
212
|
+
scope.external_ancestor_name_candidates(class_name).each do |candidates|
|
|
213
|
+
known = candidates.find { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: scope) }
|
|
214
|
+
return false if known.nil?
|
|
215
|
+
return true if catalogued_owner?(known, method_sym)
|
|
216
|
+
|
|
217
|
+
definition = method_definition(known, method_sym, :instance, scope)
|
|
218
|
+
return catalogued_declaration?(definition, method_sym) if definition
|
|
219
|
+
end
|
|
220
|
+
false
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def catalogued_declaration?(definition, method_sym)
|
|
224
|
+
return false if definition.nil? || !definition.respond_to?(:defined_in)
|
|
225
|
+
|
|
226
|
+
owner = definition.defined_in
|
|
227
|
+
!owner.nil? && catalogued_owner?(owner.to_s.delete_prefix("::"), method_sym)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def catalogued_owner?(name, method_sym)
|
|
231
|
+
methods = NON_ESCAPING[name] || MIXIN_NON_ESCAPING[name]
|
|
232
|
+
methods ? methods.include?(method_sym) : false
|
|
233
|
+
end
|
|
118
234
|
end
|
|
119
235
|
|
|
120
236
|
OBJECT_NON_ESCAPING = %i[tap then yield_self].freeze
|
|
@@ -149,9 +265,24 @@ module Rigor
|
|
|
149
265
|
# `File.foreach(path) { case … when … then flag = true when … then return true if flag end }` classifies
|
|
150
266
|
# `:unknown` and misses the loop-body re-narrowing, so a local written in one `when` arm reads its
|
|
151
267
|
# pre-loop value in a sibling arm and a guarding condition folds to a spurious constant.
|
|
268
|
+
#
|
|
269
|
+
# The three are also `Enumerable[String]` over lines (rbs core declares it for `IO`;
|
|
270
|
+
# `data/core_overlay/string_io.rbs` for `StringIO`). An eager Enumerable method runs its block from inside
|
|
271
|
+
# the call, through `each`, so `io.each_with_index { … }` / `io.detect { … }` carry the same contract and
|
|
272
|
+
# take `ENUMERABLE_NON_ESCAPING` — minus {DEFERRED_ENUMERATOR_METHODS}, whose block outlives the call. On
|
|
273
|
+
# the `singleton(File)` / `singleton(IO)` side those names only ever meet `IO.select`, which takes no
|
|
274
|
+
# block and so never retains one. The key is the exact class name: a subclass (`Tempfile`, a user
|
|
275
|
+
# `StringIO` subclass) stays `:unknown`.
|
|
152
276
|
IO_ITERATION = %i[each_line each each_byte each_char each_codepoint].freeze
|
|
153
277
|
IO_SINGLETON_ITERATION = %i[foreach].freeze
|
|
154
278
|
|
|
279
|
+
# Enumerable methods that return an Enumerator holding the block and run it only when that Enumerator is
|
|
280
|
+
# consumed, so the block may run after locals it reads or writes have changed. They are NOT
|
|
281
|
+
# non-escaping. `ENUMERABLE_NON_ESCAPING` still lists them for the collection entries — a defect tracked
|
|
282
|
+
# as #1311; the stream entries below leave them out rather than inherit it.
|
|
283
|
+
DEFERRED_ENUMERATOR_METHODS = %i[chunk chunk_while slice_when slice_before slice_after].freeze
|
|
284
|
+
STREAM_ENUMERABLE_NON_ESCAPING = (ENUMERABLE_NON_ESCAPING - DEFERRED_ENUMERATOR_METHODS).freeze
|
|
285
|
+
|
|
155
286
|
NON_ESCAPING = {
|
|
156
287
|
"Array" => (ENUMERABLE_NON_ESCAPING + ARRAY_EXTRA).freeze,
|
|
157
288
|
"Hash" => (ENUMERABLE_NON_ESCAPING + HASH_EXTRA).freeze,
|
|
@@ -160,11 +291,24 @@ module Rigor
|
|
|
160
291
|
"Integer" => INTEGER_EXTRA,
|
|
161
292
|
"Enumerator" => ENUMERABLE_NON_ESCAPING,
|
|
162
293
|
"Enumerator::Lazy" => ENUMERABLE_NON_ESCAPING,
|
|
163
|
-
"IO" => (IO_ITERATION
|
|
164
|
-
"File" => (IO_ITERATION
|
|
165
|
-
"StringIO" => IO_ITERATION
|
|
294
|
+
"IO" => (STREAM_ENUMERABLE_NON_ESCAPING | IO_ITERATION | IO_SINGLETON_ITERATION).freeze,
|
|
295
|
+
"File" => (STREAM_ENUMERABLE_NON_ESCAPING | IO_ITERATION | IO_SINGLETON_ITERATION).freeze,
|
|
296
|
+
"StringIO" => (STREAM_ENUMERABLE_NON_ESCAPING | IO_ITERATION).freeze
|
|
297
|
+
}.freeze
|
|
298
|
+
|
|
299
|
+
# Issue #1234 — the catalogued modules a project class reaches only through its ancestry, read only by
|
|
300
|
+
# {.ancestry_non_escaping?} after the project has been ruled out as the method's owner. `Enumerable`'s
|
|
301
|
+
# methods run the block from inside the call, through the includer's `each` — minus
|
|
302
|
+
# {DEFERRED_ENUMERATOR_METHODS}, whose Enumerator outlives it, and minus `each` itself, which
|
|
303
|
+
# `Enumerable` does not declare: the includer supplies it, so nothing here speaks for it. For that
|
|
304
|
+
# reason a receiver typed as the bare module is not a key of {NON_ESCAPING} either.
|
|
305
|
+
MIXIN_NON_ESCAPING = {
|
|
306
|
+
"Enumerable" => (STREAM_ENUMERABLE_NON_ESCAPING - %i[each]).freeze
|
|
166
307
|
}.freeze
|
|
167
308
|
|
|
309
|
+
# Every name a {NON_ESCAPING} entry lists as iteration, for {.iterator_name?} and the ancestry gate.
|
|
310
|
+
ITERATOR_NAMES = (NON_ESCAPING.values.flatten.to_set - OBJECT_NON_ESCAPING).freeze
|
|
311
|
+
|
|
168
312
|
# Methods that are documented to **retain** the block past the call. The block is stored or scheduled,
|
|
169
313
|
# so outer narrowing facts on writeable captured locals cannot survive.
|
|
170
314
|
ESCAPING = {
|