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,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../source/node_children"
|
|
6
|
+
require_relative "captured_locals"
|
|
7
|
+
require_relative "element_read_widening"
|
|
8
|
+
require_relative "jump_targets"
|
|
9
|
+
require_relative "mutation_widening"
|
|
10
|
+
require_relative "receiver_alias"
|
|
11
|
+
|
|
12
|
+
module Rigor
|
|
13
|
+
module Inference
|
|
14
|
+
# Whether an expression `StatementEvaluator` would otherwise type as a pure value — a call's receiver or
|
|
15
|
+
# argument, a literal, an interpolation, a `rescue` modifier — holds something the scope after it must see:
|
|
16
|
+
# a variable write whose binding outlives the expression, or a `next` / `break` whose path a jump join
|
|
17
|
+
# reads. Issue #1223: `out << (n += 1)` left `n` on its pre-write binding, straight-line and through
|
|
18
|
+
# ADR-56's block write-back, and `puts(x && next)` never reached the block's `next` join.
|
|
19
|
+
#
|
|
20
|
+
# A local write counts only when it binds past every block and lambda it is nested in within the
|
|
21
|
+
# expression (its `depth` reaches the expression's own scope): `puts(xs.map { |x| y = x })` binds `y` in
|
|
22
|
+
# the block alone, while `puts(xs.each { t = 1 })` rebinds the outer `t`. Every instance-variable,
|
|
23
|
+
# class-variable and global write counts wherever it is, and so does an index `||=` / `&&=` / `op=`,
|
|
24
|
+
# whose store widens its receiver. A jump counts only where it targets the construct around the
|
|
25
|
+
# expression ({JumpTargets}). A `def`, class or module body is a scope of its own, and `defined?`
|
|
26
|
+
# evaluates nothing, so neither is looked into.
|
|
27
|
+
#
|
|
28
|
+
# An in-place mutation counts on the same terms as a write ({.outliving_mutation?}): a call to a name the
|
|
29
|
+
# straight-line widening responds to ({MutationWidening::SHAPE_MUTATORS}) on a receiver naming a variable
|
|
30
|
+
# that outlives the expression. The mutator's widening runs in the evaluator's post-call effects, which a
|
|
31
|
+
# pure-value operand never reached, so a chained `b.push(2).size`, an argument `puts(b.push(2))` or
|
|
32
|
+
# `d.map!.with_index { … }` left `b` / `d` on the literal their assignment wrote.
|
|
33
|
+
#
|
|
34
|
+
# Short-circuiting and allocation-free outside a mutator-named call: the evaluator asks it of every call's
|
|
35
|
+
# operands, and asks it again of each operand it threads. A found effect stops the recursion without a
|
|
36
|
+
# `return` out of the child block, which would allocate once per frame it unwinds and make a deep literal
|
|
37
|
+
# quadratic.
|
|
38
|
+
module OperandEffects
|
|
39
|
+
LOCAL_WRITE_NODES = CapturedLocals::LOCAL_WRITE_NODES
|
|
40
|
+
OUTLIVING_WRITE_NODES = (
|
|
41
|
+
CapturedLocals::NON_LOCAL_WRITE_NODES |
|
|
42
|
+
Set[Prism::IndexOrWriteNode, Prism::IndexAndWriteNode, Prism::IndexOperatorWriteNode]
|
|
43
|
+
).freeze
|
|
44
|
+
INSTANCE_WRITE_NODES = Set[
|
|
45
|
+
Prism::InstanceVariableWriteNode, Prism::InstanceVariableOperatorWriteNode,
|
|
46
|
+
Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableTargetNode
|
|
47
|
+
].freeze
|
|
48
|
+
GLOBAL_WRITE_NODES = Set[
|
|
49
|
+
Prism::GlobalVariableWriteNode, Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableOrWriteNode,
|
|
50
|
+
Prism::GlobalVariableAndWriteNode, Prism::GlobalVariableTargetNode
|
|
51
|
+
].freeze
|
|
52
|
+
COMPOUND_LOCAL_WRITES = Set[
|
|
53
|
+
Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode
|
|
54
|
+
].freeze
|
|
55
|
+
COMPOUND_INSTANCE_WRITES = Set[
|
|
56
|
+
Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableOrWriteNode,
|
|
57
|
+
Prism::InstanceVariableAndWriteNode
|
|
58
|
+
].freeze
|
|
59
|
+
JUMP_NODES = Set[Prism::NextNode, Prism::BreakNode].freeze
|
|
60
|
+
SCOPE_NODES = Set[Prism::BlockNode, Prism::LambdaNode].freeze
|
|
61
|
+
OPAQUE_NODES = Set[
|
|
62
|
+
Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode, Prism::DefinedNode
|
|
63
|
+
].freeze
|
|
64
|
+
private_constant :LOCAL_WRITE_NODES, :OUTLIVING_WRITE_NODES, :INSTANCE_WRITE_NODES, :GLOBAL_WRITE_NODES,
|
|
65
|
+
:COMPOUND_LOCAL_WRITES,
|
|
66
|
+
:COMPOUND_INSTANCE_WRITES, :JUMP_NODES, :SCOPE_NODES, :OPAQUE_NODES
|
|
67
|
+
|
|
68
|
+
module_function
|
|
69
|
+
|
|
70
|
+
def any?(node)
|
|
71
|
+
node.is_a?(Prism::Node) && effect?(node, 0, true)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# `nesting` counts the blocks and lambdas between `node` and the expression's root; `jumps` is false once
|
|
75
|
+
# the walk has crossed a construct that retargets a jump.
|
|
76
|
+
def effect?(node, nesting, jumps)
|
|
77
|
+
klass = node.class
|
|
78
|
+
return true if LOCAL_WRITE_NODES.include?(klass) && node.depth >= nesting
|
|
79
|
+
return true if OUTLIVING_WRITE_NODES.include?(klass)
|
|
80
|
+
return true if jumps && JUMP_NODES.include?(klass)
|
|
81
|
+
return true if klass == Prism::CallNode && outliving_mutation?(node, nesting)
|
|
82
|
+
return false if OPAQUE_NODES.include?(klass)
|
|
83
|
+
|
|
84
|
+
nesting += 1 if SCOPE_NODES.include?(klass)
|
|
85
|
+
jumps &&= !JumpTargets.boundary?(node)
|
|
86
|
+
found = false
|
|
87
|
+
node.rigor_each_child { |child| found ||= effect?(child, nesting, jumps) }
|
|
88
|
+
found
|
|
89
|
+
end
|
|
90
|
+
private_class_method :effect?
|
|
91
|
+
|
|
92
|
+
# True when `node` is a call the straight-line widening would answer for a variable that outlives the
|
|
93
|
+
# expression: a {MutationWidening::SHAPE_MUTATORS} name on a receiver whose {ReceiverAlias.mutated_reads},
|
|
94
|
+
# or the local an element read is rooted at ({ElementReadWidening.element_read_path}, `a[0] << e`), reach
|
|
95
|
+
# past the blocks between `node` and the expression's root. A block's own local — its parameter, a name it
|
|
96
|
+
# introduces — is that block's to widen.
|
|
97
|
+
def outliving_mutation?(node, nesting)
|
|
98
|
+
return false unless MutationWidening::SHAPE_MUTATORS.include?(node.name)
|
|
99
|
+
|
|
100
|
+
receiver = node.receiver
|
|
101
|
+
return false if receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
102
|
+
|
|
103
|
+
path = ElementReadWidening.element_read_path(receiver)
|
|
104
|
+
return outliving_read?(path.first, nesting) if path
|
|
105
|
+
|
|
106
|
+
ReceiverAlias.mutated_reads(receiver).any? { |read| outliving_read?(read, nesting) }
|
|
107
|
+
end
|
|
108
|
+
private_class_method :outliving_mutation?
|
|
109
|
+
|
|
110
|
+
# A local read reaches the expression's own scope when its `depth` does; an `it` read is always the
|
|
111
|
+
# innermost block's parameter, so only one outside every block in the expression does. An instance
|
|
112
|
+
# variable, class variable or global always outlives it.
|
|
113
|
+
def outliving_read?(read, nesting)
|
|
114
|
+
case read
|
|
115
|
+
when Prism::LocalVariableReadNode then read.depth >= nesting
|
|
116
|
+
when Prism::ItLocalVariableReadNode then nesting.zero?
|
|
117
|
+
else true
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
private_class_method :outliving_read?
|
|
121
|
+
|
|
122
|
+
# The locals (bare names), instance variables and globals (sigil-prefixed names, as {CapturedLocals.bind}
|
|
123
|
+
# reads them) `node` writes on the terms {.any?} counts a write, in first-write order.
|
|
124
|
+
def written_variables(node)
|
|
125
|
+
names = []
|
|
126
|
+
collect_written(node, 0, names) if node.is_a?(Prism::Node)
|
|
127
|
+
names.uniq
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# The locals and instance variables `node` reads, named as {.written_variables} names them. A compound write
|
|
131
|
+
# (`x += 1`, `x ||= v`) reads the variable it writes.
|
|
132
|
+
def read_variables(node)
|
|
133
|
+
names = []
|
|
134
|
+
collect_read(node, 0, names) if node.is_a?(Prism::Node)
|
|
135
|
+
names.uniq
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def collect_read(node, nesting, names)
|
|
139
|
+
klass = node.class
|
|
140
|
+
if klass == Prism::LocalVariableReadNode || COMPOUND_LOCAL_WRITES.include?(klass)
|
|
141
|
+
names << node.name if node.depth >= nesting
|
|
142
|
+
elsif klass == Prism::InstanceVariableReadNode || COMPOUND_INSTANCE_WRITES.include?(klass)
|
|
143
|
+
names << node.name
|
|
144
|
+
end
|
|
145
|
+
return if OPAQUE_NODES.include?(klass)
|
|
146
|
+
|
|
147
|
+
nesting += 1 if SCOPE_NODES.include?(klass)
|
|
148
|
+
node.rigor_each_child { |child| collect_read(child, nesting, names) }
|
|
149
|
+
end
|
|
150
|
+
private_class_method :collect_read
|
|
151
|
+
|
|
152
|
+
def collect_written(node, nesting, names)
|
|
153
|
+
klass = node.class
|
|
154
|
+
if LOCAL_WRITE_NODES.include?(klass)
|
|
155
|
+
names << node.name if node.depth >= nesting
|
|
156
|
+
elsif INSTANCE_WRITE_NODES.include?(klass) || GLOBAL_WRITE_NODES.include?(klass)
|
|
157
|
+
names << node.name
|
|
158
|
+
end
|
|
159
|
+
return if OPAQUE_NODES.include?(klass)
|
|
160
|
+
|
|
161
|
+
nesting += 1 if SCOPE_NODES.include?(klass)
|
|
162
|
+
node.rigor_each_child { |child| collect_written(child, nesting, names) }
|
|
163
|
+
end
|
|
164
|
+
private_class_method :collect_written
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
# Issue #1256 — the later operands of one expression `StatementEvaluator` threads and types as a whole (a
|
|
8
|
+
# call, a literal, a `rescue` modifier): the operands entered from a scope an earlier operand moved, rather
|
|
9
|
+
# than from the one the whole expression is typed from. `puts(b.unshift("s"), b.first.upcase)` enters
|
|
10
|
+
# `b.first.upcase` after the `unshift` widened `b`, and `[n += 1, n += 1]` its second element after the first
|
|
11
|
+
# write.
|
|
12
|
+
#
|
|
13
|
+
# Each such operand is recorded into the per-node scope index with the scope it was entered from, so the
|
|
14
|
+
# diagnostics read it there, and typed from that scope once, so the whole expression's value holds it
|
|
15
|
+
# ({#types}). The operand holding a write is still typed from its own entry: `out << (n += 1)` appends `1`.
|
|
16
|
+
#
|
|
17
|
+
# The walk takes an operand before anything below it, and compares an operand's entry against the scope its
|
|
18
|
+
# nearest taken ancestor is typed from, so each later operand is typed exactly once: from the bottom up, each
|
|
19
|
+
# holding the values of the ones below it. A threaded call below the root is still not typed as a whole
|
|
20
|
+
# (`StatementEvaluator#call_effects`); its later operands join the root's walk.
|
|
21
|
+
class OperandWalk
|
|
22
|
+
# Positions `ExpressionTyper` gives no value of their own: it reads their children directly, never the node,
|
|
23
|
+
# so a later one is recorded but not typed, and its children are compared against its ancestor's scope.
|
|
24
|
+
# A keyword hash is typed whole only as a plain hash: the `**h` form reads the splatted value itself
|
|
25
|
+
# (`ExpressionTyper#double_splat_hash_shape`), so its children are taken instead.
|
|
26
|
+
NON_VALUE_NODES = Set[
|
|
27
|
+
Prism::ArgumentsNode, Prism::AssocNode, Prism::AssocSplatNode, Prism::BlockArgumentNode, Prism::SplatNode,
|
|
28
|
+
Prism::KeywordHashNode
|
|
29
|
+
].freeze
|
|
30
|
+
private_constant :NON_VALUE_NODES
|
|
31
|
+
|
|
32
|
+
# The per-node scope index's `(node, scope) ->` recorder, or nil for an unrecorded pass.
|
|
33
|
+
attr_reader :recorder
|
|
34
|
+
|
|
35
|
+
def initialize(recorder)
|
|
36
|
+
@recorder = recorder
|
|
37
|
+
@entries = nil
|
|
38
|
+
@types = nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The position the next taken operand lands at, for {#types}' `since:`.
|
|
42
|
+
def mark
|
|
43
|
+
@entries.nil? ? 0 : @entries.size
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Takes `node`, entered from `entry`. Answers the slot {#resolve} fills, or nil for a {NON_VALUE_NODES}
|
|
47
|
+
# position.
|
|
48
|
+
def later(node, entry)
|
|
49
|
+
@recorder&.call(node, entry)
|
|
50
|
+
return nil if NON_VALUE_NODES.include?(node.class)
|
|
51
|
+
|
|
52
|
+
(@entries ||= []) << [node, entry, nil]
|
|
53
|
+
@entries.size - 1
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The value the handler that evaluated the operand in `slot` typed it to, from its entry.
|
|
57
|
+
def resolve(slot, type)
|
|
58
|
+
@entries[slot][2] = type
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The identity-comparing table of every taken operand's value, for `ExpressionTyper`'s `operand_types:`, or
|
|
62
|
+
# nil when the walk took none at or after `since` (a {#mark}). An operand the evaluator did not type is typed
|
|
63
|
+
# from its entry here, after every operand below it. A threaded call below the root asks for the operands
|
|
64
|
+
# taken under it once they are all taken; they are typed then and only then, and the table keeps growing,
|
|
65
|
+
# so the root's own request types only what is left.
|
|
66
|
+
def types(tracer, since: 0)
|
|
67
|
+
return nil if @entries.nil? || @entries.size <= since
|
|
68
|
+
|
|
69
|
+
@types ||= {}.compare_by_identity
|
|
70
|
+
(@entries.size - 1).downto(since) do |index|
|
|
71
|
+
node, entry, type = @entries[index]
|
|
72
|
+
next if @types.key?(node)
|
|
73
|
+
|
|
74
|
+
@types[node] = type || self.class.type_of(entry, node, tracer, @types)
|
|
75
|
+
end
|
|
76
|
+
@types
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# `node`'s type from `scope`, answering each node `operand_types` holds with its value there. `Scope#type_of`
|
|
80
|
+
# itself is plugin-facing and keeps its surface, so the table goes to the typer directly.
|
|
81
|
+
def self.type_of(scope, node, tracer, operand_types)
|
|
82
|
+
return scope.type_of(node, tracer: tracer) if operand_types.nil?
|
|
83
|
+
|
|
84
|
+
ExpressionTyper.new(scope: scope, tracer: tracer, operand_types: operand_types).type_of(node)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -43,16 +43,68 @@ module Rigor
|
|
|
43
43
|
# them would widen this channel into a taint that silences genuine diagnostics.
|
|
44
44
|
NIL_COLLAPSING_PREDICATES = %i[nil? !].freeze
|
|
45
45
|
|
|
46
|
+
# Issue #1094 — the binary comparisons that are the same statement as `nil?` when one operand is the `nil`
|
|
47
|
+
# literal: `x == nil`, `nil != x`, `x.equal?(nil)`, `nil === x`. Every one folds from the other operand's
|
|
48
|
+
# nil-freeness alone, so the derivation is exactly as sound as the unary one. A comparison against any
|
|
49
|
+
# other value folds from the carrier's value and stays out, for the reason the value predicates do.
|
|
50
|
+
NIL_COMPARISONS = %i[== != eql? equal? ===].freeze
|
|
51
|
+
|
|
52
|
+
# {.miss_answer}'s answer for an expression whose value on a miss cannot be told.
|
|
53
|
+
UNKNOWN_MISS = Object.new.freeze
|
|
54
|
+
|
|
55
|
+
# Issue #1302 — a local / ivar mark that also records what the bound value answers on a miss, so
|
|
56
|
+
# {.miss_answer} reads a predicate through the binding as it reads the inline form. `Scope` stores it in
|
|
57
|
+
# the mark table in place of the bare cause, so the answer is kept and dropped with the mark itself; a
|
|
58
|
+
# binding whose miss answer cannot be told stores the bare cause, as before.
|
|
59
|
+
BoundMark = Data.define(:cause, :miss)
|
|
60
|
+
|
|
61
|
+
# The marks a binding of today's single cause records, interned so that marking `v = h[k]` allocates
|
|
62
|
+
# nothing beyond the table it always rebuilt.
|
|
63
|
+
INTERNED_BOUND_MARKS = [nil, true, false].to_h do |miss|
|
|
64
|
+
[miss, BoundMark.new(cause: IMPLICITLY_RETURNS_NIL, miss: miss)]
|
|
65
|
+
end.freeze
|
|
66
|
+
private_constant :INTERNED_BOUND_MARKS
|
|
67
|
+
|
|
46
68
|
module_function
|
|
47
69
|
|
|
70
|
+
# What `Scope` stores for a binding marked with `cause` whose value answers `miss` on a miss.
|
|
71
|
+
def bound_mark(cause, miss)
|
|
72
|
+
return cause if miss.equal?(UNKNOWN_MISS)
|
|
73
|
+
|
|
74
|
+
(cause == IMPLICITLY_RETURNS_NIL && INTERNED_BOUND_MARKS[miss]) || BoundMark.new(cause: cause, miss: miss)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The cause and the recorded miss answer of an entry {.bound_mark} made (or nil for no entry).
|
|
78
|
+
def bound_cause(mark) = mark.is_a?(BoundMark) ? mark.cause : mark
|
|
79
|
+
def bound_miss(mark) = mark.is_a?(BoundMark) ? mark.miss : UNKNOWN_MISS
|
|
80
|
+
|
|
81
|
+
# The entry a control-flow join keeps for a name both arms mark: the answer survives only when the arms
|
|
82
|
+
# agree on it, since otherwise a miss can take either arm's.
|
|
83
|
+
def join_bound_marks(mine, theirs) = mine == theirs ? mine : bound_cause(mine)
|
|
84
|
+
|
|
85
|
+
# `rebound` — `scope` with the local `name` rebound to its type across a loop's iterations
|
|
86
|
+
# (`CapturedLocals.bind`) — re-marked with the mark `scope` holds for `name` or `optimistic`, the one an
|
|
87
|
+
# iteration's own rebind made. `scope`'s recorded miss answer is kept unless an iteration's mark joins it:
|
|
88
|
+
# that one comes without its answer, so the pair's cannot be told.
|
|
89
|
+
def carry_local_mark(scope, rebound, name, optimistic)
|
|
90
|
+
miss = optimistic ? UNKNOWN_MISS : scope.optimistic_local_miss(name)
|
|
91
|
+
rebound.with_optimistic_local(name, scope.optimistic_local(name) || optimistic, miss: miss)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# {.carry_local_mark} for an instance variable.
|
|
95
|
+
def carry_ivar_mark(scope, rebound, name, optimistic)
|
|
96
|
+
miss = optimistic ? UNKNOWN_MISS : scope.optimistic_ivar_miss(name)
|
|
97
|
+
rebound.with_optimistic_ivar(name, scope.optimistic_ivar(name) || optimistic, miss: miss)
|
|
98
|
+
end
|
|
99
|
+
|
|
48
100
|
# The effective optimistic-nil-free cause of an expression under `scope`, or nil when its nil-freeness is
|
|
49
101
|
# a property of the value rather than a bet. The single owner of the judgment: `ExpressionTyper`,
|
|
50
102
|
# `StatementEvaluator` and `AlwaysTruthyConditionCollector` all route here, so the three consumers the
|
|
51
103
|
# spec binds cannot drift apart.
|
|
52
104
|
#
|
|
53
105
|
# Resolution order — the mark recorded on the node itself, then the binding a bare local / ivar read (or
|
|
54
|
-
# a write in value position, `if (x = MAP[k])`) resolves through, then
|
|
55
|
-
# issue #313 added.
|
|
106
|
+
# a write in value position, `if (x = MAP[k])`) resolves through, then a safe-navigation call and the
|
|
107
|
+
# predicate-fold derivation issue #313 added.
|
|
56
108
|
def resolve(node, scope)
|
|
57
109
|
return nil if node.nil? || scope.nil?
|
|
58
110
|
|
|
@@ -63,20 +115,85 @@ module Rigor
|
|
|
63
115
|
when Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode then scope.optimistic_local(node.name)
|
|
64
116
|
when Prism::InstanceVariableReadNode, Prism::InstanceVariableWriteNode then scope.optimistic_ivar(node.name)
|
|
65
117
|
when Prism::AndNode, Prism::OrNode then resolve(node.left, scope) || resolve(node.right, scope)
|
|
66
|
-
when Prism::CallNode then resolve_through_predicate(node, scope)
|
|
118
|
+
when Prism::CallNode then resolve_through_safe_navigation(node, scope) || resolve_through_predicate(node, scope)
|
|
67
119
|
when Prism::ParenthesesNode then resolve_through_parentheses(node, scope)
|
|
68
120
|
end
|
|
69
121
|
end
|
|
70
122
|
|
|
71
|
-
# `recv
|
|
72
|
-
# is
|
|
73
|
-
#
|
|
123
|
+
# `recv&.m` is `nil` exactly when `recv` is (or when `m` answers `nil`), so it restates `recv`'s presence
|
|
124
|
+
# and is as optimistic as `recv`. Only the call carrying the `&.` is derived: Ruby skips that one call and
|
|
125
|
+
# no more, so `recv&.m.n` sends `n` to the `nil` and raises (or answers `NilClass#n`), and neither it nor
|
|
126
|
+
# a plain read `recv.m` produces the `nil` a miss would — their own nil-freeness rests on the method's
|
|
127
|
+
# answer. A nil-collapsing predicate over the call (`recv&.m.nil?`) still resolves, through
|
|
128
|
+
# {.resolve_through_predicate} and back here.
|
|
129
|
+
def resolve_through_safe_navigation(node, scope)
|
|
130
|
+
resolve(node.receiver, scope) if node.safe_navigation?
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# `recv.nil?` / `!recv` / `recv == nil` — the fold is a statement about `recv`, so it is exactly as
|
|
134
|
+
# optimistic as `recv` is.
|
|
74
135
|
def resolve_through_predicate(node, scope)
|
|
75
|
-
|
|
136
|
+
operand = nil_question_operand(node)
|
|
137
|
+
operand && resolve(operand, scope)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# The operand a nil-collapsing predicate restates the nil-ness of, or nil when `node` is not one. A block
|
|
141
|
+
# or any argument means `nil?` / `!` is not the unary predicate it looks like (`x.!(y)` is a user-defined
|
|
142
|
+
# operator). A comparison needs exactly one positional argument and exactly one `nil` literal side, and
|
|
143
|
+
# answers the other side (`nil == nil` states nothing about a carrier).
|
|
144
|
+
def nil_question_operand(node)
|
|
76
145
|
return nil unless node.block.nil?
|
|
77
|
-
return nil unless node.arguments.nil? || node.arguments.arguments.empty?
|
|
78
146
|
|
|
79
|
-
|
|
147
|
+
arguments = node.arguments&.arguments || []
|
|
148
|
+
if NIL_COLLAPSING_PREDICATES.include?(node.name)
|
|
149
|
+
node.receiver if arguments.empty?
|
|
150
|
+
elsif NIL_COMPARISONS.include?(node.name) && arguments.size == 1
|
|
151
|
+
nil_comparison_operand(node.receiver, arguments.first)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def nil_comparison_operand(receiver, argument)
|
|
156
|
+
if argument.is_a?(Prism::NilNode) && !receiver.nil? && !receiver.is_a?(Prism::NilNode)
|
|
157
|
+
receiver
|
|
158
|
+
elsif receiver.is_a?(Prism::NilNode) && !argument.is_a?(Prism::NilNode)
|
|
159
|
+
argument
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# What a marked expression answers when the carrier its mark rests on misses, or {UNKNOWN_MISS}. The
|
|
164
|
+
# carrier itself — the read the mark is recorded on, or a `recv&.m` over a marked receiver — answers `nil`,
|
|
165
|
+
# and each nil-collapsing predicate over it answers what it answers for that value: `!` its negation,
|
|
166
|
+
# `nil?` / `== nil` whether it is `nil`, `!= nil` the reverse. A local / ivar read answers what its binding
|
|
167
|
+
# recorded beside the mark ({BoundMark}, issue #1302): `nil` for `x = h[k]` and for `x = recv&.m?`. An
|
|
168
|
+
# `&&` / `||`, a binding that recorded nothing, and anything else is unknown. `ExpressionTyper` widens a
|
|
169
|
+
# predicate's folded boolean only when the miss answers the other boolean (or cannot be told), so
|
|
170
|
+
# `!recv&.empty?` — `!false` on a hit, `!nil` on a miss — stays `true`, and so does `x = recv&.empty?; !x`.
|
|
171
|
+
def miss_answer(node, scope)
|
|
172
|
+
return nil if scope.optimistic_origins[node]
|
|
173
|
+
|
|
174
|
+
case node
|
|
175
|
+
when Prism::LocalVariableReadNode then scope.optimistic_local_miss(node.name)
|
|
176
|
+
when Prism::InstanceVariableReadNode then scope.optimistic_ivar_miss(node.name)
|
|
177
|
+
when Prism::CallNode then miss_answer_of_call(node, scope)
|
|
178
|
+
when Prism::ParenthesesNode
|
|
179
|
+
body = node.body
|
|
180
|
+
body.is_a?(Prism::StatementsNode) && body.body.size == 1 ? miss_answer(body.body.first, scope) : UNKNOWN_MISS
|
|
181
|
+
else UNKNOWN_MISS
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def miss_answer_of_call(node, scope)
|
|
186
|
+
return nil if node.safe_navigation? && resolve(node.receiver, scope)
|
|
187
|
+
|
|
188
|
+
operand = nil_question_operand(node)
|
|
189
|
+
inner = operand ? miss_answer(operand, scope) : UNKNOWN_MISS
|
|
190
|
+
return UNKNOWN_MISS if inner.equal?(UNKNOWN_MISS)
|
|
191
|
+
|
|
192
|
+
case node.name
|
|
193
|
+
when :! then !inner
|
|
194
|
+
when :!= then !inner.nil?
|
|
195
|
+
else inner.nil?
|
|
196
|
+
end
|
|
80
197
|
end
|
|
81
198
|
|
|
82
199
|
# `(x.nil?)` — a single-statement parenthesised body is its own value, and authors do parenthesise a
|
|
@@ -89,6 +206,32 @@ module Rigor
|
|
|
89
206
|
resolve(body.body.first, scope)
|
|
90
207
|
end
|
|
91
208
|
|
|
209
|
+
# The mark a multiple assignment's right-hand side hands to the slots it fills, in the shape
|
|
210
|
+
# `MultiTargetBinder.bind_marked`'s `optimistic:` takes. `true` when the whole right-hand side resolves
|
|
211
|
+
# to a mark: a miss makes it `nil`, and `k, v = nil` binds `nil` to every fixed slot, so each slot's
|
|
212
|
+
# nil-freeness is the same bet (the rule a nested target under an optimistic slot already follows). A
|
|
213
|
+
# literal array right-hand side (`x, y = h[k], 1`) is judged element-wise instead — an Array of each
|
|
214
|
+
# element's own marks, nested for a nested literal — since each element is its slot's value; a splat
|
|
215
|
+
# element makes the slot positions unknowable and declines. `false` when nothing is marked.
|
|
216
|
+
def destructuring_marks(node, scope)
|
|
217
|
+
return true if resolve(node, scope)
|
|
218
|
+
return false unless node.is_a?(Prism::ArrayNode) && node.elements.none?(Prism::SplatNode)
|
|
219
|
+
|
|
220
|
+
marks = node.elements.map { |element| destructuring_marks(element, scope) }
|
|
221
|
+
marks.any? { |mark| mark != false } ? marks : false
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Issue #1302 — the miss answer every slot {.destructuring_marks} marks records: `nil` when each marked
|
|
225
|
+
# part of the right-hand side answers `nil` on a miss, since `nil` destructures to `nil` in every slot, and
|
|
226
|
+
# {UNKNOWN_MISS} otherwise. A marked right-hand side a miss makes a boolean (`a, b = !h[k]`) binds it to
|
|
227
|
+
# the first slot only, so its slots are not told apart and all of them record nothing.
|
|
228
|
+
def destructuring_miss(node, scope)
|
|
229
|
+
return miss_answer(node, scope).nil? ? nil : UNKNOWN_MISS if resolve(node, scope)
|
|
230
|
+
return nil unless node.is_a?(Prism::ArrayNode)
|
|
231
|
+
|
|
232
|
+
node.elements.all? { |element| destructuring_miss(element, scope).nil? } ? nil : UNKNOWN_MISS
|
|
233
|
+
end
|
|
234
|
+
|
|
92
235
|
# Whether the overload the selector actually picked carries the ignored annotation. The judgment is
|
|
93
236
|
# per-overload, which is what makes it precise: `Array#first` is optimistic while `Array#first(3)` is
|
|
94
237
|
# not, and `String#[]` / `Enumerable#find` are honest because they already spell the miss as `?`.
|
|
@@ -265,7 +265,7 @@ module Rigor
|
|
|
265
265
|
method_visibilities: :discovered_method_visibilities,
|
|
266
266
|
methods: :discovered_methods,
|
|
267
267
|
data_member_layouts: :data_member_layouts,
|
|
268
|
-
struct_member_layouts: :struct_member_layouts
|
|
268
|
+
struct_member_layouts: :struct_member_layouts, deferred_ranges: :discovered_deferred_ranges
|
|
269
269
|
}.freeze
|
|
270
270
|
private_constant :DISCOVERY_FIELD
|
|
271
271
|
|
|
@@ -329,7 +329,8 @@ module Rigor
|
|
|
329
329
|
arg.is_a?(Prism::AssocSplatNode)
|
|
330
330
|
end
|
|
331
331
|
|
|
332
|
-
# @return , nil
|
|
332
|
+
# @return the callee's `[class_name, method_name, kind, def_node]`, or nil when the receiver's class
|
|
333
|
+
# or the method's `def` is not in the project index
|
|
333
334
|
def resolve_callee(call_node, scope, index)
|
|
334
335
|
if call_node.receiver.nil?
|
|
335
336
|
class_name, kind = implicit_self_target(scope)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../type"
|
|
4
|
+
require_relative "external_ancestor_resolution"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
# Issue #1234 — does the PROJECT define the method a receiver answers a name with? The captured-binding
|
|
9
|
+
# pass reads a catalogued iterator name on an unclassified receiver as repetition
|
|
10
|
+
# ({ClosureEscapeAnalyzer.repeats_by_name?}), and that reading is wrong for a method the project wrote
|
|
11
|
+
# under the same name: `class Vault; def select(key) = yield(key.to_s); end` runs its block once. The
|
|
12
|
+
# ancestry step of {ClosureEscapeAnalyzer.classify} asks the same question, of the same call, to rule the
|
|
13
|
+
# project out as the method's owner.
|
|
14
|
+
#
|
|
15
|
+
# "Defines" is a `def`, `define_method` or `attr_*` anywhere in a project class's ancestry, on the
|
|
16
|
+
# instance side or on the singleton side for a class-object receiver, or a signature whose declaring
|
|
17
|
+
# owner is a project class or module.
|
|
18
|
+
module ProjectMethodOwnership
|
|
19
|
+
NO_TARGETS = [].freeze
|
|
20
|
+
private_constant :NO_TARGETS
|
|
21
|
+
|
|
22
|
+
MEMO_KEY = :__rigor_project_method_ownership_memo__
|
|
23
|
+
private_constant :MEMO_KEY
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# The `[class_name, kind]` pairs the receiver's method is looked up on — `[]` for a receiver Rigor
|
|
28
|
+
# cannot see at all — or nil when the carrier names a class this cannot resolve, or is one it does not
|
|
29
|
+
# recognise, so a caller that cannot tell whether the project owns the method can decline. The audit,
|
|
30
|
+
# carrier by carrier:
|
|
31
|
+
#
|
|
32
|
+
# - `Dynamic`, `Top` — unseen: `[]`.
|
|
33
|
+
# - `Nominal`, `StructInstance`, `DataInstance` — their `class_name`, instance side.
|
|
34
|
+
# - `Singleton`, `StructClass`, `DataClass` — their `class_name`, singleton side.
|
|
35
|
+
# - `Tuple` → `Array`, `HashShape` → `Hash`, `IntegerRange` → `Integer`, `FloatRange` → `Float`,
|
|
36
|
+
# `Constant` → its value's class, `BoundMethod` → `Method`: instance side of that core class, which a
|
|
37
|
+
# project reopening may still define the method on.
|
|
38
|
+
# - `Union`, `Intersection` — every member's targets; a member it cannot resolve resolves the whole.
|
|
39
|
+
# - `Refined`, `Difference` — their `base`. `App` — its erasure `bound`.
|
|
40
|
+
# - A struct / data carrier with no `class_name` (an anonymous `Struct.new` value), `Bot`, `Maybe`,
|
|
41
|
+
# `Result`, and any carrier added later — nil.
|
|
42
|
+
def targets(receiver_type)
|
|
43
|
+
case receiver_type
|
|
44
|
+
when Type::Dynamic, Type::Top then NO_TARGETS
|
|
45
|
+
when Type::Nominal, Type::StructInstance, Type::DataInstance
|
|
46
|
+
class_target(receiver_type.class_name, :instance)
|
|
47
|
+
when Type::Singleton, Type::StructClass, Type::DataClass
|
|
48
|
+
class_target(receiver_type.class_name, :singleton)
|
|
49
|
+
when Type::Union, Type::Intersection then member_targets(receiver_type.members)
|
|
50
|
+
when Type::Refined, Type::Difference then targets(receiver_type.base)
|
|
51
|
+
when Type::App then targets(receiver_type.bound)
|
|
52
|
+
else core_targets(receiver_type)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Whether the project's source, or a signature whose declaring owner is a project class or module,
|
|
57
|
+
# defines `method_sym` on `class_name` or an ancestor the project declares. Memoised ({.memo}): the
|
|
58
|
+
# ancestry step and the name reading ask it of the same call, and every block call on the class asks
|
|
59
|
+
# it again.
|
|
60
|
+
def defines?(class_name, method_sym, kind, scope)
|
|
61
|
+
return false if scope.nil?
|
|
62
|
+
|
|
63
|
+
by_method = (memo(scope)[kind][class_name] ||= {})
|
|
64
|
+
return by_method[method_sym] if by_method.key?(method_sym)
|
|
65
|
+
|
|
66
|
+
by_method[method_sym] = compute_defines?(class_name, method_sym, kind, scope)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# One slot per thread, keyed on the identity of the scope's frozen discovery index and its environment —
|
|
70
|
+
# the only inputs {.defines?} and the ancestry step read — and replaced, not accumulated, when either
|
|
71
|
+
# changes: the shape of `ExpressionTyper#class_graph_buckets`, whose rationale applies unchanged. A
|
|
72
|
+
# `Scope` merges each file's discovery with the project pre-pass, so the slot turns over per file, and
|
|
73
|
+
# an ADR-46 dependency the first lookup recorded belongs to the file every later hit serves. Pool
|
|
74
|
+
# workers are separate processes, and a Ractor has its own `Thread.current`. The `:ancestry` bucket
|
|
75
|
+
# belongs to {ClosureEscapeAnalyzer}'s ancestry step.
|
|
76
|
+
def memo(scope)
|
|
77
|
+
discovery = scope.discovery
|
|
78
|
+
environment = scope.environment
|
|
79
|
+
slot = Thread.current[MEMO_KEY]
|
|
80
|
+
unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment)
|
|
81
|
+
slot = [discovery, environment, { instance: {}, singleton: {}, ancestry: {} }]
|
|
82
|
+
Thread.current[MEMO_KEY] = slot
|
|
83
|
+
end
|
|
84
|
+
slot[2]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
class << self
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def class_target(class_name, kind)
|
|
91
|
+
class_name.nil? ? nil : [[class_name.to_s, kind]]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def member_targets(members)
|
|
95
|
+
members.each_with_object([]) do |member, out|
|
|
96
|
+
resolved = targets(member)
|
|
97
|
+
return nil if resolved.nil?
|
|
98
|
+
|
|
99
|
+
out.concat(resolved)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def core_targets(receiver_type)
|
|
104
|
+
case receiver_type
|
|
105
|
+
when Type::Tuple then class_target("Array", :instance)
|
|
106
|
+
when Type::HashShape then class_target("Hash", :instance)
|
|
107
|
+
when Type::IntegerRange then class_target("Integer", :instance)
|
|
108
|
+
when Type::FloatRange then class_target("Float", :instance)
|
|
109
|
+
when Type::Constant then class_target(receiver_type.value.class.name, :instance)
|
|
110
|
+
when Type::BoundMethod then class_target("Method", :instance)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def compute_defines?(class_name, method_sym, kind, scope)
|
|
115
|
+
return true if source_defines?(class_name, method_sym, kind, scope)
|
|
116
|
+
|
|
117
|
+
# `ExternalAncestorResolution.method_definition` owns the rescue for a malformed signature.
|
|
118
|
+
definition = ExternalAncestorResolution.method_definition(class_name, method_sym, kind, scope: scope)
|
|
119
|
+
owner = definition.respond_to?(:defined_in) ? definition.defined_in : nil
|
|
120
|
+
!owner.nil? && scope.known_user_class?(owner.to_s.delete_prefix("::"))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def source_defines?(class_name, method_sym, kind, scope)
|
|
124
|
+
return true if scope.discovered_method_through_ancestors?(class_name, method_sym, kind)
|
|
125
|
+
|
|
126
|
+
found, = if kind == :singleton
|
|
127
|
+
scope.singleton_def_through_ancestors(class_name, method_sym)
|
|
128
|
+
else
|
|
129
|
+
scope.user_def_through_ancestors(class_name, method_sym)
|
|
130
|
+
end
|
|
131
|
+
!found.nil?
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "global_write_census"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
module Inference
|
|
5
7
|
# ADR-17 § "Inference contract" — project-wide patched-method registry populated by the pre-eval
|
|
@@ -26,16 +28,19 @@ module Rigor
|
|
|
26
28
|
end
|
|
27
29
|
end
|
|
28
30
|
|
|
29
|
-
attr_reader :by_key
|
|
31
|
+
attr_reader :by_key, :write_census
|
|
30
32
|
|
|
31
33
|
# @param entries — flat list of declarations observed during the pre-pass.
|
|
32
34
|
# First-write-wins on `(class_name, method_name, kind)` duplicates so the
|
|
33
35
|
# `pre-eval.duplicate-declaration` diagnostic emission stays decoupled from registry behaviour.
|
|
34
|
-
|
|
36
|
+
# @param write_census — issue #1367: the `pre_eval:` files' {GlobalWriteCensus}, which the `global.*` write
|
|
37
|
+
# rules join with the project's, since a patch file is loaded ahead of the code it patches.
|
|
38
|
+
def initialize(entries: [], write_census: GlobalWriteCensus::EMPTY)
|
|
35
39
|
@by_key = entries.each_with_object({}) do |entry, acc|
|
|
36
40
|
key = [entry.class_name, entry.method_name, entry.kind]
|
|
37
41
|
acc[key] ||= entry
|
|
38
42
|
end.freeze
|
|
43
|
+
@write_census = write_census.frozen? ? write_census : write_census.dup.freeze
|
|
39
44
|
freeze
|
|
40
45
|
end
|
|
41
46
|
|