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
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../source/node_walker"
|
|
6
|
+
require_relative "../type"
|
|
7
|
+
require_relative "block_parameter_binder"
|
|
8
|
+
require_relative "captured_locals"
|
|
9
|
+
require_relative "index_write_widening"
|
|
10
|
+
require_relative "mutation_widening"
|
|
11
|
+
require_relative "unknown_store_widening"
|
|
12
|
+
|
|
13
|
+
module Rigor
|
|
14
|
+
module Inference
|
|
15
|
+
# The index `||=` sites of a repeating block body whose slot an EARLIER run of the body may already have
|
|
16
|
+
# filled. `StatementEvaluator#index_compound_write_value` reads a `||=` whose slot types as a lone `Dynamic`
|
|
17
|
+
# as the memoization idiom and answers the rvalue, on the ADR-5 reading that nothing the analyzer saw set
|
|
18
|
+
# the slot. A block-return pass types every run of the body from one entry scope, so under it that reading
|
|
19
|
+
# is wrong for a slot an earlier run stored into: the per-element fold's second position of
|
|
20
|
+
# `cache = {}; [1, 2].find { |e| (cache[:first] ||= e) == 2 }` answered its own `2`, where Ruby keeps the
|
|
21
|
+
# first iteration's `1`, folded `find` to `2` and reported `found == 2` always-truthy. The pass marks the
|
|
22
|
+
# sites this module returns (`Scope#with_repeated_or_writes`), and the reading is withheld at a marked site.
|
|
23
|
+
#
|
|
24
|
+
# The mark is per site, not per receiver, because nothing about the receiver's type can say that its slot
|
|
25
|
+
# was filled: `{}` widens to `Hash[Dynamic[top], Dynamic[top]]`, a bare `Hash.new` is not widened at all, and
|
|
26
|
+
# a constant, class variable, attribute reader or nested memo (`(cache[:a] ||= {})[:b] ||= e`) is no binding
|
|
27
|
+
# the fold rebinds. A narrowing or a rebind of the receiver's variable leaves the mark where it is.
|
|
28
|
+
#
|
|
29
|
+
# A site whose receiver is fresh at every run — a hash or array literal, a `.new` call on a constant, or the
|
|
30
|
+
# own slots of a local only the body binds, always to such an object — is never marked. Otherwise:
|
|
31
|
+
#
|
|
32
|
+
# - The generic block-return pass types the rvalue from the signature's parameter type, which covers every
|
|
33
|
+
# run's store, so it marks a site only when the site is not ISOLATED ({.isolated?}): some other store in the
|
|
34
|
+
# body may fill its slot with something else. `words.map { |w| pool[w] ||= w }` keeps `Array[String]`.
|
|
35
|
+
# - The per-element fold types each position's rvalue on its own, so it marks per position ({Marks}). The
|
|
36
|
+
# first position has no earlier one and is never marked. A later one is marked unless the site is isolated,
|
|
37
|
+
# sits at the body's own level (not in a nested block or loop), and its key differs from every earlier
|
|
38
|
+
# position's ({FreshKeys.positions}): `pool = {}; %w[a b].map { |s| pool[s] ||= s.upcase }` keeps `["A", "B"]`,
|
|
39
|
+
# and `%w[a a b].find { |s| (pool[s] ||= s) == "b" }` marks only the second position.
|
|
40
|
+
module RepeatedOrWrites
|
|
41
|
+
NO_SITES = [].freeze
|
|
42
|
+
|
|
43
|
+
# The sites a pass marks at each run it types on its own. `shared` is marked at every run (the generic pass);
|
|
44
|
+
# `positional[i]` at the per-element fold's position `i` only. {#at} with a nil position — a caller that
|
|
45
|
+
# does not type one position — answers every site.
|
|
46
|
+
Marks = Data.define(:shared, :positional) do
|
|
47
|
+
def at(position)
|
|
48
|
+
return shared if positional.empty?
|
|
49
|
+
return positional.flatten.uniq if position.nil?
|
|
50
|
+
|
|
51
|
+
positional.fetch(position, NO_SITES)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def empty? = shared.empty? && positional.all?(&:empty?)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
NO_MARKS = Marks.new(shared: NO_SITES, positional: NO_SITES)
|
|
58
|
+
|
|
59
|
+
# A store in the body and the object it stores into, as `[root, depth]`: the variable the receiver
|
|
60
|
+
# evaluates from (`[:ivar, :@cache]`) and how many element reads lie between the two (`@cache[k][j] = v`
|
|
61
|
+
# stores into depth 1). `path` is nil for an object the store cannot name. `installs_shared` is whether it
|
|
62
|
+
# can put an object an earlier run may have filled into a slot — anything but a fresh object, and nothing
|
|
63
|
+
# for a store that only removes or reorders.
|
|
64
|
+
Store = Data.define(:node, :path, :installs_shared)
|
|
65
|
+
|
|
66
|
+
# What the body holds: the `||=` sites to consider, the ones at the body's own level, how many stores that
|
|
67
|
+
# may reach an object an earlier run saw there are (those sites included) and how many reach each path,
|
|
68
|
+
# whether some store's object is unknown, the paths a store may put a shared object into, and every local
|
|
69
|
+
# name the body writes.
|
|
70
|
+
Scan = Data.define(:or_writes, :top_level, :store_count, :path_counts, :unattributable, :installs,
|
|
71
|
+
:written_locals)
|
|
72
|
+
|
|
73
|
+
# The walk's accumulators. `writes` maps each variable the body writes, as `[kind, name]`, to whether every
|
|
74
|
+
# write stores a fresh object into it.
|
|
75
|
+
Walk = Struct.new(:or_writes, :top_level, :stores, :written_locals, :writes)
|
|
76
|
+
|
|
77
|
+
# The node forms and source patterns the scan recognises.
|
|
78
|
+
module Forms
|
|
79
|
+
STORE_NODES = IndexWriteWidening::CONTENT_WRITE_NODE_CLASSES
|
|
80
|
+
INDEX_WRITE_NODES = IndexWriteWidening::NODE_CLASSES
|
|
81
|
+
|
|
82
|
+
# A call that can store through any method name, so into any slot of its receiver.
|
|
83
|
+
DYNAMIC_SEND = Set[:send, :public_send, :__send__].freeze
|
|
84
|
+
|
|
85
|
+
# A call that hands out a method as an object, which a store can then be called through.
|
|
86
|
+
METHOD_HANDLES = Set[:method, :public_method].freeze
|
|
87
|
+
|
|
88
|
+
# A heredoc opener: its text lies outside its node's location, so a `||=` in it escapes the source slice.
|
|
89
|
+
HEREDOC_OPENER = /<<[~-]?["'`]?[A-Za-z_]/
|
|
90
|
+
|
|
91
|
+
# The nodes whose children run more than once for each run of the body that contains them.
|
|
92
|
+
REPEATING_NODES = Set[
|
|
93
|
+
Prism::BlockNode, Prism::LambdaNode, Prism::WhileNode, Prism::UntilNode, Prism::ForNode
|
|
94
|
+
].freeze
|
|
95
|
+
|
|
96
|
+
# Every variable read and write form, with the kind of variable it names.
|
|
97
|
+
VARIABLE_KINDS = {
|
|
98
|
+
local: [Prism::LocalVariableReadNode, *CapturedLocals::LOCAL_WRITE_NODES],
|
|
99
|
+
ivar: [
|
|
100
|
+
Prism::InstanceVariableReadNode, Prism::InstanceVariableWriteNode, Prism::InstanceVariableOperatorWriteNode,
|
|
101
|
+
Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableTargetNode
|
|
102
|
+
],
|
|
103
|
+
cvar: [
|
|
104
|
+
Prism::ClassVariableReadNode, Prism::ClassVariableWriteNode, Prism::ClassVariableOperatorWriteNode,
|
|
105
|
+
Prism::ClassVariableOrWriteNode, Prism::ClassVariableAndWriteNode, Prism::ClassVariableTargetNode
|
|
106
|
+
],
|
|
107
|
+
gvar: [
|
|
108
|
+
Prism::GlobalVariableReadNode, Prism::GlobalVariableWriteNode, Prism::GlobalVariableOperatorWriteNode,
|
|
109
|
+
Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableAndWriteNode, Prism::GlobalVariableTargetNode
|
|
110
|
+
],
|
|
111
|
+
const: [
|
|
112
|
+
Prism::ConstantReadNode, Prism::ConstantWriteNode, Prism::ConstantOperatorWriteNode,
|
|
113
|
+
Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode, Prism::ConstantTargetNode
|
|
114
|
+
]
|
|
115
|
+
}.flat_map { |kind, classes| classes.map { |node_class| [node_class, kind] } }.to_h.freeze
|
|
116
|
+
|
|
117
|
+
READ_NODES = Set[
|
|
118
|
+
Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode, Prism::ClassVariableReadNode,
|
|
119
|
+
Prism::GlobalVariableReadNode, Prism::ConstantReadNode
|
|
120
|
+
].freeze
|
|
121
|
+
|
|
122
|
+
# The writes that bind their value as it is: a plain write, and an `||=` (which keeps an object already set).
|
|
123
|
+
VALUE_WRITE_NODES = Set[
|
|
124
|
+
Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode, Prism::ClassVariableWriteNode,
|
|
125
|
+
Prism::GlobalVariableWriteNode, Prism::ConstantWriteNode,
|
|
126
|
+
Prism::LocalVariableOrWriteNode, Prism::InstanceVariableOrWriteNode, Prism::ClassVariableOrWriteNode,
|
|
127
|
+
Prism::GlobalVariableOrWriteNode, Prism::ConstantOrWriteNode
|
|
128
|
+
].freeze
|
|
129
|
+
end
|
|
130
|
+
private_constant :Forms
|
|
131
|
+
|
|
132
|
+
module_function
|
|
133
|
+
|
|
134
|
+
# @param block — the repeating block.
|
|
135
|
+
# @param stores — {CapturedLocals.content_mutations} of the block (with `non_locals: true`): the captured
|
|
136
|
+
# variables its body mutates in place, each with its sites; its callee stores count as stores here.
|
|
137
|
+
# @param scope — the call-site scope, in which a per-element fold's positions bind their parameters.
|
|
138
|
+
# @param element_types — the per-element fold's position types, one per position; nil for the generic
|
|
139
|
+
# block-return pass.
|
|
140
|
+
# @return the {Marks} to lay, {NO_MARKS} for the overwhelmingly common body with no such site.
|
|
141
|
+
def sites(block, stores, scope, element_types: nil)
|
|
142
|
+
body = block.body
|
|
143
|
+
return NO_MARKS unless body && may_hold_or_write?(body)
|
|
144
|
+
|
|
145
|
+
found = scan(block, stores, scope)
|
|
146
|
+
return NO_MARKS if found.or_writes.empty?
|
|
147
|
+
return generic_marks(found) if element_types.nil?
|
|
148
|
+
|
|
149
|
+
positional_marks(block, found, stores, scope, element_types)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# One source slice is far cheaper than the walk, and a body that spells no `||=` holds no such site. A
|
|
153
|
+
# heredoc's text lies outside its node's location, so a body that opens one is walked regardless.
|
|
154
|
+
def may_hold_or_write?(body)
|
|
155
|
+
source = body.slice
|
|
156
|
+
source.include?("||=") || source.match?(Forms::HEREDOC_OPENER)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def generic_marks(found)
|
|
160
|
+
Marks.new(shared: found.or_writes.reject { |node| isolated?(node, found) }, positional: NO_SITES)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def positional_marks(block, found, stores, scope, element_types)
|
|
164
|
+
positional = Array.new(element_types.size) { [] }
|
|
165
|
+
parameters = nil
|
|
166
|
+
found.or_writes.each do |node|
|
|
167
|
+
fresh = exempt_candidate?(node, found) &&
|
|
168
|
+
FreshKeys.positions(block, node, found.written_locals, stores, scope, element_types,
|
|
169
|
+
parameters ||= CapturedLocals.introduced_locals(block))
|
|
170
|
+
(1...element_types.size).each { |index| positional[index] << node unless fresh && fresh[index] }
|
|
171
|
+
end
|
|
172
|
+
Marks.new(shared: NO_SITES, positional: positional)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def exempt_candidate?(node, found) = found.top_level.key?(node) && isolated?(node, found)
|
|
176
|
+
|
|
177
|
+
# Walks the body, then settles each store's object ({.settle}), and drops a site on a body-local fresh at
|
|
178
|
+
# every run ({.fresh_roots}), whose own slots no earlier run saw.
|
|
179
|
+
def scan(block, stores, scope)
|
|
180
|
+
walked = Walk.new([], {}.compare_by_identity, [], Set.new, {})
|
|
181
|
+
walk(block.body, true, walked)
|
|
182
|
+
fresh = fresh_roots(block, walked.writes, scope)
|
|
183
|
+
settled = (walked.stores + callee_stores(stores)).filter_map do |store|
|
|
184
|
+
settle(store, walked.writes, fresh, scope)
|
|
185
|
+
end
|
|
186
|
+
or_writes = walked.or_writes.reject { |node| on_fresh_slot?(receiver_path(node.receiver, 0), fresh) }
|
|
187
|
+
summary(walked, or_writes, settled)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def summary(walked, or_writes, settled)
|
|
191
|
+
Scan.new(or_writes: or_writes, top_level: walked.top_level, store_count: settled.size,
|
|
192
|
+
path_counts: settled.map(&:path).tally, unattributable: settled.any? { |store| store.path.nil? },
|
|
193
|
+
installs: settled.select(&:installs_shared).map(&:path), written_locals: walked.written_locals)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def on_fresh_slot?(path, fresh) = !path.nil? && path.last.zero? && fresh.include?(path.first)
|
|
197
|
+
|
|
198
|
+
# The locals the body alone binds — not captured, not a block parameter or block-local — whose every write
|
|
199
|
+
# stores a fresh object.
|
|
200
|
+
def fresh_roots(block, writes, scope)
|
|
201
|
+
introduced = nil
|
|
202
|
+
writes.each_with_object(Set.new) do |((kind, name), all_fresh), roots|
|
|
203
|
+
next unless all_fresh && kind == :local && !scope.locals.key?(name)
|
|
204
|
+
|
|
205
|
+
introduced ||= CapturedLocals.introduced_locals(block)
|
|
206
|
+
roots << [kind, name] unless introduced.include?(name)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# The store as the isolation test reads it; nil for one that reaches nothing an earlier run saw. A store is
|
|
211
|
+
# made unattributable when its variable may name some other object: one the body binds from anything but a
|
|
212
|
+
# fresh object (`other = pool`), or a local that is neither captured nor written by the body — a block
|
|
213
|
+
# parameter, `it`, `_1` — which holds whatever it is handed. A body-local fresh at every run holds a new
|
|
214
|
+
# object, so a store into its own slots is dropped; but that object may hold an old one (`box = [pool]`), so
|
|
215
|
+
# a store deeper in is unattributable.
|
|
216
|
+
def settle(store, writes, fresh, scope)
|
|
217
|
+
root = store.path&.first
|
|
218
|
+
return store if root.nil?
|
|
219
|
+
|
|
220
|
+
if writes.key?(root)
|
|
221
|
+
return nil if on_fresh_slot?(store.path, fresh)
|
|
222
|
+
return store if writes[root] && !fresh.include?(root)
|
|
223
|
+
elsif root.first != :local || scope.locals.key?(root.last)
|
|
224
|
+
return store
|
|
225
|
+
end
|
|
226
|
+
Store.new(node: store.node, path: nil, installs_shared: true)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def walk(node, top_level, found)
|
|
230
|
+
return unless node.is_a?(Prism::Node)
|
|
231
|
+
return if node.is_a?(Prism::DefNode)
|
|
232
|
+
|
|
233
|
+
record_write(node, found)
|
|
234
|
+
record_store(node, top_level, found)
|
|
235
|
+
return if node.is_a?(Prism::DefinedNode)
|
|
236
|
+
|
|
237
|
+
nested_level = top_level && !Forms::REPEATING_NODES.include?(node.class)
|
|
238
|
+
node.rigor_each_child { |child| walk(child, nested_level, found) }
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def record_write(node, found)
|
|
242
|
+
kind = Forms::VARIABLE_KINDS[node.class]
|
|
243
|
+
return if kind.nil? || Forms::READ_NODES.include?(node.class)
|
|
244
|
+
|
|
245
|
+
found.written_locals << node.name if kind == :local
|
|
246
|
+
root = [kind, node.name]
|
|
247
|
+
fresh = Forms::VALUE_WRITE_NODES.include?(node.class) && fresh_object?(node.value)
|
|
248
|
+
found.writes[root] = found.writes.fetch(root, true) && fresh
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def record_store(node, top_level, found)
|
|
252
|
+
if dynamic_store?(node)
|
|
253
|
+
found.stores << Store.new(node: node, path: nil, installs_shared: true)
|
|
254
|
+
return
|
|
255
|
+
end
|
|
256
|
+
return unless store_node?(node)
|
|
257
|
+
return if fresh_object?(node.receiver)
|
|
258
|
+
|
|
259
|
+
found.stores << Store.new(node: node, path: store_path(node), installs_shared: installs_shared?(node))
|
|
260
|
+
return unless node.is_a?(Prism::IndexOrWriteNode)
|
|
261
|
+
|
|
262
|
+
found.or_writes << node
|
|
263
|
+
found.top_level[node] = true if top_level
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def store_node?(node)
|
|
267
|
+
return true if Forms::STORE_NODES.include?(node.class)
|
|
268
|
+
|
|
269
|
+
node.is_a?(Prism::CallNode) && store_method?(node.name)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def store_method?(name) = name == :[]= || MutationWidening::SHAPE_MUTATORS.include?(name)
|
|
273
|
+
|
|
274
|
+
# A call that can store through a method it does not name — `send` and its kin — or that hands out a store
|
|
275
|
+
# method to be called later (`pool.method(:[]=)`).
|
|
276
|
+
def dynamic_store?(node)
|
|
277
|
+
return false unless node.is_a?(Prism::CallNode)
|
|
278
|
+
return true if Forms::DYNAMIC_SEND.include?(node.name)
|
|
279
|
+
return false unless Forms::METHOD_HANDLES.include?(node.name)
|
|
280
|
+
|
|
281
|
+
name = node.arguments&.arguments&.first
|
|
282
|
+
name.is_a?(Prism::SymbolNode) && store_method?(name.unescaped.to_sym)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Whether the store `node` can put an object an earlier run may have filled into a slot: an index write of
|
|
286
|
+
# anything but a fresh object, a mutator handed one, or any other store form; not a mutator that only
|
|
287
|
+
# removes or reorders.
|
|
288
|
+
def installs_shared?(node)
|
|
289
|
+
case node
|
|
290
|
+
when Prism::IndexOrWriteNode, Prism::IndexAndWriteNode then !fresh_object?(node.value)
|
|
291
|
+
when Prism::CallNode
|
|
292
|
+
return false if UnknownStoreWidening::VALUE_PRESERVING.include?(node.name)
|
|
293
|
+
|
|
294
|
+
arguments = node.arguments&.arguments || NO_SITES
|
|
295
|
+
stored = node.name == :[]= ? arguments.last(1) : arguments
|
|
296
|
+
stored.any? { |argument| !fresh_object?(argument) }
|
|
297
|
+
else true
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# A call with no receiver stores into `self`.
|
|
302
|
+
def store_path(node)
|
|
303
|
+
return [[:self], 0] if node.receiver.nil?
|
|
304
|
+
|
|
305
|
+
receiver_path(node.receiver, 0)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# `[root, depth]` for the object `node` evaluates to, or nil when no variable roots it.
|
|
309
|
+
def receiver_path(node, depth)
|
|
310
|
+
case node
|
|
311
|
+
when Prism::ParenthesesNode then parenthesized_path(node, depth)
|
|
312
|
+
when Prism::SelfNode then [[:self], depth]
|
|
313
|
+
when Prism::ItLocalVariableReadNode then [%i[local it], depth]
|
|
314
|
+
when Prism::CallNode then element_read_path(node, depth)
|
|
315
|
+
when *Forms::INDEX_WRITE_NODES then receiver_path(node.receiver, depth + 1)
|
|
316
|
+
else variable_path(node, depth)
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def parenthesized_path(node, depth)
|
|
321
|
+
statements = node.body
|
|
322
|
+
return nil unless statements.is_a?(Prism::StatementsNode) && statements.body.size == 1
|
|
323
|
+
|
|
324
|
+
receiver_path(statements.body.first, depth)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def element_read_path(node, depth)
|
|
328
|
+
return nil unless node.name == :[] && node.receiver && !node.safe_navigation?
|
|
329
|
+
|
|
330
|
+
receiver_path(node.receiver, depth + 1)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def variable_path(node, depth)
|
|
334
|
+
kind = Forms::VARIABLE_KINDS[node.class]
|
|
335
|
+
kind && [[kind, node.name], depth]
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# {CapturedLocals.content_mutations}'s callee stores — a self-call passing a captured local to a parameter
|
|
339
|
+
# its callee mutates — as stores into that local.
|
|
340
|
+
def callee_stores(stores)
|
|
341
|
+
stores.flat_map do |name, sites|
|
|
342
|
+
sites.grep(UnknownStoreWidening::CalleeStore).map do |site|
|
|
343
|
+
Store.new(node: site.call, path: [[:local, name.to_sym], 0], installs_shared: true)
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# An expression that evaluates to a new object at every run: a hash or array literal, or `.new` called on a
|
|
349
|
+
# constant.
|
|
350
|
+
def fresh_object?(node)
|
|
351
|
+
case node
|
|
352
|
+
when Prism::HashNode, Prism::ArrayNode then true
|
|
353
|
+
when Prism::CallNode
|
|
354
|
+
node.name == :new &&
|
|
355
|
+
(node.receiver.is_a?(Prism::ConstantReadNode) || node.receiver.is_a?(Prism::ConstantPathNode))
|
|
356
|
+
else false
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# True when no other store in the body can fill `node`'s slot: no other store reaches the object `node` stores
|
|
361
|
+
# into (the same variable, at the same element depth), none can put a shared object into a container above it
|
|
362
|
+
# on the way (`pool[:a] = shared` above `pool[:a][s] ||= s`), and every store names the object it reaches
|
|
363
|
+
# ({.settle}), the site's own included. A site whose receiver no variable roots is isolated only as the
|
|
364
|
+
# body's sole store.
|
|
365
|
+
def isolated?(node, found)
|
|
366
|
+
path = receiver_path(node.receiver, 0)
|
|
367
|
+
return found.store_count == 1 if path.nil?
|
|
368
|
+
return false if found.unattributable || found.path_counts[path] != 1
|
|
369
|
+
|
|
370
|
+
root, depth = path
|
|
371
|
+
found.installs.none? { |(other_root, other_depth)| other_root == root && other_depth < depth }
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Whether an index `||=` site's key differs, position by position, from every earlier position's key under
|
|
375
|
+
# the per-element fold.
|
|
376
|
+
module FreshKeys
|
|
377
|
+
# The nodes a key expression may not contain; see {.fixed_key?}.
|
|
378
|
+
KEY_UNFIXED_NODES = Set[
|
|
379
|
+
Prism::InstanceVariableReadNode, Prism::ClassVariableReadNode, Prism::GlobalVariableReadNode,
|
|
380
|
+
Prism::BlockNode, Prism::LambdaNode, *CapturedLocals::LOCAL_WRITE_NODES, *CapturedLocals::NON_LOCAL_WRITE_NODES
|
|
381
|
+
].freeze
|
|
382
|
+
|
|
383
|
+
HASH_KEY_CLASSES = [Symbol, String, Integer].freeze
|
|
384
|
+
private_constant :KEY_UNFIXED_NODES, :HASH_KEY_CLASSES
|
|
385
|
+
|
|
386
|
+
module_function
|
|
387
|
+
|
|
388
|
+
# For each position, whether `node`'s key there is provably a value no earlier position's key equals; nil
|
|
389
|
+
# when the key cannot be typed position by position. The key must be a single index argument that reads no
|
|
390
|
+
# variable but ones the body never changes ({.fixed_key?}), so typing it under each position's parameter
|
|
391
|
+
# binding is typing what the position evaluates. The keys up to a position must be `Constant`s its receiver
|
|
392
|
+
# tells apart ({.distinguishable?}), and its own must equal none before it. A key that fails to type is not
|
|
393
|
+
# shown fresh anywhere, so the site stays marked, the wider answer.
|
|
394
|
+
def positions(block, node, written_locals, stores, scope, element_types, parameters)
|
|
395
|
+
key = sole_key(node)
|
|
396
|
+
return nil if key.nil? || !fixed_key?(key, parameters, written_locals, stores)
|
|
397
|
+
|
|
398
|
+
typed = element_types.map do |element_type|
|
|
399
|
+
position = BlockParameterBinder.new(expected_param_types: [element_type]).bind_onto(block, scope)
|
|
400
|
+
[position.type_of(key), receiver_kind(position.type_of(node.receiver))]
|
|
401
|
+
end
|
|
402
|
+
fresh_by_position(typed)
|
|
403
|
+
rescue StandardError
|
|
404
|
+
nil
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def fresh_by_position(typed)
|
|
408
|
+
values = []
|
|
409
|
+
typed.each_with_index.map do |(key_type, receiver_kind), index|
|
|
410
|
+
return typed.map { false } unless key_type.is_a?(Type::Constant)
|
|
411
|
+
|
|
412
|
+
value = key_type.value
|
|
413
|
+
unseen = values.none? { |earlier| earlier.eql?(value) }
|
|
414
|
+
values << value
|
|
415
|
+
index.zero? || (unseen && distinguishable?(values, receiver_kind))
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# A core `Hash` tells apart any two `Symbol`, `String` or `Integer` keys `eql?` does, and a core `Array` any two
|
|
420
|
+
# non-negative indices. Any other receiver's `[]` may normalise a key (`with_indifferent_access`, a
|
|
421
|
+
# case-insensitive hash), so only non-negative `Integer`s, or keys all of one class, `Symbol` or `String`, pass.
|
|
422
|
+
def distinguishable?(values, receiver_kind)
|
|
423
|
+
case receiver_kind
|
|
424
|
+
when :hash then values.all? { |value| HASH_KEY_CLASSES.any? { |key_class| value.is_a?(key_class) } }
|
|
425
|
+
when :array then values.all? { |value| value.is_a?(Integer) && !value.negative? }
|
|
426
|
+
else
|
|
427
|
+
values.all? { |value| value.is_a?(Integer) && !value.negative? } ||
|
|
428
|
+
values.all?(Symbol) || values.all?(String)
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def receiver_kind(type)
|
|
433
|
+
case type
|
|
434
|
+
when Type::HashShape then :hash
|
|
435
|
+
when Type::Tuple then :array
|
|
436
|
+
when Type::Nominal then { "Hash" => :hash, "Array" => :array }[type.class_name]
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def sole_key(node)
|
|
441
|
+
arguments = node.arguments&.arguments
|
|
442
|
+
return nil unless arguments&.size == 1 && node.block.nil?
|
|
443
|
+
|
|
444
|
+
key = arguments.first
|
|
445
|
+
key.is_a?(Prism::SplatNode) || key.is_a?(Prism::KeywordHashNode) ? nil : key
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# True when `key` reads no variable the body can change between positions: every local it reads is one of
|
|
449
|
+
# the block's `parameters` or a captured local, which the body neither writes nor mutates in place, and it
|
|
450
|
+
# contains no instance-, class- or global-variable read, no write, and no block or lambda.
|
|
451
|
+
def fixed_key?(key, parameters, written_locals, stores)
|
|
452
|
+
Source::NodeWalker.each(key) do |node|
|
|
453
|
+
return false if KEY_UNFIXED_NODES.include?(node.class)
|
|
454
|
+
next unless node.is_a?(Prism::LocalVariableReadNode)
|
|
455
|
+
return false if written_locals.include?(node.name)
|
|
456
|
+
return false unless node.depth.zero? ? parameters.include?(node.name) : !stores.key?(node.name)
|
|
457
|
+
end
|
|
458
|
+
true
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
# The constructs whose body `return` leaves only that body, not the enclosing method. A `return` anywhere else —
|
|
8
|
+
# an ordinary block included — exits the method (control-flow-analysis.md § "Non-local exits").
|
|
9
|
+
# `StatementEvaluator`'s return sink and `DefReturnTyper`'s return collection both stop here; each used to carry
|
|
10
|
+
# its own list, and sig-gen's treated every block as a barrier (issue #1382).
|
|
11
|
+
module ReturnBarrier
|
|
12
|
+
# A nested `def` and a `->` literal: the body is its own method or lambda.
|
|
13
|
+
NODES = Set[Prism::DefNode, Prism::LambdaNode].freeze
|
|
14
|
+
|
|
15
|
+
# The block calls whose body `return` leaves only the block: the method a `define_method` /
|
|
16
|
+
# `define_singleton_method` block defines, on whatever receiver (`klass.define_method(:m) { … }`), and
|
|
17
|
+
# `Kernel#lambda` — called bare, on `self`, or on `Kernel`. Each also counts through `send`
|
|
18
|
+
# (`klass.send(:define_method, :m) { … }`). A `lambda` on any other receiver is some other method.
|
|
19
|
+
DEFINE_CALLS = %i[define_method define_singleton_method].to_set.freeze
|
|
20
|
+
SEND_CALLS = %i[send public_send __send__].to_set.freeze
|
|
21
|
+
private_constant :DEFINE_CALLS, :SEND_CALLS
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# True when `node` is a nested `def` or a lambda literal.
|
|
26
|
+
def node?(node)
|
|
27
|
+
NODES.include?(node.class)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# True when the block `call_node` carries is a return barrier.
|
|
31
|
+
def block_call?(call_node)
|
|
32
|
+
name = call_node.name
|
|
33
|
+
if SEND_CALLS.include?(name)
|
|
34
|
+
sent = call_node.arguments&.arguments&.first
|
|
35
|
+
return false unless sent.is_a?(Prism::SymbolNode)
|
|
36
|
+
|
|
37
|
+
name = sent.unescaped.to_sym
|
|
38
|
+
end
|
|
39
|
+
DEFINE_CALLS.include?(name) || (name == :lambda && kernel_receiver?(call_node.receiver))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# True when a `lambda` call on `receiver` reaches `Kernel#lambda`: no receiver, `self`, `Kernel` or `::Kernel`.
|
|
43
|
+
def kernel_receiver?(receiver)
|
|
44
|
+
case receiver
|
|
45
|
+
when nil, Prism::SelfNode then true
|
|
46
|
+
when Prism::ConstantReadNode then receiver.name == :Kernel
|
|
47
|
+
when Prism::ConstantPathNode then receiver.parent.nil? && receiver.name == :Kernel
|
|
48
|
+
else false
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
private_class_method :kernel_receiver?
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../type"
|
|
4
|
+
require_relative "refinement_mutation"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
# The in-place mutators whose ARGUMENTS do not describe what they store — the other half of what
|
|
9
|
+
# {MutationWidening} joins. A block computes the value (`map!`, `transform_values!`, a block-form `fill`), the
|
|
10
|
+
# argument is a collection whose contents {ContentJoin} does not read for that name (`merge!`, `Hash#replace`),
|
|
11
|
+
# or the stored values are the receiver's own elements taken apart (`flatten!`).
|
|
12
|
+
#
|
|
13
|
+
# The widening kept the SEED's types for such a site, because nothing it joins describes the new values:
|
|
14
|
+
# `a = [1]; a.map!(&:to_s)` read `Array[Integer]`, and `a.first.upcase` drew a false
|
|
15
|
+
# `undefined method 'upcase' for Integer`. The seam cannot see the values, so the answer at each position the
|
|
16
|
+
# mutator writes is gradual — {REPLACED} or {JOINED}, depending on whether the old values can survive it.
|
|
17
|
+
module RewriteMutation
|
|
18
|
+
# Mutators that rewrite EVERY value at the listed type-argument positions, keyed by carrier class: nothing the
|
|
19
|
+
# seed held there is known to survive, so the position becomes `Dynamic[top]` outright. Keeping the seed beside
|
|
20
|
+
# the gradual arm would leave a closed claim about values that are gone — `a = [1, 2]; a.map!(&:to_s)` read
|
|
21
|
+
# `Array[1 | 2 | Dynamic[top]]`, which a hand-written `-> Array[String]` rejects on correct code, and the kept
|
|
22
|
+
# `1` gated a later `a << "x"` out of the re-join as foreign evidence. `Array#replace` is listed although its
|
|
23
|
+
# argument IS its new content: {MutationWidening#join_added_elements} still joins that evidence after the
|
|
24
|
+
# replacement, and the seed it would otherwise keep beside it is gone.
|
|
25
|
+
REPLACED = Ractor.make_shareable({
|
|
26
|
+
"Array" => { map!: [0], collect!: [0], flatten!: [0], replace: [0] },
|
|
27
|
+
"Hash" => { transform_keys!: [0], transform_values!: [1], replace: [0, 1] }
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
# Mutators that store values nothing describes BESIDE values they may keep: `fill(x, start)` leaves the slots
|
|
31
|
+
# before `start`, and `merge!` / `update` leave every key the argument lacks. The listed positions join
|
|
32
|
+
# `Dynamic[top]` and keep the seed's own arms.
|
|
33
|
+
#
|
|
34
|
+
# `fill` is listed although its no-block form's argument IS its value: the widening cannot see the block, and
|
|
35
|
+
# the arm costs the argument form nothing, since {MutationWidening#join_added_elements} already floors every
|
|
36
|
+
# straight-line store it joins. `merge!` on a `Hash.new(0)` counter opens the value side the default proved
|
|
37
|
+
# (`Dynamic[top] | Integer`), since the seam cannot tell a merging block or a foreign argument from neither.
|
|
38
|
+
JOINED = Ractor.make_shareable({
|
|
39
|
+
"Array" => { fill: [0] },
|
|
40
|
+
"Hash" => { merge!: [0, 1], update: [0, 1] }
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
module_function
|
|
44
|
+
|
|
45
|
+
# True when `method_name` is a rewriter for some carrier class.
|
|
46
|
+
def rewriter?(method_name)
|
|
47
|
+
[REPLACED, JOINED].any? { |table| table.each_value.any? { |names| names.key?(method_name) } }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# `carrier` with each position `method_name` rewrites made gradual — replaced for a {REPLACED} name, joined
|
|
51
|
+
# for a {JOINED} one; any carrier other than an `Array` / `Hash` nominal, or a name neither table lists for its
|
|
52
|
+
# class, is returned untouched. Idempotent, so a path that reaches it twice answers the same.
|
|
53
|
+
def arm(carrier, method_name)
|
|
54
|
+
return carrier unless carrier.is_a?(Type::Nominal) && !carrier.type_args.empty?
|
|
55
|
+
|
|
56
|
+
replaced = REPLACED.dig(carrier.class_name, method_name) || []
|
|
57
|
+
joined = JOINED.dig(carrier.class_name, method_name) || []
|
|
58
|
+
return carrier if replaced.empty? && joined.empty?
|
|
59
|
+
|
|
60
|
+
args = carrier.type_args.each_with_index.map do |arg, i|
|
|
61
|
+
if replaced.include?(i) then Type::Combinator.untyped
|
|
62
|
+
elsif joined.include?(i) then Type::Combinator.union(arg, Type::Combinator.untyped)
|
|
63
|
+
else arg
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
Type::Combinator.nominal_of(carrier.class_name, type_args: args)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# {.arm} through a `Union` (member by member) and an empty-witness refinement (its base, keeping the witness
|
|
70
|
+
# only when the mutator cannot empty the receiver — {RefinementMutation.preserves_witness?}). For a carrier
|
|
71
|
+
# the ADR-56 slice-C join rebuilt from its seed, which knows the adders' content but not the rewrite.
|
|
72
|
+
def arm_through(type, method_name)
|
|
73
|
+
case type
|
|
74
|
+
when Type::Union then Type::Combinator.union(*type.members.map { |member| arm_through(member, method_name) })
|
|
75
|
+
when Type::Difference then arm_refinement(type, method_name)
|
|
76
|
+
else arm(type, method_name)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def arm_refinement(difference, method_name)
|
|
81
|
+
base = difference.base
|
|
82
|
+
return difference unless difference.removes_empty_witness? && base.is_a?(Type::Nominal)
|
|
83
|
+
|
|
84
|
+
armed = arm(base, method_name)
|
|
85
|
+
return difference if armed.equal?(base)
|
|
86
|
+
|
|
87
|
+
kept = RefinementMutation.preserves_witness?(base.class_name, method_name)
|
|
88
|
+
kept ? Type::Combinator.difference(armed, difference.removed) : armed
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# `values`, or `:keep` when `method_name` rewrites the carrier's value position (an `Array`'s element, a
|
|
92
|
+
# `Hash`'s value) — the pre-state widening's pinning mode.
|
|
93
|
+
#
|
|
94
|
+
# Erasing the pinning there buys nothing and costs the #561 guarantee. The erasure exists to stop a stale
|
|
95
|
+
# constant fold (issue #560), and a union carrying `Dynamic` never folds; but the erased form is a CLOSED
|
|
96
|
+
# class a {JOINED} arm then sits beside, and one a hand-written signature pinning the seed rejects — haml's
|
|
97
|
+
# `-> Array[:multi]` accepts `Array[:multi | Dynamic[top]]`, not `Array[Dynamic[top] | Symbol]`. A {REPLACED}
|
|
98
|
+
# position discards the pinning either way.
|
|
99
|
+
#
|
|
100
|
+
# `type` is the widening's pre-state; a `Union` answers `values` and each member asks again as it widens.
|
|
101
|
+
def values_mode(type, method_name, values)
|
|
102
|
+
class_name = value_carrier_class(type)
|
|
103
|
+
return values if class_name.nil?
|
|
104
|
+
|
|
105
|
+
value_position = class_name == "Hash" ? 1 : 0
|
|
106
|
+
rewritten = [REPLACED, JOINED].any? { |table| table.dig(class_name, method_name)&.include?(value_position) }
|
|
107
|
+
rewritten ? :keep : values
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def value_carrier_class(type)
|
|
111
|
+
case type
|
|
112
|
+
when Type::Tuple then "Array"
|
|
113
|
+
when Type::HashShape then "Hash"
|
|
114
|
+
when Type::Nominal then type.class_name
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
private_class_method :value_carrier_class, :arm_refinement
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|