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
|
@@ -41,10 +41,11 @@ module Rigor
|
|
|
41
41
|
def scan(paths, buffer: nil)
|
|
42
42
|
entries = []
|
|
43
43
|
diagnostics = []
|
|
44
|
-
|
|
44
|
+
census = Set.new
|
|
45
|
+
paths.each { |path| scan_file(path, entries, diagnostics, buffer, census) }
|
|
45
46
|
diagnostics.concat(duplicate_declaration_diagnostics(entries))
|
|
46
47
|
Result.new(
|
|
47
|
-
registry: ProjectPatchedMethods.new(entries: entries),
|
|
48
|
+
registry: ProjectPatchedMethods.new(entries: entries, write_census: census.freeze),
|
|
48
49
|
diagnostics: diagnostics
|
|
49
50
|
)
|
|
50
51
|
end
|
|
@@ -77,7 +78,7 @@ module Rigor
|
|
|
77
78
|
end
|
|
78
79
|
private_class_method :duplicate_declaration_diagnostics
|
|
79
80
|
|
|
80
|
-
def scan_file(path, entries, diagnostics, buffer = nil)
|
|
81
|
+
def scan_file(path, entries, diagnostics, buffer = nil, census = Set.new)
|
|
81
82
|
physical = buffer ? buffer.resolve(path) : path
|
|
82
83
|
parse_result =
|
|
83
84
|
if physical == path
|
|
@@ -91,6 +92,9 @@ module Rigor
|
|
|
91
92
|
end
|
|
92
93
|
|
|
93
94
|
walk_node(parse_result.value, [], false, path, entries)
|
|
95
|
+
# Issue #1367 — the file's `global.*` write facts: a patch file may alias a special or give a class the
|
|
96
|
+
# method a setter asks for, as any project file may.
|
|
97
|
+
census.merge(GlobalWriteCensus.scan(parse_result.value))
|
|
94
98
|
rescue StandardError => e
|
|
95
99
|
diagnostics << build_diagnostic(
|
|
96
100
|
path: path, line: 1, column: 1,
|
|
@@ -24,10 +24,40 @@ module Rigor
|
|
|
24
24
|
# object no binding can be attributed to and yields `[]`, which is what every receiver form
|
|
25
25
|
# outside the single-read case already contributed. The walk is depth-capped so a pathological
|
|
26
26
|
# nest cannot make receiver classification unbounded.
|
|
27
|
+
#
|
|
28
|
+
# The `it` parameter (Ruby 3.4) is a local like any other, but Prism reads it through
|
|
29
|
+
# `Prism::ItLocalVariableReadNode`, which carries no `name`; {.read_name} answers `:it` for it, the
|
|
30
|
+
# binding `BlockParameterBinder` installs. A consumer names a read through {.read_name}, never `#name`.
|
|
31
|
+
#
|
|
32
|
+
# A local or instance-variable write evaluates to the variable it writes, so `(buf ||= []) << x`
|
|
33
|
+
# mutates `buf` and yields a read of it ({.read_of}). Issue #1223 made that binding visible: the
|
|
34
|
+
# statement evaluator threads a write in a receiver, and without the alias `buf` kept the `[]`
|
|
35
|
+
# the `||=` stored.
|
|
27
36
|
module ReceiverAlias
|
|
28
37
|
# Deep enough for any hand-written selection; a nest beyond it degrades to "names no binding".
|
|
29
38
|
WALK_DEPTH_CAP = 6
|
|
30
39
|
|
|
40
|
+
NON_ALIASED_READS = [Prism::ClassVariableReadNode, Prism::GlobalVariableReadNode].freeze
|
|
41
|
+
private_constant :NON_ALIASED_READS
|
|
42
|
+
|
|
43
|
+
LOCAL_WRITE_NODES = Set[
|
|
44
|
+
Prism::LocalVariableWriteNode, Prism::LocalVariableOrWriteNode,
|
|
45
|
+
Prism::LocalVariableAndWriteNode, Prism::LocalVariableOperatorWriteNode
|
|
46
|
+
].freeze
|
|
47
|
+
INSTANCE_WRITE_NODES = Set[
|
|
48
|
+
Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode,
|
|
49
|
+
Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableOperatorWriteNode
|
|
50
|
+
].freeze
|
|
51
|
+
CLASS_VARIABLE_WRITE_NODES = Set[
|
|
52
|
+
Prism::ClassVariableWriteNode, Prism::ClassVariableOrWriteNode,
|
|
53
|
+
Prism::ClassVariableAndWriteNode, Prism::ClassVariableOperatorWriteNode
|
|
54
|
+
].freeze
|
|
55
|
+
GLOBAL_WRITE_NODES = Set[
|
|
56
|
+
Prism::GlobalVariableWriteNode, Prism::GlobalVariableOrWriteNode,
|
|
57
|
+
Prism::GlobalVariableAndWriteNode, Prism::GlobalVariableOperatorWriteNode
|
|
58
|
+
].freeze
|
|
59
|
+
private_constant :LOCAL_WRITE_NODES, :INSTANCE_WRITE_NODES, :CLASS_VARIABLE_WRITE_NODES, :GLOBAL_WRITE_NODES
|
|
60
|
+
|
|
31
61
|
module_function
|
|
32
62
|
|
|
33
63
|
# @param node — the receiver expression.
|
|
@@ -36,9 +66,11 @@ module Rigor
|
|
|
36
66
|
# read the expression can evaluate to; empty when it can evaluate to none.
|
|
37
67
|
def candidates(node, depth = 0)
|
|
38
68
|
return [] if node.nil? || depth > WALK_DEPTH_CAP
|
|
69
|
+
# Tested before the `case`: a `when *SET` arm would copy the set into an Array on every call.
|
|
70
|
+
return [read_of(node)] if variable_write?(node)
|
|
39
71
|
|
|
40
72
|
case node
|
|
41
|
-
when Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode then [node]
|
|
73
|
+
when Prism::LocalVariableReadNode, Prism::ItLocalVariableReadNode, Prism::InstanceVariableReadNode then [node]
|
|
42
74
|
when Prism::ParenthesesNode then candidates(node.body, depth + 1)
|
|
43
75
|
when Prism::StatementsNode then candidates(node.body.last, depth + 1)
|
|
44
76
|
when Prism::ElseNode then candidates(node.statements, depth + 1)
|
|
@@ -52,6 +84,63 @@ module Rigor
|
|
|
52
84
|
def branches(first, second, depth)
|
|
53
85
|
candidates(first, depth + 1) + candidates(second, depth + 1)
|
|
54
86
|
end
|
|
87
|
+
|
|
88
|
+
# The variable reads an in-place mutation of `receiver` changes: {.candidates}' locals and instance
|
|
89
|
+
# variables, or the class variable or global the receiver reads or writes directly, parenthesised or not
|
|
90
|
+
# (`(@@c ||= []) << x` mutates `@@c`, as `(buf ||= []) << x` mutates `buf`). A class variable or global counts
|
|
91
|
+
# only as the receiver itself, not through a branch that selects it. This is the
|
|
92
|
+
# one answer the straight-line widening ({MutationWidening.widen_receiver_aliases}), the block-return
|
|
93
|
+
# threading gate that predicts it (`ExpressionTyper#prefix_statement_jump_free?`) and the per-element fold's
|
|
94
|
+
# content-mutation scan ({CapturedLocals.content_mutations}) all read, so "the scan says the body changed
|
|
95
|
+
# it" and "evaluating the body changes it" cannot disagree about which kinds of variable count.
|
|
96
|
+
def mutated_reads(receiver)
|
|
97
|
+
direct = receiver
|
|
98
|
+
direct = direct.body.body.last while direct.is_a?(Prism::ParenthesesNode) && direct.body.is_a?(Prism::StatementsNode)
|
|
99
|
+
return [direct] if NON_ALIASED_READS.include?(direct.class)
|
|
100
|
+
return [read_of(direct)] if non_aliased_write?(direct)
|
|
101
|
+
|
|
102
|
+
candidates(receiver)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# True when `node` is a class-variable or global write, which {.mutated_reads} answers only as the receiver
|
|
106
|
+
# itself, as it answers a class-variable or global read.
|
|
107
|
+
def non_aliased_write?(node)
|
|
108
|
+
CLASS_VARIABLE_WRITE_NODES.include?(node.class) || GLOBAL_WRITE_NODES.include?(node.class)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# The binding a read from {.candidates} or {.mutated_reads} names.
|
|
112
|
+
def read_name(read) = read.is_a?(Prism::ItLocalVariableReadNode) ? :it : read.name
|
|
113
|
+
|
|
114
|
+
# True when `read` is a local the innermost block around it owns: one Prism resolves at `depth == 0`, or an
|
|
115
|
+
# `it` read, which is always that block's own parameter and never a capture of an enclosing scope.
|
|
116
|
+
def block_local?(read)
|
|
117
|
+
read.is_a?(Prism::ItLocalVariableReadNode) || (read.is_a?(Prism::LocalVariableReadNode) && read.depth.zero?)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# True when `node` is a local or instance-variable write, whose value is the variable it leaves.
|
|
121
|
+
def variable_write?(node)
|
|
122
|
+
LOCAL_WRITE_NODES.include?(node.class) || INSTANCE_WRITE_NODES.include?(node.class)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# A read of the variable `write` writes, at the write's name, for the analysers that key a receiver on a
|
|
126
|
+
# read node — a local, instance-variable, class-variable or global write. `Prism` 1.x constructs every node
|
|
127
|
+
# from `source, node_id, location, flags` and its fields.
|
|
128
|
+
def read_of(write)
|
|
129
|
+
source = write.send(:source)
|
|
130
|
+
if LOCAL_WRITE_NODES.include?(write.class)
|
|
131
|
+
Prism::LocalVariableReadNode.new(source, write.node_id, write.name_loc, 0, write.name, write.depth)
|
|
132
|
+
else
|
|
133
|
+
named_read_class(write.class).new(source, write.node_id, write.name_loc, 0, write.name)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# The read node class for an instance-variable, class-variable or global write class.
|
|
138
|
+
def named_read_class(write_class)
|
|
139
|
+
if INSTANCE_WRITE_NODES.include?(write_class) then Prism::InstanceVariableReadNode
|
|
140
|
+
elsif CLASS_VARIABLE_WRITE_NODES.include?(write_class) then Prism::ClassVariableReadNode
|
|
141
|
+
else Prism::GlobalVariableReadNode
|
|
142
|
+
end
|
|
143
|
+
end
|
|
55
144
|
end
|
|
56
145
|
end
|
|
57
146
|
end
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "block_parameter_binder"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
# Can a block see the hash that an in-place transform is rewriting? This is the gate for the in-place forms
|
|
10
|
+
# of the HashShape per-pair fold (`ExpressionTyper#try_hash_shape_block_fold`).
|
|
11
|
+
#
|
|
12
|
+
# `transform_values!` / `transform_keys!` rewrite their receiver pair by pair while they iterate, so a block
|
|
13
|
+
# that reads the receiver sees the pairs it has already rewritten. The fold types every pair against the
|
|
14
|
+
# pre-call state held by the entry scope, so it gives a precise wrong answer: `h = { x: 1, y: 2 };
|
|
15
|
+
# h.transform_values! { |e| h[:x] + e }` folded to `{ x: 2, y: 3 }`, where Ruby answers `{ x: 2, y: 4 }`.
|
|
16
|
+
#
|
|
17
|
+
# The engine records no aliasing between bindings, and a method can return the receiver under any name.
|
|
18
|
+
# The gate therefore does not look for the receiver. It admits a block only when everything the block
|
|
19
|
+
# reads from outside itself is incapable of describing a hash's contents:
|
|
20
|
+
#
|
|
21
|
+
# - A local declared inside the block tree — a parameter or a block-local — is admitted. Prism's `depth`
|
|
22
|
+
# decides this, so a nested parameter that shadows an outer local is still a parameter.
|
|
23
|
+
# - A captured local, instance, class or global variable, or a constant, is admitted when its entry type
|
|
24
|
+
# carries no hash contents ({#carries_hash_contents?}). A compound write reads first, so it counts too.
|
|
25
|
+
# A constant compound write (`F ||= …`) names no read node to type, and a constant path under a
|
|
26
|
+
# non-constant parent (`m::T`) cannot be typed from the entry scope, so neither is admitted.
|
|
27
|
+
# - A method call is admitted only when its receiver is built from block parameters and literals alone
|
|
28
|
+
# (`e.to_s.upcase`, `[e].map { … }`, `%i[a b].include?(k)`). This excludes an implicit-self call and a
|
|
29
|
+
# call on `self`, a constant, a captured variable or a block-local, because the method can return the
|
|
30
|
+
# receiver however the value it is called on is typed (`Registry.store[:x]`, `tbl[:x]` for
|
|
31
|
+
# `def tbl = TABLE`).
|
|
32
|
+
# - `super`, `yield`, and a nested `def` / `class` / `module` / `class << x` run code the walk cannot
|
|
33
|
+
# see, so they are never admitted.
|
|
34
|
+
# - The operand of `defined?` is not walked: it asks whether a name is defined and reads no value.
|
|
35
|
+
#
|
|
36
|
+
# What a method called on a block parameter reaches is outside the gate. Such a method can read global
|
|
37
|
+
# state, but its inferred return type can hold stale constant contents without any in-place transform,
|
|
38
|
+
# so this is the engine-wide method-effect gap rather than this fold's. The gate only costs precision on
|
|
39
|
+
# the value of the bang call itself, because the receiver's own binding is widened after any bang call.
|
|
40
|
+
module ReceiverBlindBlock
|
|
41
|
+
LOCAL_READS = [
|
|
42
|
+
Prism::LocalVariableReadNode,
|
|
43
|
+
Prism::LocalVariableOperatorWriteNode,
|
|
44
|
+
Prism::LocalVariableOrWriteNode,
|
|
45
|
+
Prism::LocalVariableAndWriteNode
|
|
46
|
+
].freeze
|
|
47
|
+
|
|
48
|
+
# Each captured non-local read, and the `Scope` accessor holding its entry binding.
|
|
49
|
+
VARIABLE_ACCESSORS = Ractor.make_shareable({
|
|
50
|
+
Prism::InstanceVariableReadNode => :ivar,
|
|
51
|
+
Prism::InstanceVariableOperatorWriteNode => :ivar,
|
|
52
|
+
Prism::InstanceVariableOrWriteNode => :ivar,
|
|
53
|
+
Prism::InstanceVariableAndWriteNode => :ivar,
|
|
54
|
+
Prism::ClassVariableReadNode => :cvar,
|
|
55
|
+
Prism::ClassVariableOperatorWriteNode => :cvar,
|
|
56
|
+
Prism::ClassVariableOrWriteNode => :cvar,
|
|
57
|
+
Prism::ClassVariableAndWriteNode => :cvar,
|
|
58
|
+
Prism::GlobalVariableReadNode => :global,
|
|
59
|
+
Prism::GlobalVariableOperatorWriteNode => :global,
|
|
60
|
+
Prism::GlobalVariableOrWriteNode => :global,
|
|
61
|
+
Prism::GlobalVariableAndWriteNode => :global
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
CONSTANT_READS = [Prism::ConstantReadNode, Prism::ConstantPathNode].freeze
|
|
65
|
+
|
|
66
|
+
# A constant compound write reads the constant by name, with no read node for the walk to type.
|
|
67
|
+
CONSTANT_COMPOUND_WRITES = [
|
|
68
|
+
Prism::ConstantOperatorWriteNode,
|
|
69
|
+
Prism::ConstantOrWriteNode,
|
|
70
|
+
Prism::ConstantAndWriteNode
|
|
71
|
+
].freeze
|
|
72
|
+
|
|
73
|
+
# Every node that calls a method on a receiver.
|
|
74
|
+
RECEIVER_CALLS = [
|
|
75
|
+
Prism::CallNode,
|
|
76
|
+
Prism::CallAndWriteNode,
|
|
77
|
+
Prism::CallOperatorWriteNode,
|
|
78
|
+
Prism::CallOrWriteNode,
|
|
79
|
+
Prism::CallTargetNode,
|
|
80
|
+
Prism::IndexAndWriteNode,
|
|
81
|
+
Prism::IndexOperatorWriteNode,
|
|
82
|
+
Prism::IndexOrWriteNode,
|
|
83
|
+
Prism::IndexTargetNode
|
|
84
|
+
].freeze
|
|
85
|
+
|
|
86
|
+
# Nodes that run code the walk cannot see, or open a scope the block's locals are invisible from.
|
|
87
|
+
OPAQUE = [
|
|
88
|
+
Prism::SuperNode,
|
|
89
|
+
Prism::ForwardingSuperNode,
|
|
90
|
+
Prism::YieldNode,
|
|
91
|
+
Prism::DefNode,
|
|
92
|
+
Prism::ClassNode,
|
|
93
|
+
Prism::ModuleNode,
|
|
94
|
+
Prism::SingletonClassNode
|
|
95
|
+
].freeze
|
|
96
|
+
|
|
97
|
+
# Literal nodes, whose value is built from their children alone.
|
|
98
|
+
LITERALS = [
|
|
99
|
+
Prism::IntegerNode, Prism::FloatNode, Prism::RationalNode, Prism::ImaginaryNode,
|
|
100
|
+
Prism::StringNode, Prism::SymbolNode, Prism::RegularExpressionNode,
|
|
101
|
+
Prism::InterpolatedStringNode, Prism::InterpolatedSymbolNode, Prism::EmbeddedStatementsNode,
|
|
102
|
+
Prism::NilNode, Prism::TrueNode, Prism::FalseNode,
|
|
103
|
+
Prism::ArrayNode, Prism::HashNode, Prism::AssocNode, Prism::RangeNode
|
|
104
|
+
].freeze
|
|
105
|
+
|
|
106
|
+
# A `Type` carrier is an immutable value built bottom-up, so it cannot contain itself. The cap is
|
|
107
|
+
# defensive only, and exceeding it answers "carries": declining the fold is the safe side.
|
|
108
|
+
TYPE_DEPTH_CAP = 8
|
|
109
|
+
|
|
110
|
+
module_function
|
|
111
|
+
|
|
112
|
+
# @param base_scope — the call-site scope the block closes over.
|
|
113
|
+
def blind?(block_node, base_scope)
|
|
114
|
+
scope_blind?(block_node, [], base_scope)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# `levels` holds the parameter names of each block from the transform's own block (index 0) down to
|
|
118
|
+
# the innermost block around the node, so a local read's Prism `depth` names the block that declares
|
|
119
|
+
# it, or none when the read is captured from outside.
|
|
120
|
+
def scope_blind?(block_node, levels, base_scope)
|
|
121
|
+
levels += [Set.new(BlockParameterBinder.new.bind(block_node).keys)]
|
|
122
|
+
[block_node.parameters, block_node.body].all? { |child| node_blind?(child, levels, base_scope) }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def node_blind?(node, levels, base_scope)
|
|
126
|
+
case node
|
|
127
|
+
when nil, Prism::DefinedNode then true
|
|
128
|
+
when Prism::BlockNode, Prism::LambdaNode then scope_blind?(node, levels, base_scope)
|
|
129
|
+
when *OPAQUE then false
|
|
130
|
+
else
|
|
131
|
+
admitted?(node, levels, base_scope) &&
|
|
132
|
+
node.compact_child_nodes.all? { |child| node_blind?(child, levels, base_scope) }
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def admitted?(node, levels, base_scope)
|
|
137
|
+
case node
|
|
138
|
+
when *LOCAL_READS then in_block_tree?(node, levels) || !carries_hash_contents?(base_scope.local(node.name))
|
|
139
|
+
when *VARIABLE_ACCESSORS.keys
|
|
140
|
+
!carries_hash_contents?(base_scope.public_send(VARIABLE_ACCESSORS[node.class], node.name))
|
|
141
|
+
when *CONSTANT_READS then !constant_carries?(node, base_scope)
|
|
142
|
+
when *CONSTANT_COMPOUND_WRITES then false
|
|
143
|
+
when *RECEIVER_CALLS then built_from_parameters?(node.receiver, levels)
|
|
144
|
+
else true
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def in_block_tree?(read, levels) = !declaring_level(read, levels).nil?
|
|
149
|
+
|
|
150
|
+
# The index into `levels` of the block that declares a local read, or `nil` for a captured read.
|
|
151
|
+
def declaring_level(read, levels)
|
|
152
|
+
level = levels.size - 1 - read.depth
|
|
153
|
+
level unless level.negative?
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Whether a call receiver's value comes from block parameters and literals alone. A call inside it is
|
|
157
|
+
# followed to its own receiver; its arguments are admitted by the walk on their own.
|
|
158
|
+
def built_from_parameters?(receiver, levels)
|
|
159
|
+
case receiver
|
|
160
|
+
when Prism::LocalVariableReadNode
|
|
161
|
+
level = declaring_level(receiver, levels)
|
|
162
|
+
!level.nil? && levels[level].include?(receiver.name)
|
|
163
|
+
when Prism::ItLocalVariableReadNode then true
|
|
164
|
+
when Prism::ParenthesesNode then built_from_parameters?(receiver.body, levels)
|
|
165
|
+
when Prism::StatementsNode then built_from_parameters?(receiver.body.last, levels)
|
|
166
|
+
when *RECEIVER_CALLS then built_from_parameters?(receiver.receiver, levels)
|
|
167
|
+
when *LITERALS then receiver.compact_child_nodes.all? { |child| built_from_parameters?(child, levels) }
|
|
168
|
+
else false
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def constant_carries?(node, base_scope)
|
|
173
|
+
return true if node.is_a?(Prism::ConstantPathNode) && !constant_parent?(node.parent)
|
|
174
|
+
|
|
175
|
+
carries_hash_contents?(base_scope.type_of(node))
|
|
176
|
+
rescue StandardError
|
|
177
|
+
true
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# `::T` has no parent; `A::T` and `A::B::T` have constant parents.
|
|
181
|
+
def constant_parent?(parent)
|
|
182
|
+
parent.nil? || CONSTANT_READS.include?(parent.class)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Whether a value of `type` can tell a reader what some hash holds, key by key or as `Hash[K, V]`
|
|
186
|
+
# parameters. A read through such a value in an in-place transform's block can be stale by the next
|
|
187
|
+
# pair. `Dynamic[top]`, a bare `Hash` and a scalar cannot, and neither can a `Hash` whose parameters are
|
|
188
|
+
# untyped.
|
|
189
|
+
def carries_hash_contents?(type, depth = 0)
|
|
190
|
+
return false if type.nil?
|
|
191
|
+
return true if depth > TYPE_DEPTH_CAP || type.is_a?(Type::HashShape) || typed_hash_nominal?(type)
|
|
192
|
+
|
|
193
|
+
component_types(type).any? { |inner| carries_hash_contents?(inner, depth + 1) }
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def typed_hash_nominal?(type)
|
|
197
|
+
type.is_a?(Type::Nominal) && type.class_name == "Hash" &&
|
|
198
|
+
type.type_args.any? { |arg| !arg.is_a?(Type::Dynamic) && !arg.is_a?(Type::Top) }
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# The types a carrier holds. The list is empty for a carrier that cannot hold another value's type.
|
|
202
|
+
def component_types(type)
|
|
203
|
+
case type
|
|
204
|
+
when Type::Union, Type::Intersection then type.members
|
|
205
|
+
when Type::Tuple then type.elements
|
|
206
|
+
when Type::Nominal then type.type_args
|
|
207
|
+
when Type::Refined, Type::Difference then [type.base]
|
|
208
|
+
when Type::StructInstance, Type::DataInstance then type.members.values
|
|
209
|
+
when Type::Dynamic then [type.static_facet]
|
|
210
|
+
when Type::BoundMethod then [type.receiver_type]
|
|
211
|
+
when Type::Maybe then [type.value_type]
|
|
212
|
+
when Type::Result then [type.ok_type, type.err_type]
|
|
213
|
+
when Type::App then [*type.args, type.bound]
|
|
214
|
+
else []
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
@@ -20,9 +20,11 @@ module Rigor
|
|
|
20
20
|
module RefinementMutation
|
|
21
21
|
# Mutators that CANNOT leave a non-empty receiver empty. Whitelists rather than a table of
|
|
22
22
|
# emptiers: a name missing from these retracts the witness, which is the conservative answer
|
|
23
|
-
# every mutator got before. The listed ones either only add (`<< push …`) or only
|
|
24
|
-
# fixed number of slots (`sort! map! …`); `replace`, `compact!`, `flatten!`, `uniq!`
|
|
25
|
-
# remover are deliberately absent, since each has an input that empties the receiver.
|
|
23
|
+
# every mutator got before. The listed ones either only add (`<< push …`) or only reorder or
|
|
24
|
+
# rewrite a fixed number of slots (`sort! map! …`); `replace`, `compact!`, `flatten!`, `uniq!`
|
|
25
|
+
# and every remover are deliberately absent, since each has an input that empties the receiver.
|
|
26
|
+
# Keeping the witness is all this table decides: what a rewrite leaves in those slots is
|
|
27
|
+
# {RewriteMutation}'s answer, so `map!` keeps `non-empty-array` but not its element.
|
|
26
28
|
EMPTY_PRESERVING = {
|
|
27
29
|
"Array" => %i[
|
|
28
30
|
<< push append prepend unshift concat insert
|
|
@@ -40,23 +42,25 @@ module Rigor
|
|
|
40
42
|
# The block receives the refinement's BASE and answers the base with the mutator's added
|
|
41
43
|
# content joined in, or a falsey value to decline (the mutator is not in that base's table).
|
|
42
44
|
# A String base is answered here instead: it carries no content parameter, so the only
|
|
43
|
-
# question it has is the witness one
|
|
44
|
-
def widen(difference, method_name)
|
|
45
|
+
# question it has is the witness one, which for `tr!` / `tr_s!` turns on `arg_types`.
|
|
46
|
+
def widen(difference, method_name, arg_types = StringMutation::NO_ARG_TYPES)
|
|
45
47
|
return nil unless difference.removes_empty_witness?
|
|
46
48
|
|
|
47
49
|
base = difference.base
|
|
48
|
-
joined = base.class_name == "String" ? string_base(base, method_name) : yield(base)
|
|
50
|
+
joined = base.class_name == "String" ? string_base(base, method_name, arg_types) : yield(base)
|
|
49
51
|
return nil unless joined
|
|
50
52
|
|
|
51
53
|
widened = preserves_witness?(base.class_name, method_name) ? refine(joined, difference) : joined
|
|
52
54
|
widened == difference ? nil : widened
|
|
53
55
|
end
|
|
54
56
|
|
|
55
|
-
# A `non-empty-string` survives every mutator that cannot empty the buffer
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
def string_base(base, method_name)
|
|
59
|
-
StringMutation::
|
|
57
|
+
# A `non-empty-string` survives every mutator call that cannot empty the buffer
|
|
58
|
+
# ({StringMutation.may_empty?}), so this arm declines those and the refinement stands, rather
|
|
59
|
+
# than widening to `String` and dropping it. A name that is not a String mutator declines too.
|
|
60
|
+
def string_base(base, method_name, arg_types)
|
|
61
|
+
return nil unless StringMutation::MUTATORS.include?(method_name)
|
|
62
|
+
|
|
63
|
+
StringMutation.may_empty?(method_name, arg_types) ? base : nil
|
|
60
64
|
end
|
|
61
65
|
|
|
62
66
|
def preserves_witness?(class_name, method_name)
|