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,267 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../analysis/dependency_recorder"
|
|
4
|
+
require_relative "../reflection"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
# Issue #527 slice 0 — the single owner of one question: which RBS declaration answers
|
|
9
|
+
# `method_name` on `class_name` through an ancestor the PROJECT does not declare?
|
|
10
|
+
#
|
|
11
|
+
# A project class is usually absent from the RBS environment, so the declaration that decides the
|
|
12
|
+
# question is written about an ancestor the project does not declare — the `< StandardError`
|
|
13
|
+
# superclass, the `include Comparable` mixin, `< ::StringScanner`. `Scope#external_ancestor_name_candidates`
|
|
14
|
+
# gathers those as-written names breadth-first over the project ancestry; this module resolves each
|
|
15
|
+
# group against the RBS environment and reports WHICH declaration answered, not merely that one did.
|
|
16
|
+
#
|
|
17
|
+
# The logic existed twice before: `ExpressionTyper#rbs_ancestor_answers?` asked it as a boolean (the
|
|
18
|
+
# implicit-self binding veto of issue #633 / ADR-110), and the dispatch side of #527 needs the same
|
|
19
|
+
# walk but wants the definition and its owner so it can dispatch there. Two copies of an MRO cut-off
|
|
20
|
+
# rule is one copy too many, so the boolean is now `!resolve(...).nil?`.
|
|
21
|
+
#
|
|
22
|
+
# The one thing the consumers do NOT share is the ADR-46 cross-file edge, and that is
|
|
23
|
+
# `record_dependencies:`. The implicit-self veto genuinely READ the ancestor's declaration sites to
|
|
24
|
+
# decide a binding, so its walk records them. A dispatch lookup asking "does some ancestor happen to
|
|
25
|
+
# declare this?" did not, and filing an edge for it would mislabel the lookup as an ancestry edge —
|
|
26
|
+
# which is why `RbsDispatch.each_source_ancestor_candidate` read the raw discovery tables instead of
|
|
27
|
+
# this walk. The suppression is #992's {Analysis::DependencyRecorder.withhold} around the walk, NOT
|
|
28
|
+
# a flag threaded into `Scope`: an ADR-2 plugin-facing bypass of dependency recording would produce
|
|
29
|
+
# a silently stale warm cache that no diagnostic diff can show.
|
|
30
|
+
#
|
|
31
|
+
# There is deliberately NO memo here. The dispatch consumer this module is being extracted FOR does
|
|
32
|
+
# not exist yet, and a memo keyed on the tables this slice happens to name would be keyed on less
|
|
33
|
+
# than the walk reads — `Scope#resolve_ancestor_class_name` consults `discovered_def_nodes` and
|
|
34
|
+
# `discovered_methods` through `known_user_class?`, and `ancestor_name_candidates` reads
|
|
35
|
+
# `discovered_header_nestings`. The slice that brings the hot path brings the memo, keyed on what
|
|
36
|
+
# that consumer actually reads.
|
|
37
|
+
module ExternalAncestorResolution
|
|
38
|
+
# The owner a name must PRECEDE for its declaration to win an MRO. See {declared_before_object?}.
|
|
39
|
+
OBJECT_OWNER = "Object"
|
|
40
|
+
private_constant :OBJECT_OWNER
|
|
41
|
+
|
|
42
|
+
module_function
|
|
43
|
+
|
|
44
|
+
# Resolves `method_name` on `class_name` against the RBS environment, through the project's
|
|
45
|
+
# ancestry where `class_name` itself is absent from RBS.
|
|
46
|
+
#
|
|
47
|
+
# Returns `[definition, owner_name]` — the `RBS::Definition::Method` and the name of the class the
|
|
48
|
+
# walk asked it of (NOT `definition.defined_in`, which may be further up that class's own RBS
|
|
49
|
+
# ancestry) — or `nil` when no ancestor answers.
|
|
50
|
+
#
|
|
51
|
+
# The cut-off is `::Object`, not the own class: a top-level `def` IS a private `Object` instance
|
|
52
|
+
# method, the last link of every MRO, so any declared owner that comes before `::Object` wins at
|
|
53
|
+
# runtime (`Exception#message`, `Array#first`, `Comparable#clamp`) while a name owned by `Object`
|
|
54
|
+
# or `Kernel` themselves sits at or after that rung and contributes no evidence (#316 / #319).
|
|
55
|
+
#
|
|
56
|
+
# `kind` is `:instance` today. The singleton side of the walk (a module's `def self.` reached
|
|
57
|
+
# through a discovered `include`, #527 slice 6) is not implemented, and declines rather than
|
|
58
|
+
# guessing — which is what the engine answers there now.
|
|
59
|
+
#
|
|
60
|
+
# `record_dependencies: false` runs the ancestry walk under {Analysis::DependencyRecorder.withhold}
|
|
61
|
+
# so its reads reach no consumer. Only the WALK is withheld: the `Reflection` lookups around it
|
|
62
|
+
# read the RBS environment, which files no cross-file edge, so this suppresses exactly the intended
|
|
63
|
+
# one. `withhold` is a `[yield, nil]` fast path when nothing is recording, which is every ordinary
|
|
64
|
+
# run.
|
|
65
|
+
#
|
|
66
|
+
# `mixins: false` narrows the walk to the SUPERCLASS chain. #527 slice 1 lands `< Hash` before
|
|
67
|
+
# `include Enumerable` so that slice 2's measurement stays its own, and the narrowing belongs to
|
|
68
|
+
# the CALLER rather than to this module: the implicit-self veto must keep walking both edges,
|
|
69
|
+
# because Ruby reaches an included module's methods too.
|
|
70
|
+
# rubocop:disable-next Metrics/ParameterLists
|
|
71
|
+
def resolve(class_name, method_name, kind = :instance, scope:, environment: nil, name_memo: nil,
|
|
72
|
+
record_dependencies: true, mixins: true)
|
|
73
|
+
return nil if class_name.nil? || scope.nil?
|
|
74
|
+
return nil unless kind == :instance
|
|
75
|
+
|
|
76
|
+
compute(class_name, method_name, scope, environment, name_memo, record_dependencies, mixins)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The RBS method definition for `class_name`, or nil for a class the environment does not know, a
|
|
80
|
+
# name it does not declare, or a signature it cannot build. The single owner of the `rescue` —
|
|
81
|
+
# a malformed third-party signature is a gap, never an exception escaping into inference.
|
|
82
|
+
def method_definition(class_name, method_name, kind, scope: nil, environment: nil)
|
|
83
|
+
if kind == :singleton
|
|
84
|
+
Rigor::Reflection.singleton_method_definition(class_name, method_name, scope: scope,
|
|
85
|
+
environment: environment)
|
|
86
|
+
else
|
|
87
|
+
Rigor::Reflection.instance_method_definition(class_name, method_name, scope: scope,
|
|
88
|
+
environment: environment)
|
|
89
|
+
end
|
|
90
|
+
rescue StandardError
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# True when the RBS declaration found for the name sits on `class_name` itself rather than on an
|
|
95
|
+
# ancestor; mirrors `CheckRules#defined_on?` and `SigGen::Generator#declared_on_class_itself?`.
|
|
96
|
+
def declared_on_class?(definition, class_name)
|
|
97
|
+
return false if definition.nil?
|
|
98
|
+
return false unless definition.respond_to?(:defined_in)
|
|
99
|
+
|
|
100
|
+
defined_in = definition.defined_in
|
|
101
|
+
return false if defined_in.nil?
|
|
102
|
+
|
|
103
|
+
defined_in.to_s.delete_prefix("::") == class_name.to_s.delete_prefix("::")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Issue #633 — true when the declaration's owner sits strictly before `::Object` in `class_name`'s
|
|
107
|
+
# instance MRO, i.e. Ruby dispatches to it ahead of a top-level `def` (which is `Object`'s own
|
|
108
|
+
# private instance method). The own class trivially qualifies. An owner absent from the ancestor
|
|
109
|
+
# list, an unbuildable class, and an ancestry that does not reach `Object` (a `BasicObject`
|
|
110
|
+
# descendant) all answer false, leaving the historical top-level binding untouched.
|
|
111
|
+
def declared_before_object?(definition, class_name, scope: nil, environment: nil)
|
|
112
|
+
return true if declared_on_class?(definition, class_name)
|
|
113
|
+
return false if definition.nil? || !definition.respond_to?(:defined_in)
|
|
114
|
+
|
|
115
|
+
owner = definition.defined_in
|
|
116
|
+
return false if owner.nil?
|
|
117
|
+
|
|
118
|
+
ancestors = instance_ancestor_names(class_name, scope: scope, environment: environment)
|
|
119
|
+
object_index = ancestors.index(OBJECT_OWNER)
|
|
120
|
+
owner_index = ancestors.index(owner.to_s.delete_prefix("::"))
|
|
121
|
+
!object_index.nil? && !owner_index.nil? && owner_index < object_index
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# The class's instance-side ancestors in MRO order, `::`-stripped, or `[]` for a class the RBS
|
|
125
|
+
# environment does not know or cannot build. Read through the loader's accessor rather than
|
|
126
|
+
# `instance_definition(...).ancestors` because that is the one wired to the ancestor-name cache and
|
|
127
|
+
# marked as RIGOR'S OWN demand — ordering two ancestors is not the analysis asking whether either
|
|
128
|
+
# one's methods resolve, and the `rbs.coverage` bookkeeping must not record it as such.
|
|
129
|
+
def instance_ancestor_names(class_name, scope: nil, environment: nil)
|
|
130
|
+
loader = rbs_loader_for(scope, environment)
|
|
131
|
+
loader ? loader.ancestor_names_for(class_name.to_s) : []
|
|
132
|
+
rescue StandardError
|
|
133
|
+
[]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def compute(class_name, method_name, scope, environment, name_memo, record_dependencies, mixins)
|
|
137
|
+
kind = :instance
|
|
138
|
+
own = method_definition(class_name, method_name, kind, scope: scope, environment: environment)
|
|
139
|
+
if own
|
|
140
|
+
own_ancestors = instance_ancestor_names(class_name, scope: scope, environment: environment)
|
|
141
|
+
if declared_before_object?(own, class_name, scope: scope, environment: environment) ||
|
|
142
|
+
owned_within_candidate_chain?(own, own_ancestors)
|
|
143
|
+
return [own, class_name.to_s].freeze
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
groups = ancestor_candidate_groups(scope, class_name, name_memo, record_dependencies, mixins)
|
|
148
|
+
farther_ancestors = {}
|
|
149
|
+
groups.each_with_index do |candidates, index|
|
|
150
|
+
answer = first_known_candidate_answer(candidates, method_name, kind, scope, environment)
|
|
151
|
+
next if answer.nil?
|
|
152
|
+
next if resited_member?(answer, groups, index, farther_ancestors, scope, environment)
|
|
153
|
+
|
|
154
|
+
return answer
|
|
155
|
+
end
|
|
156
|
+
nil
|
|
157
|
+
end
|
|
158
|
+
private_class_method :compute
|
|
159
|
+
|
|
160
|
+
# The first candidate spelling the RBS environment knows is the ancestor Ruby resolves; a name it
|
|
161
|
+
# knows nothing about contributes no evidence either way, and a known one that does NOT answer
|
|
162
|
+
# ends this group rather than falling through to a less-qualified spelling of the same name.
|
|
163
|
+
def first_known_candidate_answer(candidates, method_name, kind, scope, environment)
|
|
164
|
+
candidates.each do |candidate|
|
|
165
|
+
ancestors = instance_ancestor_names(candidate, scope: scope, environment: environment)
|
|
166
|
+
next if ancestors.empty?
|
|
167
|
+
|
|
168
|
+
definition = method_definition(candidate, method_name, kind, scope: scope,
|
|
169
|
+
environment: environment)
|
|
170
|
+
return nil unless declared_before_object?(definition, candidate,
|
|
171
|
+
scope: scope, environment: environment) ||
|
|
172
|
+
owned_within_candidate_chain?(definition, ancestors)
|
|
173
|
+
|
|
174
|
+
return [definition, candidate].freeze
|
|
175
|
+
end
|
|
176
|
+
nil
|
|
177
|
+
end
|
|
178
|
+
private_class_method :first_known_candidate_answer
|
|
179
|
+
|
|
180
|
+
# The module-candidate half of the answering test (#1173 review). A module's RBS ancestry is
|
|
181
|
+
# `[itself, its own includes…]` and never reaches `Object`, so {declared_before_object?} — whose
|
|
182
|
+
# cut-off exists to keep an Object- / Kernel-owned declaration from outranking a top-level `def`
|
|
183
|
+
# — always answers false for it: the cut-off has nothing to cut. What decides instead is that
|
|
184
|
+
# the declaration came from the candidate's OWN chain: when the candidate sits in the receiver's
|
|
185
|
+
# MRO, Ruby dispatches the name through exactly those ancestors, so a `defined_in` anywhere in
|
|
186
|
+
# the list is the method that runs. Without this, a nearer RBS module that inherits the name
|
|
187
|
+
# from its own RBS `include` read as a dead group and a FARTHER ancestor's declaration was
|
|
188
|
+
# adopted in its place — `class C; include A; include B` where `B`'s RBS includes `N` answering
|
|
189
|
+
# A's declaration rather than N's. The gate keeps the Object cut-off's reach: a chain that DOES
|
|
190
|
+
# contain `Object` (every ordinary class candidate) is untouched.
|
|
191
|
+
def owned_within_candidate_chain?(definition, ancestors)
|
|
192
|
+
return false if ancestors.include?(OBJECT_OWNER)
|
|
193
|
+
return false if definition.nil? || !definition.respond_to?(:defined_in)
|
|
194
|
+
|
|
195
|
+
owner = definition.defined_in
|
|
196
|
+
!owner.nil? && ancestors.include?(owner.to_s.delete_prefix("::"))
|
|
197
|
+
end
|
|
198
|
+
private_class_method :owned_within_candidate_chain?
|
|
199
|
+
|
|
200
|
+
# The re-siting guard (#1173 review, second pass). Ruby's `include` is a no-op for a module
|
|
201
|
+
# already in the ancestry, so a member's real position is set by the EARLIEST source statement
|
|
202
|
+
# that pulled it in — which is the FARTHEST group in this search-order walk that carries it.
|
|
203
|
+
# Adopting group `index`'s answer claims the owner (and the declaration's `defined_in`) sit
|
|
204
|
+
# inside this group's segment; when either name also appears in a farther group's RBS chain,
|
|
205
|
+
# the runtime actually sites it there and everything between can shadow it —
|
|
206
|
+
# `class C < Array; include M` where `M`'s RBS includes `Enumerable` must not adopt
|
|
207
|
+
# `Enumerable#first` through `M` while `Array#first` intervenes. Skipping the answer is not a
|
|
208
|
+
# decline of the method: the farther group that truly carries the member resolves it at its
|
|
209
|
+
# real position (or nothing does, and the honest `Dynamic[top]` stands).
|
|
210
|
+
#
|
|
211
|
+
# `farther_ancestors` memoizes each farther group's resolved ancestor list — the first
|
|
212
|
+
# candidate spelling the RBS environment knows, the same spelling rule
|
|
213
|
+
# {first_known_candidate_answer} applies. A group whose names all stay project- or
|
|
214
|
+
# unresolved-side has no visible chain to compare. The deferral can trade precision for
|
|
215
|
+
# safety: when the farther carrier resolves the member without type-argument bindings the
|
|
216
|
+
# answer degrades to `Dynamic[top]` where the skipped group carried one — the intended
|
|
217
|
+
# direction, and in the pinned shapes the runtime-correct answer anyway.
|
|
218
|
+
def resited_member?(answer, groups, index, farther_ancestors, scope, environment)
|
|
219
|
+
definition, owner = answer
|
|
220
|
+
members = [owner.to_s.delete_prefix("::")]
|
|
221
|
+
defined_in = definition.defined_in if definition.respond_to?(:defined_in)
|
|
222
|
+
members << defined_in.to_s.delete_prefix("::") if defined_in
|
|
223
|
+
|
|
224
|
+
((index + 1)...groups.size).any? do |j|
|
|
225
|
+
ancestors = farther_ancestors.fetch(j) do
|
|
226
|
+
farther_ancestors[j] = resolved_group_ancestors(groups[j], scope, environment)
|
|
227
|
+
end
|
|
228
|
+
ancestors&.any? { |name| members.include?(name) }
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
private_class_method :resited_member?
|
|
232
|
+
|
|
233
|
+
# The ancestor list of the first candidate spelling the RBS environment knows — nil when no
|
|
234
|
+
# spelling resolves, which is what the walk emitted the group for in the first place.
|
|
235
|
+
def resolved_group_ancestors(candidates, scope, environment)
|
|
236
|
+
candidates.each do |candidate|
|
|
237
|
+
ancestors = instance_ancestor_names(candidate, scope: scope, environment: environment)
|
|
238
|
+
return ancestors unless ancestors.empty?
|
|
239
|
+
end
|
|
240
|
+
nil
|
|
241
|
+
end
|
|
242
|
+
private_class_method :resolved_group_ancestors
|
|
243
|
+
|
|
244
|
+
# The walk, with its ADR-46 reads attached or detached. `withhold` returns `[result, read_set]`
|
|
245
|
+
# and the read set is dropped: a caller that suppresses is saying these reads are not a dependency
|
|
246
|
+
# of its answer, not that they should be replayed somewhere else.
|
|
247
|
+
def ancestor_candidate_groups(scope, class_name, name_memo, record_dependencies, mixins)
|
|
248
|
+
memo = name_memo || {}
|
|
249
|
+
if record_dependencies
|
|
250
|
+
return scope.external_ancestor_name_candidates(class_name, name_memo: memo, mixins: mixins)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
Analysis::DependencyRecorder.withhold do
|
|
254
|
+
scope.external_ancestor_name_candidates(class_name, name_memo: memo, mixins: mixins)
|
|
255
|
+
end.first
|
|
256
|
+
end
|
|
257
|
+
private_class_method :ancestor_candidate_groups
|
|
258
|
+
|
|
259
|
+
def rbs_loader_for(scope, environment)
|
|
260
|
+
(environment || scope&.environment)&.rbs_loader
|
|
261
|
+
rescue StandardError
|
|
262
|
+
nil
|
|
263
|
+
end
|
|
264
|
+
private_class_method :rbs_loader_for
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../source/node_children"
|
|
6
|
+
require_relative "../type"
|
|
7
|
+
require_relative "stored_block_call"
|
|
8
|
+
|
|
9
|
+
module Rigor
|
|
10
|
+
module Inference
|
|
11
|
+
# Issue #1361 — the blocks whose body does not read the narrowing of the frame-local special variables (the regex
|
|
12
|
+
# match globals, and `$_` since issue #1359) where it is written ({.fresh_entry?}, {.entry}):
|
|
13
|
+
#
|
|
14
|
+
# - a root block: the block of `Thread.new` / `Thread.start` / `Thread.fork`, `Fiber.new` or `Ractor.new` on the
|
|
15
|
+
# core class ({.root_call?}). It runs as the root of a new thread, fiber or ractor, and Ruby keeps that
|
|
16
|
+
# execution context's own special-variable slot (`ec->root_svar`) for every piece of code whose local frame is
|
|
17
|
+
# the block's, the block and each block nested in it included. So the block reads `$1` nil whatever the
|
|
18
|
+
# creator matched, and a match it runs never rebinds the creator's `$~`. A `&expr` block argument's
|
|
19
|
+
# expression still runs in the creator's frame, before the thread starts;
|
|
20
|
+
# - a definer block: the block of `define_method` / `define_singleton_method`, on any receiver. It runs as a
|
|
21
|
+
# method body whenever the method is called, and reads the defining frame's slot as it is at that time, which
|
|
22
|
+
# each call's own matches write too, so the narrowing where the method is defined neither proves nor refutes
|
|
23
|
+
# what it reads. Unlike a root block it shares that slot, so a match in it still rebinds the definer's `$~`.
|
|
24
|
+
#
|
|
25
|
+
# `Enumerator.new` is neither: its block runs on an internal fiber whose root is not the block, so it reads and
|
|
26
|
+
# rebinds the creator's slot like any other block.
|
|
27
|
+
#
|
|
28
|
+
# The receiver of a root call must be the literal constant, and must resolve to the core class where a scope is
|
|
29
|
+
# at hand, so a project's own `Thread` (`module Pool; class Thread; …`) keeps the ordinary block rules.
|
|
30
|
+
module FreshFrameBlocks
|
|
31
|
+
ROOT_CONSTRUCTORS = {
|
|
32
|
+
Thread: %i[new start fork].freeze, Fiber: %i[new].freeze, Ractor: %i[new].freeze
|
|
33
|
+
}.freeze
|
|
34
|
+
DEFINERS = Set[:define_method, :define_singleton_method].freeze
|
|
35
|
+
private_constant :ROOT_CONSTRUCTORS, :DEFINERS
|
|
36
|
+
|
|
37
|
+
module_function
|
|
38
|
+
|
|
39
|
+
# True when `node` is a call whose block (a literal or a `&expr` argument) runs as the root of a new thread,
|
|
40
|
+
# fiber or ractor. `scope` resolves the receiver; without one the literal constant is taken at its word.
|
|
41
|
+
def root_call?(node, scope = nil)
|
|
42
|
+
return false unless node.is_a?(Prism::CallNode) && !node.block.nil?
|
|
43
|
+
|
|
44
|
+
constant = StoredBlockCall.root_constant_name(node.receiver)
|
|
45
|
+
return false unless ROOT_CONSTRUCTORS[constant]&.include?(node.name)
|
|
46
|
+
|
|
47
|
+
scope.nil? || core_class?(node.receiver, constant, scope)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The block of `node` when {.root_call?} holds, else nil.
|
|
51
|
+
def root_block(node, scope = nil)
|
|
52
|
+
root_call?(node, scope) ? node.block : nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# True when the block answers true for a child of `node` that runs in the frame `node` runs in: any child but
|
|
56
|
+
# a root block ({.root_block}), which runs with a slot of its own. Every {MatchRebinding} reading of what
|
|
57
|
+
# running code may match walks through here. A root `&expr` block argument's own children are asked in its
|
|
58
|
+
# place, since Ruby evaluates `expr` in this frame before the thread starts
|
|
59
|
+
# (`Thread.new(&HANDLERS.fetch(name.sub(re, "")))` rebinds the creator's `$~`). `into_root` asks a root block's
|
|
60
|
+
# children too, for a reading of what the code makes rather than what it runs: a closure made in a root block
|
|
61
|
+
# may be handed back to this frame's thread. Stops at the first true answer, without a `return` out of the
|
|
62
|
+
# walk's block.
|
|
63
|
+
def any_frame_child?(node, scope, into_root: false)
|
|
64
|
+
root = root_block(node, scope)
|
|
65
|
+
found = false
|
|
66
|
+
node.rigor_each_child do |child|
|
|
67
|
+
if !child.equal?(root)
|
|
68
|
+
found ||= yield(child)
|
|
69
|
+
elsif into_root || child.is_a?(Prism::BlockArgumentNode)
|
|
70
|
+
child.rigor_each_child { |inner| found ||= yield(inner) }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
found
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# True when the block of `call_node` does not read the narrowing where it is written: a root block, or a
|
|
77
|
+
# definer block. {.entry} gives the scope it enters with.
|
|
78
|
+
def fresh_entry?(call_node, scope = nil)
|
|
79
|
+
return false unless call_node.is_a?(Prism::CallNode)
|
|
80
|
+
|
|
81
|
+
DEFINERS.include?(call_node.name) || root_call?(call_node, scope)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# The scope the block of `call_node`, which {.fresh_entry?} names, enters with. A root block reads a slot of its
|
|
85
|
+
# own, so the match globals are unbound there, and `k = $1; k.upcase` in an entered `Thread.new { … }` block is
|
|
86
|
+
# a true positive. A definer body reads the definer's slot whenever the method is called, which the analyzer
|
|
87
|
+
# does not follow, so a global narrowed where it is written reads `Dynamic[top]` there, neither narrowed nor
|
|
88
|
+
# flagged: the dynamic-finder idiom defines `find_by_email` in a `method_missing` guard whose `$1` its body
|
|
89
|
+
# reads. This is the one place the specials are reset at such an entry: the match globals, `$_` (#1359), and
|
|
90
|
+
# `$!`, `$@` and `$?` (#1360).
|
|
91
|
+
#
|
|
92
|
+
# A root block runs in an execution context of its own, so it reads `$!` and `$@` as `nil` even where it is
|
|
93
|
+
# written in a rescue clause ({ErrorInfo}). `$?` is the thread's, so a thread's or a ractor's root block starts
|
|
94
|
+
# without one, while a fiber shares the thread's. A definer body reads all three as they are when the method is
|
|
95
|
+
# called.
|
|
96
|
+
def entry(scope, call_node)
|
|
97
|
+
if DEFINERS.include?(call_node.name)
|
|
98
|
+
scope.untyped_match_globals.untyped_last_line.untyped_error_info.untyped_last_status
|
|
99
|
+
else
|
|
100
|
+
fresh = scope.forget_match_globals.forget_last_line.forget_error_info
|
|
101
|
+
StoredBlockCall.root_constant_name(call_node.receiver) == :Fiber ? fresh : fresh.forget_last_status
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Issue #1360 — the scope a closure's body enters with: a lambda literal's (`block_node` a `Prism::LambdaNode`),
|
|
106
|
+
# or the block of a call that keeps it to run later ({StoredBlockCall.stores_block?}: `lambda`, `proc`,
|
|
107
|
+
# `Proc.new`, `Enumerator.new`, `Hash.new`). The body runs whenever the closure is called, which is usually after
|
|
108
|
+
# the rescue clause it is written in has exited (`f = -> { $! }` in a clause reads `nil` once called past the
|
|
109
|
+
# `begin`), and may be on another thread (`Thread.new(&f)`), so `$!`, `$@` and `$?` are unbound there. A root or
|
|
110
|
+
# definer block enters as {.entry} gives; every other block reads them where it is written.
|
|
111
|
+
def closure_entry(scope, block_node, call_node)
|
|
112
|
+
return scope unless block_node.is_a?(Prism::LambdaNode) ||
|
|
113
|
+
(call_node.is_a?(Prism::CallNode) && StoredBlockCall.stores_block?(call_node))
|
|
114
|
+
|
|
115
|
+
scope.forget_error_info.forget_last_status
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def core_class?(receiver, constant, scope)
|
|
119
|
+
type = scope.type_of(receiver)
|
|
120
|
+
type.is_a?(Type::Singleton) && type.class_name == constant.name
|
|
121
|
+
rescue StandardError
|
|
122
|
+
false
|
|
123
|
+
end
|
|
124
|
+
private_class_method :core_class?
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../source/constant_path"
|
|
6
|
+
require_relative "../source/node_children"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Inference
|
|
10
|
+
# Issue #1367 — the program facts the `global.*` write rules read, gathered syntactically over every file:
|
|
11
|
+
#
|
|
12
|
+
# - `[:alias, name]` — a global variable name an `alias $new $old` statement names, on either side. After it,
|
|
13
|
+
# `$new` is `$old`'s variable, setter included, so both rules exempt the name.
|
|
14
|
+
# - `[:defines, name]` — a method named after one of {ANSWER_NAMES} is defined somewhere, in any spelling and on
|
|
15
|
+
# any receiver: `def`, `def obj.m`, a `class << obj` body, `define_method`, `define_singleton_method`,
|
|
16
|
+
# `alias`, `alias_method`, `attr_*`, a delegation macro, or the same call through `send`. The
|
|
17
|
+
# type check does not ask where it lands: an object can reach any of them (`class << nil`, `K = Integer;
|
|
18
|
+
# class K`, `[Integer].each { |k| k.define_method(:write) }`), so a literal of any class declines.
|
|
19
|
+
# - {DEFINES_ANY} — a definition whose name no literal spells: a computed `define_method` name, a `send` whose
|
|
20
|
+
# method name is computed, a string `eval` / `class_eval` whose code is interpolated or not a literal, a name
|
|
21
|
+
# literal whose bytes are not valid in its encoding, or a top-level mixin of a non-constant. A node the
|
|
22
|
+
# collector fails to read records it too ({Collector#visit}).
|
|
23
|
+
# - `[:refines, name]` / {REFINES_ANY} — the same, inside a `refine` block. A refinement changes what
|
|
24
|
+
# `respond_to?(:write)` answers where a `using` is in effect, and nothing else the setters consult: an
|
|
25
|
+
# implicit conversion and a refined `respond_to?` / `respond_to_missing?` ignore it, so these entries are
|
|
26
|
+
# kept apart from `[:defines, …]`.
|
|
27
|
+
# - `[:main_mixin, name]` — a module a top-level `include`, `prepend` or `extend` mixes into `Object` (or
|
|
28
|
+
# `main`), as written. A module RBS does not know may carry any method, so it reaches every class.
|
|
29
|
+
#
|
|
30
|
+
# The census is a `Set` of those frozen entries, merged by union across files, the project pre-pass and the
|
|
31
|
+
# `pre_eval:` files. It is recorded inside descents that already visit every node ({Collector#visit}), never
|
|
32
|
+
# by a walk of its own, except for a `pre_eval:` file ({.scan}).
|
|
33
|
+
module GlobalWriteCensus
|
|
34
|
+
# The methods whose presence lets an object answer a setter: the one it asks `respond_to?` about (`write`),
|
|
35
|
+
# the implicit conversions (`to_str`, `to_int`), and the escape hatches both consult.
|
|
36
|
+
ANSWER_NAMES = %i[write to_str to_int method_missing respond_to_missing? respond_to?].to_set.freeze
|
|
37
|
+
|
|
38
|
+
DEFINES_ANY = [:defines_any].freeze
|
|
39
|
+
REFINES_ANY = [:refines_any].freeze
|
|
40
|
+
|
|
41
|
+
# `define_method`-family calls whose first argument names the method they define.
|
|
42
|
+
NAMING_CALLS = %i[define_method define_singleton_method alias_method].to_set.freeze
|
|
43
|
+
# Calls every literal argument of which may name a method they define.
|
|
44
|
+
ATTRIBUTE_CALLS = %i[attr attr_reader attr_writer attr_accessor].to_set.freeze
|
|
45
|
+
DELEGATION_CALLS = %i[
|
|
46
|
+
def_delegator def_delegators def_instance_delegator def_instance_delegators def_single_delegator
|
|
47
|
+
def_single_delegators delegate instance_delegate single_delegate
|
|
48
|
+
].to_set.freeze
|
|
49
|
+
# `delegate_missing_to` defines `method_missing` and `respond_to_missing?` whatever it is handed.
|
|
50
|
+
MISSING_DELEGATION_CALLS = %i[delegate_missing_to].to_set.freeze
|
|
51
|
+
SEND_CALLS = %i[send __send__ public_send].to_set.freeze
|
|
52
|
+
EVAL_CALLS = %i[eval class_eval module_eval instance_eval].to_set.freeze
|
|
53
|
+
MIXIN_CALLS = %i[include prepend extend].to_set.freeze
|
|
54
|
+
DEFINING_CALLS =
|
|
55
|
+
(NAMING_CALLS | ATTRIBUTE_CALLS | DELEGATION_CALLS | MISSING_DELEGATION_CALLS | EVAL_CALLS).freeze
|
|
56
|
+
# Every call name {Collector#visit} reads; any other call is skipped at once.
|
|
57
|
+
VISITED_CALLS = (DEFINING_CALLS | SEND_CALLS | MIXIN_CALLS | %i[import_methods]).freeze
|
|
58
|
+
NO_ARGUMENTS = [].freeze
|
|
59
|
+
NAME_PATTERN = /(?<![\w@$])(?:write|to_str|to_int|method_missing|respond_to_missing\?|respond_to\?)(?![\w?!=])/
|
|
60
|
+
private_constant :NAMING_CALLS, :ATTRIBUTE_CALLS, :DELEGATION_CALLS, :MISSING_DELEGATION_CALLS,
|
|
61
|
+
:SEND_CALLS, :EVAL_CALLS, :MIXIN_CALLS, :DEFINING_CALLS, :VISITED_CALLS, :NO_ARGUMENTS,
|
|
62
|
+
:NAME_PATTERN
|
|
63
|
+
|
|
64
|
+
EMPTY = Set.new.freeze
|
|
65
|
+
|
|
66
|
+
module_function
|
|
67
|
+
|
|
68
|
+
def alias_entry(name) = [:alias, name].freeze
|
|
69
|
+
|
|
70
|
+
def aliased?(census, name) = census.include?([:alias, name])
|
|
71
|
+
|
|
72
|
+
# Whether the program may define one of `names` somewhere, outside a refinement.
|
|
73
|
+
def defines_any_of?(census, names)
|
|
74
|
+
census.include?(DEFINES_ANY) || names.any? { |name| census.include?([:defines, name]) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Whether some refinement may add `write`.
|
|
78
|
+
def refines_write?(census) = census.include?(REFINES_ANY) || census.include?(%i[refines write])
|
|
79
|
+
|
|
80
|
+
# The module names the top-level mixins spell.
|
|
81
|
+
def main_mixins(census) = census.filter_map { |entry| entry[1] if entry[0] == :main_mixin }
|
|
82
|
+
|
|
83
|
+
# A standalone walk, for a file no existing descent visits (a `pre_eval:` entry).
|
|
84
|
+
def scan(root)
|
|
85
|
+
collector = Collector.new
|
|
86
|
+
walk(root, collector, true)
|
|
87
|
+
collector.census.freeze
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def walk(node, collector, top_level)
|
|
91
|
+
return unless node.is_a?(Prism::Node)
|
|
92
|
+
|
|
93
|
+
collector.visit(node, top_level: top_level)
|
|
94
|
+
inner = top_level && !(node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode))
|
|
95
|
+
node.rigor_each_child { |child| walk(child, collector, inner) }
|
|
96
|
+
end
|
|
97
|
+
private_class_method :walk
|
|
98
|
+
|
|
99
|
+
# Accumulates one file's census, fed each node of a pre-order descent. `top_level` says whether the node sits
|
|
100
|
+
# outside every `class` / `module` body; it is the host walk's to track. `refine` blocks are remembered by
|
|
101
|
+
# offset as they are met, so a node inside one is known without the host walk tracking it.
|
|
102
|
+
class Collector
|
|
103
|
+
attr_reader :census
|
|
104
|
+
|
|
105
|
+
def initialize
|
|
106
|
+
@census = Set.new
|
|
107
|
+
@refine_ranges = []
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# A node the collector cannot read must not fail the analysis of its file, nor of every file the pre-pass
|
|
111
|
+
# feeds: the census then counts it as a definition of every name, which only declines a report.
|
|
112
|
+
def visit(node, top_level:)
|
|
113
|
+
case node
|
|
114
|
+
when Prism::AliasGlobalVariableNode then visit_global_alias(node)
|
|
115
|
+
when Prism::DefNode then record(node, node.name)
|
|
116
|
+
when Prism::AliasMethodNode then record_argument(node, node.new_name)
|
|
117
|
+
when Prism::CallNode then visit_call(node, top_level)
|
|
118
|
+
end
|
|
119
|
+
rescue StandardError
|
|
120
|
+
@census << DEFINES_ANY
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def visit_global_alias(node)
|
|
126
|
+
[node.new_name, node.old_name].each do |side|
|
|
127
|
+
@census << GlobalWriteCensus.alias_entry(side.name) if side.is_a?(Prism::GlobalVariableReadNode)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def visit_call(node, top_level)
|
|
132
|
+
remember_refine(node)
|
|
133
|
+
name = node.name
|
|
134
|
+
return unless VISITED_CALLS.include?(name)
|
|
135
|
+
|
|
136
|
+
arguments = node.arguments&.arguments || NO_ARGUMENTS
|
|
137
|
+
if SEND_CALLS.include?(name)
|
|
138
|
+
return if arguments.empty?
|
|
139
|
+
|
|
140
|
+
# A method name no literal spells (computed, or with bytes invalid in its encoding) may be any of them. A
|
|
141
|
+
# literal one is read as that call written directly: `send(:include, M)` at the top level is a mixin, and
|
|
142
|
+
# `send(:size)` defines nothing.
|
|
143
|
+
first = literal_name(arguments.first)
|
|
144
|
+
return @census << any_entry(node) if first.nil?
|
|
145
|
+
|
|
146
|
+
name = first
|
|
147
|
+
arguments = arguments.drop(1)
|
|
148
|
+
end
|
|
149
|
+
visit_defining_call(node, name, arguments)
|
|
150
|
+
visit_main_mixin(node, arguments) if top_level && MIXIN_CALLS.include?(name) && self_receiver?(node)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def visit_defining_call(node, name, arguments)
|
|
154
|
+
if NAMING_CALLS.include?(name)
|
|
155
|
+
record_argument(node, arguments.first)
|
|
156
|
+
elsif ATTRIBUTE_CALLS.include?(name) || DELEGATION_CALLS.include?(name)
|
|
157
|
+
arguments.each { |argument| record_every_literal(node, argument) }
|
|
158
|
+
elsif MISSING_DELEGATION_CALLS.include?(name)
|
|
159
|
+
record(node, :method_missing)
|
|
160
|
+
record(node, :respond_to_missing?)
|
|
161
|
+
elsif EVAL_CALLS.include?(name)
|
|
162
|
+
# Only the first argument is code; the rest are a binding, a file name and a line number.
|
|
163
|
+
record_eval_text(node, arguments.first) unless arguments.empty?
|
|
164
|
+
elsif name == :import_methods && inside_refine?(node)
|
|
165
|
+
@census << REFINES_ANY
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# A top-level `include M` / `prepend M` / `extend M` mixes M into `Object` (or `main`).
|
|
170
|
+
def visit_main_mixin(node, arguments)
|
|
171
|
+
arguments.each do |argument|
|
|
172
|
+
constant = Source::ConstantPath.qualified_name(argument) if constant_node?(argument)
|
|
173
|
+
@census << (constant ? [:main_mixin, constant.delete_prefix("::").freeze].freeze : any_entry(node))
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def record(node, name)
|
|
178
|
+
return unless ANSWER_NAMES.include?(name)
|
|
179
|
+
|
|
180
|
+
@census << [inside_refine?(node) ? :refines : :defines, name].freeze
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# A literal method name, or any name when the argument spells none.
|
|
184
|
+
def record_argument(node, argument)
|
|
185
|
+
name = literal_name(argument)
|
|
186
|
+
name ? record(node, name) : @census << any_entry(node)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def record_every_literal(node, argument)
|
|
190
|
+
case argument
|
|
191
|
+
when Prism::SymbolNode, Prism::StringNode then record_argument(node, argument)
|
|
192
|
+
when Prism::ArrayNode then argument.elements.each { |element| record_every_literal(node, element) }
|
|
193
|
+
when Prism::KeywordHashNode, Prism::HashNode
|
|
194
|
+
argument.elements.each { |pair| record_every_literal(node, pair) }
|
|
195
|
+
when Prism::AssocNode
|
|
196
|
+
record_every_literal(node, argument.key)
|
|
197
|
+
record_every_literal(node, argument.value)
|
|
198
|
+
else @census << any_entry(node)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def record_eval_text(node, argument)
|
|
203
|
+
if argument.is_a?(Prism::StringNode) && argument.unescaped.valid_encoding?
|
|
204
|
+
argument.unescaped.scan(NAME_PATTERN) { |name| record(node, name.to_sym) }
|
|
205
|
+
else
|
|
206
|
+
@census << any_entry(node)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def any_entry(node) = inside_refine?(node) ? REFINES_ANY : DEFINES_ANY
|
|
211
|
+
|
|
212
|
+
# The name a Symbol or String literal spells, or nil for any other node and for a literal whose bytes are
|
|
213
|
+
# not valid in its encoding, which names no method Ruby can define.
|
|
214
|
+
def literal_name(argument)
|
|
215
|
+
return unless argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode)
|
|
216
|
+
|
|
217
|
+
text = argument.unescaped
|
|
218
|
+
text.to_sym if text.valid_encoding?
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def constant_node?(node) = node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
|
222
|
+
|
|
223
|
+
def self_receiver?(node) = node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
224
|
+
|
|
225
|
+
def remember_refine(node)
|
|
226
|
+
return unless node.name == :refine && self_receiver?(node) && node.block.is_a?(Prism::BlockNode)
|
|
227
|
+
|
|
228
|
+
location = node.block.location
|
|
229
|
+
@refine_ranges << (location.start_offset...location.end_offset)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def inside_refine?(node)
|
|
233
|
+
offset = node.location.start_offset
|
|
234
|
+
@refine_ranges.any? { |range| range.cover?(offset) }
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|