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,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../../inference/scope_indexer"
|
|
6
|
+
require_relative "../../source/constant_path"
|
|
7
|
+
require_relative "../../source/node_children"
|
|
8
|
+
|
|
9
|
+
module Rigor
|
|
10
|
+
module Analysis
|
|
11
|
+
module CheckRules
|
|
12
|
+
# Issue #1120 — the two ways one file makes a method callable at some sites and not at others, which
|
|
13
|
+
# `call.undefined-method` has to ask the file's syntax about because no project-wide table can answer:
|
|
14
|
+
#
|
|
15
|
+
# - **Refinements.** `using M` activates M's refinements from the end of the call to the end of the body
|
|
16
|
+
# that holds it — the file's top level or a `class` / `module` body, including everything nested in it —
|
|
17
|
+
# and nowhere else, not even in another file. A `refine X do … end` block is active inside itself. What
|
|
18
|
+
# each module refines is the project-wide `Scope#discovered_refinements`; this answers whether any of
|
|
19
|
+
# those modules is in effect at a call site. A `using` whose argument is not a constant
|
|
20
|
+
# (`using Module.new { refine … }`) names no module, so every refinement counts as in effect throughout
|
|
21
|
+
# its file. A `using` inside a `def` raises in Ruby and activates nothing.
|
|
22
|
+
# - **Singleton defs on locals.** `def o.m` defines `m` on the one object `o` holds. The local's type
|
|
23
|
+
# is not changed; `o.m` is simply not reported within the scope the `def` is written in (the
|
|
24
|
+
# enclosing `def`, `class` / `module` body, or file — a block shares its enclosing scope's locals).
|
|
25
|
+
#
|
|
26
|
+
# Built lazily: the walk runs the first time a would-fire call asks, so a file with no such call pays
|
|
27
|
+
# one small object and nothing else.
|
|
28
|
+
class LexicalMethodSites
|
|
29
|
+
def initialize(root)
|
|
30
|
+
@root = root
|
|
31
|
+
@built = false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Is a refinement from one of `modules` (refining-module names) in effect at `call_node`?
|
|
35
|
+
def refinement_active?(call_node, modules)
|
|
36
|
+
build
|
|
37
|
+
return true if @unresolved_using
|
|
38
|
+
|
|
39
|
+
offset = call_node.location.start_offset
|
|
40
|
+
return true if @refine_blocks.any? { |start, stop| offset >= start && offset < stop }
|
|
41
|
+
|
|
42
|
+
@usings.any? do |start, stop, candidates|
|
|
43
|
+
offset >= start && offset < stop && candidates.any? { |name| modules.include?(name) }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Issue #1367 — is any refinement in effect at `node`: a `using` of any module, a `using` of a non-constant, or
|
|
48
|
+
# the inside of a `refine` block? The `global.*` stream check asks it where it cannot name the refining
|
|
49
|
+
# module (a `define_method` or `alias_method` inside `refine` records none).
|
|
50
|
+
def using_in_effect?(node)
|
|
51
|
+
build
|
|
52
|
+
return true if @unresolved_using
|
|
53
|
+
|
|
54
|
+
offset = node.location.start_offset
|
|
55
|
+
@refine_blocks.any? { |start, stop| offset >= start && offset < stop } ||
|
|
56
|
+
@usings.any? { |start, stop, _candidates| offset >= start && offset < stop }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Is `def_node` one of the defs a `refine X do … end` body defines on X? Such a def redefines X's method by
|
|
60
|
+
# design, so X's declared signature for the name is not its contract (issue #1120, maintainer ruling a′).
|
|
61
|
+
def refinement_def?(def_node)
|
|
62
|
+
build
|
|
63
|
+
@refinement_defs.include?(def_node.location.start_offset)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Does a `def <local>.<name>` for this call's local receiver and method name sit in the scope the call
|
|
67
|
+
# is in?
|
|
68
|
+
def singleton_local_def?(call_node)
|
|
69
|
+
receiver = call_node.receiver
|
|
70
|
+
return false unless receiver.is_a?(Prism::LocalVariableReadNode)
|
|
71
|
+
|
|
72
|
+
build
|
|
73
|
+
offset = call_node.location.start_offset
|
|
74
|
+
@singleton_defs.any? do |local, method_name, start, stop|
|
|
75
|
+
local == receiver.name && method_name == call_node.name && offset >= start && offset < stop &&
|
|
76
|
+
!nested_scope_holds?(offset, start, stop)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# Is `offset` inside a local scope nested in the one `[start, stop)` spans? A `def` in the file's top level
|
|
83
|
+
# does not see the top level's `o`.
|
|
84
|
+
def nested_scope_holds?(offset, start, stop)
|
|
85
|
+
@scopes.any? do |inner_start, inner_stop|
|
|
86
|
+
(inner_start > start || inner_stop < stop) && inner_start >= start && inner_stop <= stop &&
|
|
87
|
+
offset >= inner_start && offset < inner_stop
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def build
|
|
92
|
+
return if @built
|
|
93
|
+
|
|
94
|
+
@built = true
|
|
95
|
+
@usings = []
|
|
96
|
+
@refine_blocks = []
|
|
97
|
+
@singleton_defs = []
|
|
98
|
+
@scopes = []
|
|
99
|
+
@refinement_defs = Set.new
|
|
100
|
+
@unresolved_using = false
|
|
101
|
+
return if @root.nil?
|
|
102
|
+
|
|
103
|
+
location = @root.location
|
|
104
|
+
span = [location.start_offset, location.end_offset]
|
|
105
|
+
walk(@root, [], span, span, false)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# `body` is the `[start, end]` of the body a `using` here stays in effect to the end of; `locals` is the
|
|
109
|
+
# span of the scope a local written here belongs to.
|
|
110
|
+
def walk(node, prefix, body, locals, in_def)
|
|
111
|
+
case node
|
|
112
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
113
|
+
span = scope_span(node)
|
|
114
|
+
inner = Source::ConstantPath.declaration_prefix(prefix, node.constant_path) || prefix
|
|
115
|
+
return walk_children(node.body, inner, span, span, false)
|
|
116
|
+
when Prism::SingletonClassNode
|
|
117
|
+
walk(node.expression, prefix, body, locals, in_def)
|
|
118
|
+
span = scope_span(node)
|
|
119
|
+
return walk_children(node.body, prefix, span, span, false)
|
|
120
|
+
when Prism::DefNode
|
|
121
|
+
record_singleton_def(node, locals)
|
|
122
|
+
return walk_children(node.body, prefix, body, scope_span(node), true)
|
|
123
|
+
when Prism::CallNode
|
|
124
|
+
record_call(node, prefix, body, in_def)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
walk_children(node, prefix, body, locals, in_def)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def walk_children(node, prefix, body, locals, in_def)
|
|
131
|
+
return if node.nil?
|
|
132
|
+
|
|
133
|
+
node.rigor_each_child { |child| walk(child, prefix, body, locals, in_def) }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def record_singleton_def(node, locals)
|
|
137
|
+
receiver = node.receiver
|
|
138
|
+
return unless receiver.is_a?(Prism::LocalVariableReadNode)
|
|
139
|
+
|
|
140
|
+
@singleton_defs << [receiver.name, node.name, *locals]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def record_call(node, prefix, body, in_def)
|
|
144
|
+
if Inference::ScopeIndexer.refine_target(node)
|
|
145
|
+
@refine_blocks << span_of(node.block)
|
|
146
|
+
record_refinement_defs(node.block.body)
|
|
147
|
+
elsif using_call?(node) && !in_def
|
|
148
|
+
record_using(node, prefix, body)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def record_refinement_defs(body)
|
|
153
|
+
return if body.nil?
|
|
154
|
+
|
|
155
|
+
Inference::ScopeIndexer.each_refinement_def(body) do |def_node|
|
|
156
|
+
@refinement_defs << def_node.location.start_offset
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def using_call?(node)
|
|
161
|
+
node.name == :using && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
|
|
162
|
+
node.arguments&.arguments&.size == 1
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def record_using(node, prefix, body)
|
|
166
|
+
argument = node.arguments.arguments.first
|
|
167
|
+
if argument.is_a?(Prism::ConstantReadNode) || argument.is_a?(Prism::ConstantPathNode)
|
|
168
|
+
candidates = Inference::ScopeIndexer.constant_receiver_candidates(argument, prefix)
|
|
169
|
+
@usings << [node.location.end_offset, body.last, candidates]
|
|
170
|
+
else
|
|
171
|
+
@unresolved_using = true
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# The span of a node that opens a local scope, recorded for {#nested_scope_holds?}.
|
|
176
|
+
def scope_span(node)
|
|
177
|
+
span = span_of(node)
|
|
178
|
+
@scopes << span
|
|
179
|
+
span
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def span_of(node)
|
|
183
|
+
location = node.location
|
|
184
|
+
[location.start_offset, location.end_offset]
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -19,10 +19,12 @@ module Rigor
|
|
|
19
19
|
# accumulated {Diagnostic}s, in the same emission order the inline `NodeWalker.each` produced them (the
|
|
20
20
|
# shared walk is the same visit-before-descend DFS over `compact_child_nodes`).
|
|
21
21
|
class MainPassCollector
|
|
22
|
-
# The node classes the
|
|
23
|
-
#
|
|
22
|
+
# The node classes the main pass branches on. A plain `Prism::IfNode` covers ternaries and postfix `if`
|
|
23
|
+
# too (the legacy `when Prism::IfNode, Prism::UnlessNode` arm did the same). The three global-write
|
|
24
|
+
# classes carry issue #1367's `global.*` rules.
|
|
24
25
|
NODE_CLASSES = [
|
|
25
|
-
Prism::CallNode, Prism::DefNode, Prism::IfNode, Prism::UnlessNode
|
|
26
|
+
Prism::CallNode, Prism::DefNode, Prism::IfNode, Prism::UnlessNode,
|
|
27
|
+
Prism::GlobalVariableWriteNode, Prism::GlobalVariableOperatorWriteNode, Prism::MultiWriteNode
|
|
26
28
|
].freeze
|
|
27
29
|
|
|
28
30
|
# @param node_diagnostics — maps a `Prism::Node` to the array of diagnostics the main pass emits
|
|
@@ -6,9 +6,9 @@ module Rigor
|
|
|
6
6
|
# Canonical identifiers for each rule. Per ADR-8 § "Diagnostic ID family hierarchy", rule names are
|
|
7
7
|
# `family.rule-name` two-segment strings; the families group diagnostics by where they originate
|
|
8
8
|
# (`call.*` for call-site rules, `flow.*` for flow-analysis proofs, `assert.*` for runtime-assertion
|
|
9
|
-
# rules, `dump.*` for debug helpers, `def.*` for method-definition rules
|
|
10
|
-
# `disable:` list and the in-source `# rigor:disable <rule>`
|
|
11
|
-
# register here so user configuration can refer to them.
|
|
9
|
+
# rules, `dump.*` for debug helpers, `def.*` for method-definition rules, `global.*` for writes to special
|
|
10
|
+
# globals). Used by the configuration `disable:` list and the in-source `# rigor:disable <rule>`
|
|
11
|
+
# suppression comment system; new rules MUST register here so user configuration can refer to them.
|
|
12
12
|
#
|
|
13
13
|
# ADR-87 WD4 — this pure-data constant table is split out of the (engine-heavy) `check_rules.rb` so
|
|
14
14
|
# {Analysis::RuleCatalog} — which the CLI's JSON `evidence_tier` / `documentation_url` enrichment reads
|
|
@@ -66,6 +66,18 @@ module Rigor
|
|
|
66
66
|
# annotation must never turn collection on by itself (that would be a project-wide cost cliff
|
|
67
67
|
# nobody asked for), and it must equally never be silently inert.
|
|
68
68
|
RULE_EFFECT_ANNOTATIONS_UNCHECKED = "effect.annotations-unchecked"
|
|
69
|
+
# ADR-112 WD5 / #1075 — two statements of one member's contract that contradict: a `sig/` declaration
|
|
70
|
+
# against an inline `@rbs` / `#:` one (and, once #1073 reads it, `@rbs` / `#:` against `@extrbs`), or a
|
|
71
|
+
# `rigor:v1:` refinement outside the member's own declared type. `rbs.` because the finding is about
|
|
72
|
+
# the RBS the environment is built from, whichever file spelled it; not `rbs_extended.`, whose rows
|
|
73
|
+
# are payload validity, since most contradictions involve no `RBS::Extended` annotation at all.
|
|
74
|
+
RULE_CONTRADICTING_SIGNATURE = "rbs.contradicting-signature"
|
|
75
|
+
# Issue #1367 (ADR-117 WD2) — the `global.*` family: a write to a special global that its setter rejects, so
|
|
76
|
+
# the write raises on every run. Two ids so a project can tune the type check without hiding read-only
|
|
77
|
+
# writes; the `write` wording follows `def.ivar-write-mismatch`. The envelope is the interpreter's setter
|
|
78
|
+
# (`SpecialGlobalSetters`), not the global's RBS declaration.
|
|
79
|
+
RULE_GLOBAL_WRITE_TYPE_MISMATCH = "global.write-type-mismatch"
|
|
80
|
+
RULE_GLOBAL_READONLY_WRITE = "global.readonly-write"
|
|
69
81
|
|
|
70
82
|
ALL_RULES = [
|
|
71
83
|
RULE_UNDEFINED_METHOD,
|
|
@@ -91,6 +103,8 @@ module Rigor
|
|
|
91
103
|
RULE_OVERRIDE_RETURN_WIDENED,
|
|
92
104
|
RULE_OVERRIDE_PARAM_NARROWED,
|
|
93
105
|
RULE_IVAR_WRITE_MISMATCH,
|
|
106
|
+
RULE_GLOBAL_WRITE_TYPE_MISMATCH,
|
|
107
|
+
RULE_GLOBAL_READONLY_WRITE,
|
|
94
108
|
RULE_SUPPRESSION_UNKNOWN_RULE,
|
|
95
109
|
RULE_SUPPRESSION_EMPTY,
|
|
96
110
|
RULE_SUPPRESSION_UNKNOWN_MARKER,
|
|
@@ -98,7 +112,8 @@ module Rigor
|
|
|
98
112
|
RULE_EFFECT_ENVELOPE_EXCEEDED,
|
|
99
113
|
RULE_EFFECT_LISKOV_WIDENED,
|
|
100
114
|
RULE_EFFECT_UNKNOWN_LABEL,
|
|
101
|
-
RULE_EFFECT_ANNOTATIONS_UNCHECKED
|
|
115
|
+
RULE_EFFECT_ANNOTATIONS_UNCHECKED,
|
|
116
|
+
RULE_CONTRADICTING_SIGNATURE
|
|
102
117
|
].freeze
|
|
103
118
|
|
|
104
119
|
# Backward-compat alias table (ADR-8 § "Backward compatibility"). Existing user code with
|
|
@@ -128,7 +143,7 @@ module Rigor
|
|
|
128
143
|
|
|
129
144
|
# Family wildcard — a `<family>` token in a suppression comment or `disable:` list disables every rule
|
|
130
145
|
# whose canonical id starts with `<family>.`. Per ADR-8 § "1".
|
|
131
|
-
RULE_FAMILIES = %w[call flow assert dump def suppression static effect].freeze
|
|
146
|
+
RULE_FAMILIES = %w[call flow assert dump def global suppression static effect].freeze
|
|
132
147
|
|
|
133
148
|
# Families of diagnostics the engine emits OUTSIDE the CheckRules catalogue (aggregator-level and
|
|
134
149
|
# reporter-level diagnostics such as `rbs_extended.unsatisfied-conformance`,
|
|
@@ -50,6 +50,7 @@ module Rigor
|
|
|
50
50
|
Struct: %i[new],
|
|
51
51
|
Data: %i[define]
|
|
52
52
|
}.freeze
|
|
53
|
+
Ractor.make_shareable(DETACHED_CLASS_FACTORIES)
|
|
53
54
|
private_constant :DETACHED_CLASS_FACTORIES
|
|
54
55
|
|
|
55
56
|
EMPTY_METHODS = [].freeze
|
|
@@ -108,7 +109,7 @@ module Rigor
|
|
|
108
109
|
end
|
|
109
110
|
end
|
|
110
111
|
|
|
111
|
-
#
|
|
112
|
+
# Mirrors {RuleWalk.descend}.
|
|
112
113
|
def descend(node, context)
|
|
113
114
|
RuleWalk.descend(node, context)
|
|
114
115
|
end
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../reflection"
|
|
4
|
+
require_relative "../../source/parameter_envelope"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Analysis
|
|
8
|
+
module CheckRules
|
|
9
|
+
# Issue #992 — the positional envelope `call.wrong-arity` may check a call against when no signature
|
|
10
|
+
# declares the method: the {Source::ParameterEnvelope} the project's own `def` records in
|
|
11
|
+
# `Scope::DiscoveryIndex#discovered_parameter_envelopes`, and only when nothing the project can see
|
|
12
|
+
# could make a different definition of that name the one that runs.
|
|
13
|
+
#
|
|
14
|
+
# Asked in two stages, because every condition past the first can only WITHHOLD a firing:
|
|
15
|
+
#
|
|
16
|
+
# 1. {#owner_envelope} walks the receiver's superclass chain to the first class whose own body, or one of
|
|
17
|
+
# whose project mixins, records the name. Every record at that level must agree — `include` and
|
|
18
|
+
# `prepend` share one table, so a mixin's definition may shadow the class's own — and none may be
|
|
19
|
+
# opaque. A call that fits this envelope is silent, and nothing below is consulted.
|
|
20
|
+
# 2. {#authoritative?} runs only for a call that would fire. It declines when a `method_missing` /
|
|
21
|
+
# `respond_to_missing?` hook or a dynamic-surface mark sits anywhere in the chain; when a mixin the
|
|
22
|
+
# project does not declare is not a known RBS module that lacks the name; when a `pre_eval:` patch or
|
|
23
|
+
# a plugin's synthetic method names it; when the owner does not make the method public; and when any
|
|
24
|
+
# project subclass of the receiver — whose instances the receiver's type admits — records a different
|
|
25
|
+
# or opaque envelope for it.
|
|
26
|
+
#
|
|
27
|
+
# Deliberately NOT in scope: a constructor reached through `Class#new` (nothing records `initialize` as
|
|
28
|
+
# `[:singleton, :new]`, so `.new` finds no owner unless the class writes `def self.new` itself); keyword
|
|
29
|
+
# arity (the envelope's required-keyword flag declines, as `compute_arity_envelope` declines an RBS
|
|
30
|
+
# function with required keywords); and a `class_eval` whose receiver is not a constant, which the
|
|
31
|
+
# discovery walk cannot name.
|
|
32
|
+
class SourceArity
|
|
33
|
+
MISSING_HOOKS = %i[method_missing respond_to_missing?].freeze
|
|
34
|
+
private_constant :MISSING_HOOKS
|
|
35
|
+
|
|
36
|
+
# One class on the walk: its own name, the project modules its mixins resolve to (transitively through
|
|
37
|
+
# their own `include`s), and the candidate-name lists of the mixins that resolve to no project module.
|
|
38
|
+
Level = Data.define(:class_name, :modules, :externals)
|
|
39
|
+
private_constant :Level
|
|
40
|
+
|
|
41
|
+
def initialize(scope, method_name, kind)
|
|
42
|
+
@scope = scope
|
|
43
|
+
@method_name = method_name.to_sym
|
|
44
|
+
@kind = kind
|
|
45
|
+
@name_memo = {}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The nearest definition's envelope, or nil when there is none or it is not one shape. Remembers the
|
|
49
|
+
# levels it walked for {#authoritative?}.
|
|
50
|
+
#
|
|
51
|
+
# Its ADR-46 reads are withheld ({Analysis::DependencyRecorder.withhold}) until the caller settles
|
|
52
|
+
# the verdict with {#settle_by_definitions} or {#settle_by_walk}.
|
|
53
|
+
def owner_envelope(class_name)
|
|
54
|
+
@owner_entries = []
|
|
55
|
+
@ambiguous = false
|
|
56
|
+
envelope, @withheld = Analysis::DependencyRecorder.withhold { walk_to_owner(class_name) }
|
|
57
|
+
envelope
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# True when the nearest level recorded the name but not as one envelope.
|
|
61
|
+
def ambiguous? = @ambiguous
|
|
62
|
+
|
|
63
|
+
# For a silent verdict that only a definition can overturn — the call fits, the owner takes a required
|
|
64
|
+
# keyword, or nothing on the chain records the name: the owner `def`s themselves (their symbol edges)
|
|
65
|
+
# and a definition appearing at a level the walk passed over (a name-keyed negative edge). A mixin or
|
|
66
|
+
# reopening added at the owner's level makes it opaque, and everything {#authoritative?} reads can only
|
|
67
|
+
# withhold, so none of these needs the file-level class edge the bucket reads would otherwise record.
|
|
68
|
+
def settle_by_definitions
|
|
69
|
+
return if @withheld.nil?
|
|
70
|
+
|
|
71
|
+
@owner_entries.each do |name, kind|
|
|
72
|
+
if kind == :singleton
|
|
73
|
+
@scope.user_singleton_def_site_for(name, @method_name)
|
|
74
|
+
else
|
|
75
|
+
@scope.user_def_site_for(name, @method_name)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
separator = @kind == :singleton ? "." : "#"
|
|
79
|
+
@passed.each { |name| Analysis::DependencyRecorder.read_missing(:method, "#{name}#{separator}#{@method_name}") }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# For every other verdict — a firing, or an opaque owner level whose opacity another file's edit can
|
|
83
|
+
# lift — the walk's reads are the dependency, so they are recorded as read.
|
|
84
|
+
def settle_by_walk
|
|
85
|
+
Analysis::DependencyRecorder.replay(@withheld) unless @withheld.nil?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Stage 2, for the class {#owner_envelope} last answered.
|
|
89
|
+
def authoritative?(class_name)
|
|
90
|
+
!load_order_dependent?(class_name) && !object_extension_may_shadow? &&
|
|
91
|
+
@levels.all? { |level| clean_level?(level) && public_at?(level.class_name) } &&
|
|
92
|
+
chain_free_of_hooks?(class_name) &&
|
|
93
|
+
subclasses_agree?(class_name, @envelope)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def walk_to_owner(class_name)
|
|
99
|
+
@levels = []
|
|
100
|
+
@passed = []
|
|
101
|
+
walked_whole_chain?(class_name) do |current|
|
|
102
|
+
level = level_for(current)
|
|
103
|
+
@levels << level
|
|
104
|
+
found = level_envelopes(level)
|
|
105
|
+
if found.empty?
|
|
106
|
+
@passed.concat([current] + level.modules)
|
|
107
|
+
next
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
envelope = found.first
|
|
111
|
+
@owner_entries = owner_entries(level)
|
|
112
|
+
unless found.all?(envelope) && !Source::ParameterEnvelope.opaque?(envelope)
|
|
113
|
+
@ambiguous = true
|
|
114
|
+
return nil
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
return @envelope = envelope
|
|
118
|
+
end
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def owner_entries(level)
|
|
123
|
+
own = @scope.parameter_envelopes_of(level.class_name).key?([@kind, @method_name])
|
|
124
|
+
entries = own ? [[level.class_name, @kind]] : []
|
|
125
|
+
level.modules.each do |mod|
|
|
126
|
+
entries << [mod, :instance] if @scope.parameter_envelopes_of(mod).key?([:instance, @method_name])
|
|
127
|
+
end
|
|
128
|
+
entries
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# The survey corpus's own lesson. `PStore.new(path).transaction { … }` inside `module TDiary::IO`
|
|
132
|
+
# resolves `PStore` to the project's `TDiary::IO::PStore` when that class's file is loaded, and to the
|
|
133
|
+
# stdlib `::PStore` when it is not — and tdiary loads it only under one `io_class` setting, so under
|
|
134
|
+
# the default one the call is correct code. The analysis universe holds every file at once, so it
|
|
135
|
+
# always picks the inner class. Whenever the receiver's class is one of SEVERAL known classes the
|
|
136
|
+
# call site's `Module.nesting` spells with the same last segment, which one the receiver is depends
|
|
137
|
+
# on load order, and a verdict against the project class's `def` is not one the program backs.
|
|
138
|
+
def load_order_dependent?(class_name)
|
|
139
|
+
segment = class_name.to_s.split("::").last
|
|
140
|
+
candidates = Rigor::Reflection.lexical_nesting_chain(@scope).map { |entry| "#{entry}::#{segment}" }
|
|
141
|
+
candidates << segment
|
|
142
|
+
known = candidates.uniq.select { |name| known_class?(name) }
|
|
143
|
+
known.include?(class_name.to_s) && known.size > 1
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# A module some method body hands to `obj.extend` sits ahead of the object's class, so if it defines
|
|
147
|
+
# the name at all, that object answers with the module's definition.
|
|
148
|
+
def object_extension_may_shadow?
|
|
149
|
+
@scope.discovered_parameter_envelopes.any? do |_name, bucket|
|
|
150
|
+
bucket.key?(Scope::DiscoveryIndex::ENVELOPE_OBJECT_EXTENDED_MARK) && bucket.key?([:instance, @method_name])
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def known_class?(name)
|
|
155
|
+
@scope.discovered_classes.key?(name) || Rigor::Reflection.rbs_class_known?(name, scope: @scope)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# False when the walk stopped at the ADR-41 budget rather than at the top of the chain: budget
|
|
159
|
+
# exhaustion is uncertainty, and every caller reads it as a reason to decline.
|
|
160
|
+
def walked_whole_chain?(class_name)
|
|
161
|
+
current = class_name.to_s
|
|
162
|
+
seen = {}
|
|
163
|
+
while current && !seen[current]
|
|
164
|
+
return false if seen.size >= Scope::ANCESTOR_WALK_LIMIT
|
|
165
|
+
|
|
166
|
+
seen[current] = true
|
|
167
|
+
yield current
|
|
168
|
+
current = resolve(current, @scope.superclass_of(current))
|
|
169
|
+
end
|
|
170
|
+
true
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def level_for(class_name)
|
|
174
|
+
modules = []
|
|
175
|
+
externals = []
|
|
176
|
+
collect_mixins(class_name, own_mixins(class_name), modules, externals, {})
|
|
177
|
+
Level.new(class_name: class_name, modules: modules, externals: externals)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Instance methods reach a receiver through `include` / `prepend`; class methods through `extend`, and
|
|
181
|
+
# an extended module's own `include`s reach the same singleton.
|
|
182
|
+
def own_mixins(class_name)
|
|
183
|
+
@kind == :singleton ? (@scope.discovered_extends[class_name] || []) : @scope.includes_of(class_name)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Issue #986 — a name the compact-header rename collision left ambiguous resolves to no ONE class,
|
|
187
|
+
# but BOTH classes it names are ancestors at runtime; only their MRO order is unknowable. Taking
|
|
188
|
+
# both as levels is what keeps this rule alive for the rest of the receiver: a method only one of
|
|
189
|
+
# them declares still has one envelope and still fires, a method they disagree about lands two on
|
|
190
|
+
# `#envelope_for`'s join and declines there, and the class's own `def`s, its superclass's and its
|
|
191
|
+
# unambiguous mixins are untouched. Letting the decline arrive as an unknown EXTERNAL mixin instead
|
|
192
|
+
# suppressed the rule for every level of the receiver.
|
|
193
|
+
def collect_mixins(owner, raw_names, modules, externals, seen)
|
|
194
|
+
raw_names.each do |raw|
|
|
195
|
+
resolved = resolve(owner, raw)
|
|
196
|
+
names = resolved ? [resolved] : @scope.ambiguous_ancestor_resolutions(owner, raw)
|
|
197
|
+
next externals << @scope.ancestor_name_candidates(owner, raw) if names.empty?
|
|
198
|
+
|
|
199
|
+
names.each do |name|
|
|
200
|
+
next if seen[name]
|
|
201
|
+
|
|
202
|
+
seen[name] = true
|
|
203
|
+
modules << name
|
|
204
|
+
collect_mixins(name, @scope.includes_of(name), modules, externals, seen)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def resolve(owner, raw)
|
|
210
|
+
return nil if raw.nil?
|
|
211
|
+
|
|
212
|
+
bucket = (@name_memo[owner] ||= {})
|
|
213
|
+
return bucket[raw] if bucket.key?(raw)
|
|
214
|
+
|
|
215
|
+
bucket[raw] = @scope.ancestor_name_candidates(owner, raw).find { |name| project_class?(name) }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def project_class?(name)
|
|
219
|
+
@scope.known_user_class?(name) || @scope.discovered_parameter_envelopes.key?(name)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# A module's methods reach the receiver as instance methods whichever side the receiver is on.
|
|
223
|
+
def level_envelopes(level)
|
|
224
|
+
own = @scope.parameter_envelopes_of(level.class_name)[[@kind, @method_name]]
|
|
225
|
+
mixed = level.modules.map { |mod| @scope.parameter_envelopes_of(mod)[[:instance, @method_name]] }
|
|
226
|
+
([own] + mixed).compact
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def clean_level?(level)
|
|
230
|
+
([level.class_name] + level.modules).none? { |name| dynamic_surface?(name) || project_patched?(name) } &&
|
|
231
|
+
level.externals.all? { |candidates| external_mixin_lacks_method?(candidates) }
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def dynamic_surface?(class_name)
|
|
235
|
+
Scope::DiscoveryIndex.rewritten_surface?(@scope.parameter_envelopes_of(class_name))
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def project_patched?(class_name)
|
|
239
|
+
environment = @scope.environment
|
|
240
|
+
patched = environment&.project_patched_methods
|
|
241
|
+
return true if patched && !patched.empty? && patch_names_method?(patched, class_name)
|
|
242
|
+
|
|
243
|
+
synthetic = environment&.synthetic_method_index
|
|
244
|
+
return false if synthetic.nil? || synthetic.empty?
|
|
245
|
+
|
|
246
|
+
synthetic.knows_class?(class_name) || !synthetic.lookup_instance(class_name, @method_name).empty? ||
|
|
247
|
+
!synthetic.lookup_singleton(class_name, @method_name).empty?
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def patch_names_method?(patched, class_name)
|
|
251
|
+
%i[instance singleton].any? do |kind|
|
|
252
|
+
!patched.lookup(class_name: class_name, method_name: @method_name, kind: kind).nil?
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# A mixin the project does not declare may define the name with any arity, and `prepend` lets it win
|
|
257
|
+
# over the class's own `def`. Only a module RBS knows, and whose declaration lacks the name, is
|
|
258
|
+
# evidence that it does not.
|
|
259
|
+
def external_mixin_lacks_method?(candidates)
|
|
260
|
+
name = candidates.find { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: @scope) }
|
|
261
|
+
return false if name.nil?
|
|
262
|
+
|
|
263
|
+
Rigor::Reflection.instance_method_definition(name, @method_name, scope: @scope).nil?
|
|
264
|
+
rescue StandardError
|
|
265
|
+
false
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def public_at?(class_name)
|
|
269
|
+
visibility = @scope.discovered_method_visibility(class_name, @method_name)
|
|
270
|
+
visibility.nil? || visibility == :public
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# A hook anywhere in the chain, including above the owner, and on either side: a class that answers
|
|
274
|
+
# names it does not define is a class whose `def`s are not the whole story of what a call reaches.
|
|
275
|
+
def chain_free_of_hooks?(class_name)
|
|
276
|
+
walked_whole_chain?(class_name) do |current|
|
|
277
|
+
([current] + level_for(current).modules).each do |name|
|
|
278
|
+
envelopes = @scope.parameter_envelopes_of(name)
|
|
279
|
+
return false if dynamic_surface?(name)
|
|
280
|
+
return false if MISSING_HOOKS.any? { |hook| hook_recorded?(envelopes, hook) }
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def hook_recorded?(envelopes, hook)
|
|
286
|
+
envelopes.key?([:instance, hook]) || envelopes.key?([:singleton, hook])
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Every project class whose superclass chain reaches the receiver's class: a value typed as the
|
|
290
|
+
# receiver may be any of them, and each one's own definition of the name is what runs for it.
|
|
291
|
+
def subclasses_agree?(class_name, envelope)
|
|
292
|
+
each_subclass(class_name) do |subclass|
|
|
293
|
+
level = level_for(subclass)
|
|
294
|
+
return false unless clean_level?(level)
|
|
295
|
+
return false unless level_envelopes(level).all?(envelope)
|
|
296
|
+
end
|
|
297
|
+
true
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def each_subclass(class_name)
|
|
301
|
+
children = children_by_parent
|
|
302
|
+
queue = children.fetch(class_name.to_s, []).dup
|
|
303
|
+
seen = {}
|
|
304
|
+
until queue.empty?
|
|
305
|
+
subclass = queue.shift
|
|
306
|
+
next if seen[subclass]
|
|
307
|
+
|
|
308
|
+
seen[subclass] = true
|
|
309
|
+
yield subclass
|
|
310
|
+
queue.concat(children.fetch(subclass, []))
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def children_by_parent
|
|
315
|
+
@scope.discovered_superclasses.each_with_object({}) do |(child, raw_parent), children|
|
|
316
|
+
parent = resolve(child, raw_parent)
|
|
317
|
+
(children[parent] ||= []) << child if parent
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|