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,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
# The calls that keep their literal block to run later instead of running it before they return: the Kernel
|
|
8
|
+
# functions that wrap it in a Proc (`lambda`, `proc`), the definers that install it as a method body
|
|
9
|
+
# (`define_method`, `define_singleton_method`, on any receiver), and the constructors that keep it as a proc,
|
|
10
|
+
# a thread or fiber body, an enumerator's generator or a hash's default proc.
|
|
11
|
+
#
|
|
12
|
+
# A `break` in such a block never becomes the call's value — it returns from the lambda or the defined method
|
|
13
|
+
# when that is called, or raises `LocalJumpError` — so the #853 break-arm union
|
|
14
|
+
# (`ExpressionTyper#call_break_arm_types`) MUST NOT read it. Unioning the arm typed
|
|
15
|
+
# `lambda do |t| break if flag; t end` as `Proc | nil`, and the lambda's next `.call` drew
|
|
16
|
+
# `call.possible-nil-receiver`.
|
|
17
|
+
#
|
|
18
|
+
# A project redefinition of one of these names is not excluded: dropping the arm there loses a `break`
|
|
19
|
+
# value, never invents one.
|
|
20
|
+
module StoredBlockCall
|
|
21
|
+
KERNEL_WRAPPERS = %i[lambda proc].freeze
|
|
22
|
+
DEFINERS = %i[define_method define_singleton_method].freeze
|
|
23
|
+
CONSTRUCTORS = {
|
|
24
|
+
Proc: %i[new], Thread: %i[new start fork], Fiber: %i[new], Enumerator: %i[new], Hash: %i[new]
|
|
25
|
+
}.transform_values(&:freeze).freeze
|
|
26
|
+
NO_NAMES = [].freeze
|
|
27
|
+
private_constant :KERNEL_WRAPPERS, :DEFINERS, :CONSTRUCTORS, :NO_NAMES
|
|
28
|
+
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
def stores_block?(call_node)
|
|
32
|
+
name = call_node.name
|
|
33
|
+
return kernel_spelled?(call_node.receiver) if KERNEL_WRAPPERS.include?(name)
|
|
34
|
+
return true if DEFINERS.include?(name)
|
|
35
|
+
|
|
36
|
+
constant = root_constant_name(call_node.receiver)
|
|
37
|
+
!constant.nil? && CONSTRUCTORS.fetch(constant, NO_NAMES).include?(name)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Implicit self, `self.`, or `Kernel.` / `::Kernel.` — the spellings that reach Kernel's function.
|
|
41
|
+
def kernel_spelled?(receiver)
|
|
42
|
+
receiver.nil? || receiver.is_a?(Prism::SelfNode) || root_constant_name(receiver) == :Kernel
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# `Foo` or `::Foo`'s name, else nil.
|
|
46
|
+
def root_constant_name(receiver)
|
|
47
|
+
case receiver
|
|
48
|
+
when Prism::ConstantReadNode then receiver.name
|
|
49
|
+
when Prism::ConstantPathNode then receiver.name if receiver.parent.nil?
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -17,21 +17,39 @@ module Rigor
|
|
|
17
17
|
module StringMutation
|
|
18
18
|
# Receiver-mutating String methods only. The non-bang siblings (`upcase` vs `upcase!`, `sub` vs `sub!`)
|
|
19
19
|
# return a new String and leave the receiver's value intact, so they stay precise.
|
|
20
|
+
#
|
|
21
|
+
# This is the one String table: `MutationWidening::SHAPE_MUTATORS` unions it with the Array and Hash ones, and
|
|
22
|
+
# the effect classifier and the effect catalogue's `mutators: string` cite it rather than keep a list of their
|
|
23
|
+
# own (ADR-103 WD3). A spec holds it to every bang method `String` defines plus the non-bang mutators reflection
|
|
24
|
+
# cannot find by name.
|
|
20
25
|
MUTATORS = %i[
|
|
21
26
|
<< concat insert prepend replace clear []= slice!
|
|
27
|
+
setbyte bytesplice append_as_bytes force_encoding
|
|
22
28
|
sub! gsub! tr! tr_s! delete! squeeze! succ! next!
|
|
23
29
|
upcase! downcase! capitalize! swapcase! reverse!
|
|
24
|
-
strip! lstrip! rstrip! chomp! chop!
|
|
30
|
+
strip! lstrip! rstrip! chomp! chop!
|
|
31
|
+
delete_prefix! delete_suffix! encode! scrub! unicode_normalize!
|
|
25
32
|
].to_set.freeze
|
|
26
33
|
|
|
27
|
-
# The {MUTATORS} that
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
# The {MUTATORS} that CANNOT leave a non-empty receiver empty, and so keep a `non-empty-string` refinement:
|
|
35
|
+
# appenders (`<<`, `concat`, `insert`, `prepend`, `append_as_bytes`), and rewrites that keep at least one
|
|
36
|
+
# character (`setbyte`, `force_encoding`, the case mappings, `reverse!`, `succ!` / `next!`, `squeeze!`,
|
|
37
|
+
# `unicode_normalize!`). A whitelist, as `RefinementMutation::EMPTY_PRESERVING` is for Array and Hash: a mutator
|
|
38
|
+
# missing here retracts the witness, which costs precision, where a mutator missing from a list of emptiers kept
|
|
39
|
+
# the witness of a string it had just emptied — `tr!` and `tr_s!` did, since `"a".tr!("a", "")` is `""`.
|
|
40
|
+
EMPTY_PRESERVING = %i[
|
|
41
|
+
<< concat insert prepend append_as_bytes
|
|
42
|
+
setbyte force_encoding
|
|
43
|
+
upcase! downcase! capitalize! swapcase! reverse! succ! next! squeeze! unicode_normalize!
|
|
33
44
|
].to_set.freeze
|
|
34
45
|
|
|
46
|
+
# `tr!` / `tr_s!` map every character they match to one character of the replacement (padded with its last), so
|
|
47
|
+
# they empty the buffer only through an EMPTY replacement: `"a".tr!("a", "")` is `""`, `"a".tr!("a", "_")` is
|
|
48
|
+
# `"_"`. They keep the witness when their second argument is provably a non-empty String.
|
|
49
|
+
TRANSLATORS = %i[tr! tr_s!].to_set.freeze
|
|
50
|
+
|
|
51
|
+
NO_ARG_TYPES = [].freeze
|
|
52
|
+
|
|
35
53
|
module_function
|
|
36
54
|
|
|
37
55
|
# `Constant["ab"]` under a String mutator → the bare `String` nominal. The pinned value is what the
|
|
@@ -55,6 +73,25 @@ module Rigor
|
|
|
55
73
|
def constant?(type)
|
|
56
74
|
type.is_a?(Type::Constant) && type.value.is_a?(String)
|
|
57
75
|
end
|
|
76
|
+
|
|
77
|
+
# Whether `method_name`, called with `arg_types` (the call's positional argument types, possibly none), can leave
|
|
78
|
+
# a non-empty receiver empty, and so retract a `non-empty-string` refinement. Every mutator outside
|
|
79
|
+
# {EMPTY_PRESERVING} can, except a {TRANSLATORS} call whose replacement is provably non-empty.
|
|
80
|
+
def may_empty?(method_name, arg_types = NO_ARG_TYPES)
|
|
81
|
+
return false if EMPTY_PRESERVING.include?(method_name)
|
|
82
|
+
return true unless TRANSLATORS.include?(method_name)
|
|
83
|
+
|
|
84
|
+
!non_empty_string?(arg_types[1])
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def non_empty_string?(type)
|
|
88
|
+
case type
|
|
89
|
+
when Type::Constant then constant?(type) && !type.value.empty?
|
|
90
|
+
when Type::Difference then type.removes_empty_witness? && type.base.class_name == "String"
|
|
91
|
+
when Type::Union then type.members.all? { |member| non_empty_string?(member) }
|
|
92
|
+
else false
|
|
93
|
+
end
|
|
94
|
+
end
|
|
58
95
|
end
|
|
59
96
|
end
|
|
60
97
|
end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../type"
|
|
6
|
+
require_relative "element_read_widening"
|
|
7
|
+
require_relative "mutation_widening"
|
|
8
|
+
|
|
9
|
+
module Rigor
|
|
10
|
+
module Inference
|
|
11
|
+
# What a binding can hold after each of a set of in-place mutation sites — the nodes
|
|
12
|
+
# {CapturedLocals.content_mutations} collects — has run any number of times, storing values nothing typed.
|
|
13
|
+
#
|
|
14
|
+
# Each site takes its own straight-line widening ({MutationWidening.widen_for_mutator}), fed one
|
|
15
|
+
# `Dynamic[top]` per argument, and every site that can STORE a value then gives each type argument of the
|
|
16
|
+
# widened `Array` / `Hash` a `Dynamic[top]` arm: `{ a: 0 }` under `h[k] += 1` reads
|
|
17
|
+
# `Hash[Dynamic[top] | Symbol, Dynamic[top] | Integer]`. The second step is not redundant. The widening
|
|
18
|
+
# joins argument evidence only for the plain adders (`<<`, `push`, `[]=`, `store`, …); a site whose stored
|
|
19
|
+
# values its arguments do not describe — `map!`, a block-form `fill`, `transform_values!`, `merge!`,
|
|
20
|
+
# `flatten!` — comes back with the seed's element types alone, which is a wrong precise answer once the
|
|
21
|
+
# block has run: `a = [1]` under `a.map!(&:to_s)` would read `Array[Integer]`. Only a site that removes or
|
|
22
|
+
# reorders ({VALUE_PRESERVING}) keeps its content as the widening left it, since it cannot put a value
|
|
23
|
+
# there that was not there before — unless it is the site that CLOSES a literal carrier. A `Tuple` or
|
|
24
|
+
# `HashShape` knows which slots and keys exist, and the nominal it widens to cannot say that any may be
|
|
25
|
+
# missing, so it takes the gradual arm too: `{ a: 0 }` under a lone `h.delete(:a)` read `Hash[Symbol, 0]`,
|
|
26
|
+
# whose `h[:b]` answered `0` where Ruby answers `nil`, and `s = [0]` under `p = s.pop; s.push(x)` closed to
|
|
27
|
+
# `Array[0]`, a precise nominal the `push` after it then declined, so `p` stayed `0?`. With the arm the
|
|
28
|
+
# answer for a `Tuple` / `HashShape` seed (alone or in a `Union`) no longer depends on the order the sites are
|
|
29
|
+
# written in; a refinement seed (`non-empty-array[0]`) still can, since its remover does not close a literal.
|
|
30
|
+
#
|
|
31
|
+
# A site whose widening declines leaves the binding as it is, exactly as the straight-line seam does: a
|
|
32
|
+
# precise nominal (a declared or inferred `Array[String]` is a claim this seam may not grow), a receiver
|
|
33
|
+
# that is no carrier, or a name the mutator tables do not list for that carrier (`store` on a `Tuple`, a method
|
|
34
|
+
# its class does not define). The caller MUST therefore not read an unchanged binding as describing later
|
|
35
|
+
# iterations. An empty-witness refinement under a storing mutator that keeps the witness is not such a decline:
|
|
36
|
+
# the straight-line widening grows it (issue #936), and gives a site whose arguments describe nothing it stores
|
|
37
|
+
# (`non-empty-array[String]` under `map!`) the gradual arm itself ({RewriteMutation}),
|
|
38
|
+
# so the site widens like any other storing site.
|
|
39
|
+
#
|
|
40
|
+
# The per-element block fold is the consumer: it types every position from one entry scope, so the binding
|
|
41
|
+
# it lays under each position has to hold whatever earlier iterations stored. Unknown evidence is the point,
|
|
42
|
+
# not a shortcut — a stored value typed in that same entry scope (`h[k] = h[k] + 1`) is itself a
|
|
43
|
+
# first-iteration answer.
|
|
44
|
+
#
|
|
45
|
+
# Two more kinds of site change a binding's contents without naming it as the receiver, and each is widened
|
|
46
|
+
# the way straight-line code widens it:
|
|
47
|
+
#
|
|
48
|
+
# - A mutator on an ELEMENT of the binding (`a[0] << e`, `a.first.push(e)`): the site's receiver is an element
|
|
49
|
+
# read rooted at the variable ({ElementReadWidening.element_read_path}), and the same widening runs on the
|
|
50
|
+
# element the path selects, rebuilding the carrier around it. The other slots keep their types, and a path
|
|
51
|
+
# {ElementReadWidening} cannot follow (a `HashShape` slot) declines, as it does on straight-line code.
|
|
52
|
+
# - A self-call whose callee content-mutates the parameter the binding is passed to (`add_to(a, e)`, a
|
|
53
|
+
# {CalleeStore}): nothing at the call says what the callee stores or removes, so each collection the binding
|
|
54
|
+
# can hold is floored to its bare carrier ({.content_floor}), as ADR-57's callee floor does after the call.
|
|
55
|
+
module UnknownStoreWidening
|
|
56
|
+
# Mutators that only remove or reorder what the receiver already holds.
|
|
57
|
+
VALUE_PRESERVING = %i[
|
|
58
|
+
pop shift delete delete_at delete_if reject! select! filter! keep_if clear compact! uniq! slice!
|
|
59
|
+
sort! sort_by! reverse! rotate! shuffle!
|
|
60
|
+
].to_set.freeze
|
|
61
|
+
|
|
62
|
+
# A site that mutates a binding through a callee: `call` passes the variable, read by one of `arguments`, to
|
|
63
|
+
# a parameter the callee content-mutates.
|
|
64
|
+
CalleeStore = Data.define(:call, :arguments)
|
|
65
|
+
|
|
66
|
+
COLLECTION_CLASSES = %w[Array Hash].freeze
|
|
67
|
+
FLOORED_CLASSES = %w[Array Hash String].freeze
|
|
68
|
+
|
|
69
|
+
NO_ARG_NODES = [].freeze
|
|
70
|
+
private_constant :COLLECTION_CLASSES, :FLOORED_CLASSES, :NO_ARG_NODES
|
|
71
|
+
|
|
72
|
+
module_function
|
|
73
|
+
|
|
74
|
+
# @param sites — in-place mutation nodes whose receiver, or an element read their receiver starts from, can
|
|
75
|
+
# evaluate to the binding `type` describes, and {CalleeStore}s whose call passes that binding.
|
|
76
|
+
# @return `type` widened through every site; `type` itself when no site's widening applies.
|
|
77
|
+
def widen(type, sites)
|
|
78
|
+
sites.reduce(type) { |acc, site| widen_site(acc, site) || acc }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# `type` widened through one site, or `nil` when its widening declines.
|
|
82
|
+
def widen_site(type, site)
|
|
83
|
+
return content_floor(type) if site.is_a?(CalleeStore)
|
|
84
|
+
|
|
85
|
+
method_name, arg_types = unknown_store(site)
|
|
86
|
+
path = ElementReadWidening.element_read_path(site.receiver)
|
|
87
|
+
return widen_store(type, method_name, arg_types) if path.nil?
|
|
88
|
+
|
|
89
|
+
ElementReadWidening.widen_through_path(type, path.last, method_name, arg_types) do |element|
|
|
90
|
+
widen_store(element, method_name, arg_types)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# The straight-line widening of `type` under `method_name`, with the gradual arm on every site that can store.
|
|
95
|
+
def widen_store(type, method_name, arg_types)
|
|
96
|
+
widened = MutationWidening.widen_for_mutator(type, method_name, arg_types: arg_types)
|
|
97
|
+
return nil if widened.nil?
|
|
98
|
+
|
|
99
|
+
VALUE_PRESERVING.include?(method_name) && !literal_carrier?(type) ? widened : gradual_content(widened)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Every `Array` / `Hash` / `String` carrier `type` can be as its bare carrier: `Array[Dynamic[top]]`,
|
|
103
|
+
# `Hash[Dynamic[top], Dynamic[top]]`, `String`. A literal, a nominal (a precise one included) and a refinement
|
|
104
|
+
# over one ({.carrier_class}) all floor; so `non-empty-string`, which a callee can empty, reads `String`.
|
|
105
|
+
# Unlike the escaping floor (`StatementEvaluator#content_floor_for`), which answers one carrier for a whole
|
|
106
|
+
# `Union`, this floors a `Union` member by member, so a member no callee can fill (`nil`) stays: the binding
|
|
107
|
+
# stands where the entry binding stood, and dropping a member would narrow it. Anything else is returned
|
|
108
|
+
# untouched.
|
|
109
|
+
def content_floor(type)
|
|
110
|
+
return Type::Combinator.union(*type.members.map { |member| content_floor(member) }) if type.is_a?(Type::Union)
|
|
111
|
+
|
|
112
|
+
carrier = carrier_class(type)
|
|
113
|
+
carrier ? carrier_floor(carrier) : type
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# `"Array"`, `"Hash"` or `"String"` when `type` is a form of that carrier — a literal, a nominal, or a
|
|
117
|
+
# difference, refinement or intersection over one (`non-empty-array[Integer]`, `decimal-int-string`,
|
|
118
|
+
# `non-empty-uppercase-string`) — else nil.
|
|
119
|
+
def carrier_class(type)
|
|
120
|
+
case type
|
|
121
|
+
when Type::Tuple then "Array"
|
|
122
|
+
when Type::HashShape then "Hash"
|
|
123
|
+
when Type::Constant then "String" if type.value.is_a?(String)
|
|
124
|
+
when Type::Nominal then type.class_name if FLOORED_CLASSES.include?(type.class_name)
|
|
125
|
+
when Type::Difference, Type::Refined then carrier_class(type.base)
|
|
126
|
+
when Type::Intersection then intersection_carrier_class(type)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def intersection_carrier_class(type)
|
|
131
|
+
type.members.each do |member|
|
|
132
|
+
carrier = carrier_class(member)
|
|
133
|
+
return carrier if carrier
|
|
134
|
+
end
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def carrier_floor(class_name)
|
|
139
|
+
return Type::Combinator.nominal_of("String") if class_name == "String"
|
|
140
|
+
|
|
141
|
+
arity = class_name == "Hash" ? 2 : 1
|
|
142
|
+
Type::Combinator.nominal_of(class_name, type_args: Array.new(arity) { Type::Combinator.untyped })
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# A carrier that still records its slots or keys: a `Tuple` / `HashShape`, alone or as a `Union` member.
|
|
146
|
+
def literal_carrier?(type)
|
|
147
|
+
case type
|
|
148
|
+
when Type::Tuple, Type::HashShape then true
|
|
149
|
+
when Type::Union then type.members.any? { |member| literal_carrier?(member) }
|
|
150
|
+
else false
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# `[mutator, arg_types]` for one site: a call stores through its own name and arguments, an index write
|
|
155
|
+
# through `[]=` with its index arguments ahead of the value. A splat stays the `nil` arity marker the
|
|
156
|
+
# `[]=` splice classifier reads.
|
|
157
|
+
def unknown_store(site)
|
|
158
|
+
arguments = site.arguments&.arguments || NO_ARG_NODES
|
|
159
|
+
unknown = arguments.map { |arg| arg.is_a?(Prism::SplatNode) ? nil : Type::Combinator.untyped }
|
|
160
|
+
return [site.name, unknown] if site.is_a?(Prism::CallNode)
|
|
161
|
+
|
|
162
|
+
[:[]=, unknown + [Type::Combinator.untyped]]
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# An `Array` / `Hash` nominal (alone, as a `Union` member, or as a refinement's base) with a value-pinned type
|
|
166
|
+
# argument. `Type::Combinator.widen_value_pinned` does not look inside type arguments, so each one is asked on
|
|
167
|
+
# its own. A `bool` or a literal union a signature declared (`Array[:a | :b]`) counts as pinned too; the arm it
|
|
168
|
+
# takes can only quiet a report, which is the accepted cost of reading every such binding past one iteration.
|
|
169
|
+
# A class-level argument (`Array[Integer]`) is not pinned: a store of the same class leaves it true, which is
|
|
170
|
+
# the claim this seam may not grow.
|
|
171
|
+
def value_pinned_collection?(type)
|
|
172
|
+
case type
|
|
173
|
+
when Type::Union then type.members.any? { |member| value_pinned_collection?(member) }
|
|
174
|
+
when Type::Difference then value_pinned_collection?(type.base)
|
|
175
|
+
when Type::Nominal
|
|
176
|
+
COLLECTION_CLASSES.include?(type.class_name) &&
|
|
177
|
+
type.type_args.any? { |arg| Type::Combinator.widen_value_pinned(arg) != arg }
|
|
178
|
+
else false
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Every type argument of an `Array` / `Hash` carrier joined with `Dynamic[top]`, member-wise through a
|
|
183
|
+
# `Union` and through a `Difference`'s base. Anything else is returned untouched.
|
|
184
|
+
def gradual_content(type)
|
|
185
|
+
case type
|
|
186
|
+
when Type::Union then Type::Combinator.union(*type.members.map { |member| gradual_content(member) })
|
|
187
|
+
when Type::Difference then Type::Combinator.difference(gradual_content(type.base), type.removed)
|
|
188
|
+
when Type::Nominal
|
|
189
|
+
return type unless COLLECTION_CLASSES.include?(type.class_name) && !type.type_args.empty?
|
|
190
|
+
|
|
191
|
+
Type::Combinator.nominal_of(
|
|
192
|
+
type.class_name,
|
|
193
|
+
type_args: type.type_args.map { |arg| Type::Combinator.union(arg, Type::Combinator.untyped) }
|
|
194
|
+
)
|
|
195
|
+
else type
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../source/node_walker"
|
|
6
|
+
require_relative "captured_locals"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Inference
|
|
10
|
+
# The names of a {CapturedLocals.writes} set that a block body rebinds somewhere `StatementEvaluator` does
|
|
11
|
+
# not carry into the body's exit scope.
|
|
12
|
+
#
|
|
13
|
+
# The per-element fold's issue #587 (b) fixpoint reads each pass's exit binding out of that evaluator, so a
|
|
14
|
+
# rebind the exit never sees leaves the fixpoint on a binding the program has already moved past, and every
|
|
15
|
+
# position of the fold re-reads it. Issue #617's unmoved-pin floor caught such a name only while nothing else
|
|
16
|
+
# moved its binding: `seen = nil; [1, 2].find { |e| seen ||= 0; (seen += 1) == 2 }` threads the `||=`, so the
|
|
17
|
+
# fixpoint left the seed, and every predicate read `0 + 1 == 2`. A name this scan returns takes the floor
|
|
18
|
+
# whatever the fixpoint converged to.
|
|
19
|
+
#
|
|
20
|
+
# The scan admits a write from these positions only, each one the evaluator threads, measured against it
|
|
21
|
+
# rather than read off it:
|
|
22
|
+
#
|
|
23
|
+
# - a statement, and the body of `(…)`, `begin` / `rescue` / `else` / `ensure`;
|
|
24
|
+
# - an assignment's value — every variable-write form, and an index `||=` / `&&=` / `op=`, but not its
|
|
25
|
+
# receiver or index arguments;
|
|
26
|
+
# - an `if` / `unless` / `case` predicate and every branch, but not a `when` condition or an `in` guard;
|
|
27
|
+
# - both operands of `&&` / `||`;
|
|
28
|
+
# - a `while` / `until` predicate and body, a `for` index, collection and body;
|
|
29
|
+
# - a multi-assign value and its local and instance-variable targets, and the LOCAL a `rescue => e`
|
|
30
|
+
# reference, a `for` index, a pattern capture or `=~`'s named captures binds — the evaluator binds no
|
|
31
|
+
# class variable or global through any of them, and no instance variable outside a multi-assign;
|
|
32
|
+
# - a nested block or lambda body, for a local or an instance variable: the call's write-back fixpoint, which
|
|
33
|
+
# joins the block's `next` and `break` paths, or the escaping-block floor covers both, and nothing covers
|
|
34
|
+
# a class variable or global there.
|
|
35
|
+
#
|
|
36
|
+
# Every other position counts as unthreaded: a call's receiver and arguments, an array or hash literal, an
|
|
37
|
+
# interpolation, a `return` / `next` / `break` value, `rescue` modifier, `self.w =` (which rebinds `@w` with
|
|
38
|
+
# no write node at all), a `def` or class body. The list is a whitelist, so a position the evaluator gains
|
|
39
|
+
# later floors a name needlessly rather than letting a pin through. Issue #1223 is such a gain: the evaluator
|
|
40
|
+
# threads a call's receiver and arguments, the literals, the interpolations and a `rescue` modifier
|
|
41
|
+
# ({StatementEvaluator#eval_call}), and the scan does not admit them yet.
|
|
42
|
+
#
|
|
43
|
+
# A threaded rebind can still miss the exit binding when a jump leaves after it. The fold's pass and a
|
|
44
|
+
# nested block's write-back join every block-level `next` (and the write-back every `break`) into the exit
|
|
45
|
+
# ({StatementEvaluator#evaluate_invocation}), but a `while` / `until` / `for` loop's continuation joins its
|
|
46
|
+
# `next` and `break` paths only in part ({StatementEvaluator#loop_iteration}): a `for` body runs one pass,
|
|
47
|
+
# and a name outside the `while` / `until` rebind fixpoint (an instance variable) sees the first pass's
|
|
48
|
+
# `next` exits and no `break` — so the rebind such a branch made can still drop out — and `redo` / `retry`
|
|
49
|
+
# re-enter code with bindings no join sees. A rebind that textually precedes such a jump is therefore
|
|
50
|
+
# unthreaded too; the body runs forward-only, so a rebind after every jump cannot be on a jump's path, and
|
|
51
|
+
# a jump that is its construct's final statement leaves with the exit scope itself. A rebind in an `ensure`
|
|
52
|
+
# runs after any jump in its `begin`, so under any such jump it counts as preceding one.
|
|
53
|
+
module UnthreadedRebinds
|
|
54
|
+
VARIABLE_WRITE_NODES = (CapturedLocals::LOCAL_WRITE_NODES | CapturedLocals::NON_LOCAL_WRITE_NODES).freeze
|
|
55
|
+
private_constant :VARIABLE_WRITE_NODES
|
|
56
|
+
|
|
57
|
+
TARGET_NODES = Set[
|
|
58
|
+
Prism::LocalVariableTargetNode, Prism::InstanceVariableTargetNode,
|
|
59
|
+
Prism::ClassVariableTargetNode, Prism::GlobalVariableTargetNode
|
|
60
|
+
].freeze
|
|
61
|
+
private_constant :TARGET_NODES
|
|
62
|
+
|
|
63
|
+
VALUE_ONLY = { value: :code }.freeze
|
|
64
|
+
private_constant :VALUE_ONLY
|
|
65
|
+
|
|
66
|
+
# Parent class => the child fields the evaluator threads, each with how it is entered: `:code` threads
|
|
67
|
+
# writes, `:binding` threads only the targets the construct binds ({#binding_target_threaded?}),
|
|
68
|
+
# `:ensure` is an `ensure` body, `:loop` a loop body, `:nested` a call's block and `:lambda` a lambda's
|
|
69
|
+
# body.
|
|
70
|
+
THREADED_FIELDS = {
|
|
71
|
+
Prism::StatementsNode => { body: :code },
|
|
72
|
+
Prism::ParenthesesNode => { body: :code },
|
|
73
|
+
Prism::BeginNode => { statements: :code, rescue_clause: :code, else_clause: :code, ensure_clause: :code },
|
|
74
|
+
Prism::RescueNode => { reference: :binding, statements: :code, subsequent: :code },
|
|
75
|
+
Prism::EnsureNode => { statements: :ensure },
|
|
76
|
+
Prism::ElseNode => { statements: :code },
|
|
77
|
+
Prism::IfNode => { predicate: :code, statements: :code, subsequent: :code },
|
|
78
|
+
Prism::UnlessNode => { predicate: :code, statements: :code, else_clause: :code },
|
|
79
|
+
Prism::CaseNode => { predicate: :code, conditions: :code, else_clause: :code },
|
|
80
|
+
Prism::WhenNode => { statements: :code },
|
|
81
|
+
Prism::CaseMatchNode => { predicate: :code, conditions: :code, else_clause: :code },
|
|
82
|
+
Prism::InNode => { pattern: :binding, statements: :code },
|
|
83
|
+
Prism::AndNode => { left: :code, right: :code },
|
|
84
|
+
Prism::OrNode => { left: :code, right: :code },
|
|
85
|
+
Prism::MatchPredicateNode => { value: :code, pattern: :binding },
|
|
86
|
+
Prism::MatchRequiredNode => { value: :code, pattern: :binding },
|
|
87
|
+
Prism::MatchWriteNode => { targets: :binding },
|
|
88
|
+
Prism::MultiWriteNode => { lefts: :binding, rest: :binding, rights: :binding, value: :code },
|
|
89
|
+
Prism::WhileNode => { predicate: :code, statements: :loop },
|
|
90
|
+
Prism::UntilNode => { predicate: :code, statements: :loop },
|
|
91
|
+
Prism::ForNode => { index: :binding, collection: :code, statements: :loop },
|
|
92
|
+
Prism::CallNode => { block: :nested },
|
|
93
|
+
Prism::BlockNode => { body: :code },
|
|
94
|
+
Prism::LambdaNode => { body: :lambda },
|
|
95
|
+
Prism::IndexOrWriteNode => VALUE_ONLY,
|
|
96
|
+
Prism::IndexAndWriteNode => VALUE_ONLY,
|
|
97
|
+
Prism::IndexOperatorWriteNode => VALUE_ONLY,
|
|
98
|
+
**(VARIABLE_WRITE_NODES - TARGET_NODES).to_h { |klass| [klass, VALUE_ONLY] }
|
|
99
|
+
}.transform_values(&:freeze).freeze
|
|
100
|
+
private_constant :THREADED_FIELDS
|
|
101
|
+
|
|
102
|
+
# The jumps whose path the exit binding misses, per construct. A block's `next` and `break` are joined
|
|
103
|
+
# ({StatementEvaluator#evaluate_invocation}, the write-back), and a `break` out of the fold's own call ends
|
|
104
|
+
# it, so no later iteration reads what it carried; `redo` and `retry` are joined nowhere. A loop joins
|
|
105
|
+
# its `next` and `break` paths only in part, so both still count.
|
|
106
|
+
BLOCK_JUMPS = Set[Prism::RedoNode, Prism::RetryNode].freeze
|
|
107
|
+
LOOP_JUMPS = (BLOCK_JUMPS | [Prism::NextNode, Prism::BreakNode]).freeze
|
|
108
|
+
private_constant :BLOCK_JUMPS, :LOOP_JUMPS
|
|
109
|
+
|
|
110
|
+
# The constructs that own the jumps inside them.
|
|
111
|
+
JUMP_BOUNDARY_NODES = Set[
|
|
112
|
+
Prism::BlockNode, Prism::LambdaNode, Prism::DefNode,
|
|
113
|
+
Prism::WhileNode, Prism::UntilNode, Prism::ForNode
|
|
114
|
+
].freeze
|
|
115
|
+
private_constant :JUMP_BOUNDARY_NODES
|
|
116
|
+
|
|
117
|
+
NO_JUMP = -1
|
|
118
|
+
private_constant :NO_JUMP
|
|
119
|
+
|
|
120
|
+
EMPTY = Set.new.freeze
|
|
121
|
+
private_constant :EMPTY
|
|
122
|
+
|
|
123
|
+
# How a walk position reaches the exit scope: whether a local rebind there is threaded, whether an
|
|
124
|
+
# instance variable's is, whether a class variable's or global's is, the offset below which a rebind
|
|
125
|
+
# precedes a jump,
|
|
126
|
+
# whether the position binds targets only (`:multi_assign` under a multi-assign, `:capture` under any
|
|
127
|
+
# other binding construct, false otherwise), and the locals a nested block shadows.
|
|
128
|
+
Position = Data.define(:local, :ivar, :other, :horizon, :binding, :shadowed)
|
|
129
|
+
private_constant :Position
|
|
130
|
+
|
|
131
|
+
module_function
|
|
132
|
+
|
|
133
|
+
# @param names — the rebound names from {CapturedLocals.writes}, sigil and all.
|
|
134
|
+
# @return the subset of `names` the body rebinds somewhere the exit scope misses.
|
|
135
|
+
def names(block_node, names)
|
|
136
|
+
body = block_node.body
|
|
137
|
+
return EMPTY if body.nil? || names.empty?
|
|
138
|
+
|
|
139
|
+
wanted = names.to_set
|
|
140
|
+
found = Set.new
|
|
141
|
+
horizon = last_jump_offset(body, BLOCK_JUMPS)
|
|
142
|
+
start = Position.new(local: true, ivar: true, other: true, horizon: horizon, binding: false, shadowed: EMPTY)
|
|
143
|
+
visit(body, start, wanted, found)
|
|
144
|
+
found
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def visit(node, position, wanted, found)
|
|
148
|
+
return unless node.is_a?(Prism::Node)
|
|
149
|
+
return if node.is_a?(Prism::DefinedNode)
|
|
150
|
+
|
|
151
|
+
check(node, position, wanted, found)
|
|
152
|
+
return node.rigor_each_child { |child| visit(child, position, wanted, found) } if position.binding
|
|
153
|
+
|
|
154
|
+
fields = THREADED_FIELDS[node.class]
|
|
155
|
+
return node.rigor_each_child { |child| visit_unthreaded(child, wanted, found) } if fields.nil?
|
|
156
|
+
|
|
157
|
+
threaded = threaded_children(node, fields)
|
|
158
|
+
node.rigor_each_child do |child|
|
|
159
|
+
mode = threaded[child]
|
|
160
|
+
if mode
|
|
161
|
+
visit(child, enter(node, child, mode, position), wanted, found)
|
|
162
|
+
else
|
|
163
|
+
visit_unthreaded(child, wanted, found)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def threaded_children(node, fields)
|
|
169
|
+
threaded = {}.compare_by_identity
|
|
170
|
+
fields.each do |field, mode|
|
|
171
|
+
value = node.public_send(field)
|
|
172
|
+
if value.is_a?(Array)
|
|
173
|
+
value.each { |child| threaded[child] = mode }
|
|
174
|
+
elsif value
|
|
175
|
+
threaded[value] = mode
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
threaded
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Every rebind under an unthreaded position misses the exit scope, whatever the position beneath it.
|
|
182
|
+
def visit_unthreaded(node, wanted, found)
|
|
183
|
+
Source::NodeWalker.each(node) do |descendant|
|
|
184
|
+
name = rebound_name(descendant)
|
|
185
|
+
found << name if name && wanted.include?(name)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def enter(parent, child, mode, position)
|
|
190
|
+
case mode
|
|
191
|
+
when :binding then position.with(binding: parent.is_a?(Prism::MultiWriteNode) ? :multi_assign : :capture)
|
|
192
|
+
when :ensure then position.with(horizon: position.horizon == NO_JUMP ? NO_JUMP : Float::INFINITY)
|
|
193
|
+
when :loop then extend_horizon(position, child, LOOP_JUMPS)
|
|
194
|
+
when :nested then nested_block(child, position)
|
|
195
|
+
when :lambda then nested_body(parent, child, position)
|
|
196
|
+
else position
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def extend_horizon(position, body, kinds)
|
|
201
|
+
position.with(horizon: [position.horizon, last_jump_offset(body, kinds)].max)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# A call's block. A block-pass argument (`&blk`) carries no body the call's write-back reads.
|
|
205
|
+
def nested_block(child, position)
|
|
206
|
+
return position.with(local: false, ivar: false, other: false) unless child.is_a?(Prism::BlockNode)
|
|
207
|
+
|
|
208
|
+
nested_body(child, child.body, position)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# A block or lambda body: its parameters and block-locals shadow, its own jumps join the horizon, and a
|
|
212
|
+
# class variable's or global's rebind is no longer carried out.
|
|
213
|
+
def nested_body(owner, body, position)
|
|
214
|
+
extend_horizon(position, body, BLOCK_JUMPS).with(
|
|
215
|
+
other: false, shadowed: position.shadowed | CapturedLocals.introduced_locals(owner)
|
|
216
|
+
)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def check(node, position, wanted, found)
|
|
220
|
+
name = rebound_name(node)
|
|
221
|
+
return if name.nil? || !wanted.include?(name)
|
|
222
|
+
return if position.shadowed.include?(name)
|
|
223
|
+
|
|
224
|
+
found << name unless threaded_rebind?(node, name, position)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def threaded_rebind?(node, name, position)
|
|
228
|
+
return false unless VARIABLE_WRITE_NODES.include?(node.class)
|
|
229
|
+
return false unless TARGET_NODES.include?(node.class) == (position.binding ? true : false)
|
|
230
|
+
|
|
231
|
+
kind = CapturedLocals.variable_kind(name)
|
|
232
|
+
return false if position.binding && !binding_target_threaded?(kind, position.binding)
|
|
233
|
+
return false unless kind_threaded?(kind, position)
|
|
234
|
+
|
|
235
|
+
node.location.start_offset >= position.horizon
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def kind_threaded?(kind, position)
|
|
239
|
+
case kind
|
|
240
|
+
when :local then position.local
|
|
241
|
+
when :ivar then position.ivar
|
|
242
|
+
else position.other
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# A binding construct's target reaches the exit scope only for the kinds the evaluator binds there: a
|
|
247
|
+
# multi-assign binds locals and instance variables, and a `rescue =>` reference, a `for` index, a pattern
|
|
248
|
+
# capture or a named capture binds locals only. `a, $g = 1, :z` leaves `$g` where it was.
|
|
249
|
+
def binding_target_threaded?(kind, binding)
|
|
250
|
+
kind == :local || (kind == :ivar && binding == :multi_assign)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def rebound_name(node)
|
|
254
|
+
return node.name if VARIABLE_WRITE_NODES.include?(node.class)
|
|
255
|
+
|
|
256
|
+
CapturedLocals.setter_ivar_name(node)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# The start offset of the last `kinds` jump that belongs to the construct whose body is `body`, or
|
|
260
|
+
# {NO_JUMP}. A jump that is the body's final statement is skipped: it leaves with the exit scope itself.
|
|
261
|
+
def last_jump_offset(body, kinds)
|
|
262
|
+
return NO_JUMP if body.nil?
|
|
263
|
+
|
|
264
|
+
final = body.body.last if body.is_a?(Prism::StatementsNode)
|
|
265
|
+
offset = NO_JUMP
|
|
266
|
+
each_owned_jump(body, kinds) do |jump|
|
|
267
|
+
offset = [offset, jump.location.start_offset].max unless jump.equal?(final)
|
|
268
|
+
end
|
|
269
|
+
offset
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def each_owned_jump(node, kinds, &)
|
|
273
|
+
yield node if kinds.include?(node.class)
|
|
274
|
+
return if node.is_a?(Prism::DefinedNode)
|
|
275
|
+
|
|
276
|
+
node.rigor_each_child do |child|
|
|
277
|
+
each_owned_jump(child, kinds, &) unless JUMP_BOUNDARY_NODES.include?(child.class)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
@@ -268,8 +268,9 @@ module Rigor
|
|
|
268
268
|
|
|
269
269
|
# Runs `Analysis::Runner` with a `BufferBinding` so the buffer bytes (instead of the on-disk file) drive
|
|
270
270
|
# the parse. The `Rigor::Analysis::ProjectScan` cached on the ProjectContext is passed through
|
|
271
|
-
# `prebuilt:` so plugin `#prepare`, the dependency-source walker,
|
|
272
|
-
# project-patched scanners do not re-run per publish
|
|
271
|
+
# `prebuilt:` so plugin `#prepare`, the dependency-source walker, the synthetic-method /
|
|
272
|
+
# project-patched scanners and (#1038) the template-unit transforms do not re-run per publish — the
|
|
273
|
+
# last of those is an Erubi compile of every view for an ERB project. The snapshot rebuilds only when
|
|
273
274
|
# `ProjectContext#invalidate!` fires (watched-file or configuration change). Returns the LSP-shaped
|
|
274
275
|
# Diagnostic Array, ready to serialize into the notification's `params.diagnostics` field.
|
|
275
276
|
def run_analysis(path:, bytes:)
|
|
@@ -83,9 +83,9 @@ module Rigor
|
|
|
83
83
|
)
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
-
# @return ,
|
|
87
|
-
#
|
|
88
|
-
#
|
|
86
|
+
# @return `[definition, receiver_class_name, kind]` — the resolved method definition (nil when the
|
|
87
|
+
# method doesn't resolve through the RBS env), the receiver class name, and the dispatch kind
|
|
88
|
+
# (`:instance` or `:singleton`); `[nil, nil, nil]` when the receiver shape isn't yet supported.
|
|
89
89
|
def lookup_method(receiver_type, method_name, scope)
|
|
90
90
|
case receiver_type
|
|
91
91
|
when Type::Singleton
|