rigortype 0.3.8 → 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/capability_roles/capability_roles.rbs +36 -0
- 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 +236 -17
- data/docs/handbook/02-everyday-types.md +17 -14
- data/docs/handbook/03-narrowing.md +132 -14
- data/docs/handbook/04-tuples-and-shapes.md +8 -6
- data/docs/handbook/05-methods-and-blocks.md +1 -1
- data/docs/handbook/06-classes.md +2 -2
- data/docs/handbook/07-rbs-and-extended.md +19 -1
- data/docs/handbook/10-sorbet.md +9 -10
- data/docs/handbook/11-sig-gen.md +525 -13
- data/docs/handbook/README.md +1 -1
- data/docs/handbook/appendix-elixir.md +2 -2
- data/docs/handbook/appendix-go.md +2 -2
- data/docs/handbook/appendix-java-csharp.md +2 -2
- data/docs/handbook/appendix-mypy.md +2 -2
- data/docs/handbook/appendix-phpstan.md +1 -1
- data/docs/handbook/appendix-rust.md +1 -1
- data/docs/handbook/appendix-type-theory.md +4 -4
- data/docs/handbook/appendix-typescript.md +1 -1
- data/docs/llms.txt +2 -0
- data/docs/manual/02-cli-reference.md +95 -6
- data/docs/manual/03-configuration.md +18 -0
- data/docs/manual/04-diagnostics.md +15 -0
- data/docs/manual/07-plugins.md +15 -5
- data/docs/manual/08-skills.md +23 -2
- data/docs/manual/10-mcp-server.md +3 -2
- data/docs/manual/11-ci.md +9 -0
- data/docs/manual/15-type-protection-coverage.md +8 -0
- data/docs/manual/16-rbs-extended-annotations.md +46 -8
- data/docs/manual/18-removing-dead-code.md +10 -8
- 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 +261 -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 +138 -4
- data/docs/manual/plugins/rigor-activestorage.md +8 -1
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +43 -7
- 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 +229 -1
- 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 +3 -3
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +1 -5
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +10 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +6 -4
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
- data/lib/rigor/analysis/check_rules/rule_ids.rb +21 -6
- data/lib/rigor/analysis/check_rules/rule_walk.rb +41 -5
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +1 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +1 -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 +38 -4
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
- data/lib/rigor/analysis/check_rules.rb +718 -79
- data/lib/rigor/analysis/crash_signature.rb +8 -24
- data/lib/rigor/analysis/dependency_recorder.rb +23 -0
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +0 -2
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +0 -2
- data/lib/rigor/analysis/dependency_source_inference/index.rb +5 -5
- data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +1 -2
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
- data/lib/rigor/analysis/effects_cache_probe.rb +3 -4
- data/lib/rigor/analysis/erb_template_detector.rb +1 -2
- data/lib/rigor/analysis/fact_store.rb +15 -1
- data/lib/rigor/analysis/incremental.rb +42 -0
- data/lib/rigor/analysis/incremental_session.rb +117 -26
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
- data/lib/rigor/analysis/project_scan.rb +11 -1
- data/lib/rigor/analysis/reachability/graph.rb +3 -5
- data/lib/rigor/analysis/reachability/plugin_roots.rb +5 -5
- data/lib/rigor/analysis/reachability/project_files.rb +2 -2
- data/lib/rigor/analysis/reachability/scan.rb +5 -5
- data/lib/rigor/analysis/reachability/scan_cache.rb +3 -2
- data/lib/rigor/analysis/reachability/signature_scan.rb +2 -2
- data/lib/rigor/analysis/result.rb +1 -3
- data/lib/rigor/analysis/rule_catalog.rb +131 -1
- data/lib/rigor/analysis/run_cache_key.rb +21 -12
- data/lib/rigor/analysis/run_cache_probe.rb +3 -5
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -8
- data/lib/rigor/analysis/runner/declaration_position.rb +1 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +340 -33
- data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +18 -14
- data/lib/rigor/analysis/runner/pool_coordinator.rb +357 -97
- data/lib/rigor/analysis/runner/project_pre_passes.rb +38 -13
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +407 -51
- 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 +62 -20
- data/lib/rigor/bleeding_edge.rb +10 -24
- data/lib/rigor/builtins/hkt_builtins.rb +3 -2
- data/lib/rigor/builtins/imported_refinements.rb +159 -21
- data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
- data/lib/rigor/builtins/regex_refinement.rb +21 -13
- data/lib/rigor/builtins/static_return_refinements.rb +7 -6
- data/lib/rigor/cache/annotation_location.rb +2 -4
- data/lib/rigor/cache/descriptor.rb +63 -20
- data/lib/rigor/cache/engine_source.rb +33 -5
- data/lib/rigor/cache/file_digest.rb +9 -1
- data/lib/rigor/cache/incremental_snapshot.rb +72 -6
- data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +0 -3
- data/lib/rigor/cache/rbs_class_type_param_names.rb +0 -3
- data/lib/rigor/cache/rbs_constant_table.rb +0 -3
- data/lib/rigor/cache/rbs_descriptor.rb +112 -14
- data/lib/rigor/cache/rbs_environment.rb +8 -4
- data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
- data/lib/rigor/cache/store.rb +76 -32
- data/lib/rigor/ci_detector.rb +1 -0
- data/lib/rigor/cli/annotate_command.rb +5 -6
- data/lib/rigor/cli/check_command.rb +57 -10
- data/lib/rigor/cli/check_invocation.rb +6 -11
- data/lib/rigor/cli/check_runner_factory.rb +1 -5
- data/lib/rigor/cli/coverage_command.rb +1 -1
- data/lib/rigor/cli/coverage_mutation.rb +12 -3
- data/lib/rigor/cli/coverage_scan.rb +1 -5
- data/lib/rigor/cli/diff_command.rb +1 -1
- data/lib/rigor/cli/doc_links.rb +4 -4
- data/lib/rigor/cli/docs_command.rb +6 -6
- data/lib/rigor/cli/doctor_command.rb +85 -38
- data/lib/rigor/cli/effects_command.rb +2 -2
- data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
- data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +34 -1
- data/lib/rigor/cli/fused_protection_report.rb +7 -1
- data/lib/rigor/cli/lsp_command.rb +1 -1
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
- data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
- data/lib/rigor/cli/mutation_protection_report.rb +7 -1
- data/lib/rigor/cli/plugin_command.rb +4 -4
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/prism_colorizer.rb +1 -2
- data/lib/rigor/cli/protection_fork_scan.rb +6 -6
- data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
- data/lib/rigor/cli/sig_gen_command.rb +212 -31
- data/lib/rigor/cli/skill_command.rb +2 -2
- data/lib/rigor/cli/skill_deep_probe.rb +4 -4
- data/lib/rigor/cli/skill_describe.rb +35 -35
- data/lib/rigor/cli/trace_command.rb +4 -4
- data/lib/rigor/cli/trace_renderer.rb +5 -6
- data/lib/rigor/cli/triage_command.rb +1 -1
- data/lib/rigor/cli/type_of_command.rb +29 -11
- 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/type_scan_command.rb +4 -4
- data/lib/rigor/cli/unused_command.rb +11 -3
- data/lib/rigor/cli/upgrade_command.rb +1 -1
- data/lib/rigor/cli.rb +89 -9
- data/lib/rigor/config_audit.rb +30 -5
- data/lib/rigor/configuration/severity_profile.rb +25 -9
- data/lib/rigor/configuration.rb +109 -7
- data/lib/rigor/effects/ancestry_recorder.rb +191 -0
- data/lib/rigor/effects/attribution.rb +12 -5
- 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 +15 -10
- data/lib/rigor/effects/definition_context.rb +179 -0
- data/lib/rigor/effects/definition_lines.rb +25 -6
- data/lib/rigor/effects/effect_table.rb +11 -4
- data/lib/rigor/effects/entry_points.rb +0 -2
- data/lib/rigor/effects/envelope_check.rb +9 -9
- data/lib/rigor/effects/envelope_index.rb +20 -8
- data/lib/rigor/effects/file_collection.rb +59 -5
- data/lib/rigor/effects/framework_units.rb +5 -7
- data/lib/rigor/effects/identity.rb +20 -6
- data/lib/rigor/effects/inline_anchor.rb +7 -7
- data/lib/rigor/effects/label_intent.rb +3 -4
- data/lib/rigor/effects/liskov_check.rb +8 -8
- data/lib/rigor/effects/local_ownership.rb +38 -12
- data/lib/rigor/effects/method_key.rb +22 -1
- data/lib/rigor/effects/mutation_classifier.rb +23 -12
- data/lib/rigor/effects/plugin_facts.rb +47 -37
- data/lib/rigor/effects/propagator.rb +297 -19
- data/lib/rigor/effects/registry.rb +1 -1
- data/lib/rigor/effects/scanner.rb +122 -73
- data/lib/rigor/effects/signature_sources.rb +4 -6
- data/lib/rigor/effects/snapshot.rb +12 -11
- data/lib/rigor/effects/snapshot_diff.rb +1 -1
- data/lib/rigor/effects/summary.rb +27 -4
- data/lib/rigor/effects/unit_scan.rb +393 -38
- data/lib/rigor/effects/unknown_label_check.rb +3 -8
- data/lib/rigor/effects/unknown_label_report.rb +3 -4
- data/lib/rigor/effects/visibility.rb +101 -0
- data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
- data/lib/rigor/environment/class_registry.rb +3 -1
- data/lib/rigor/environment/failure_slot.rb +2 -2
- data/lib/rigor/environment/installed_gem_set.rb +85 -0
- data/lib/rigor/environment/lockfile_resolver.rb +68 -4
- data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
- data/lib/rigor/environment/member_consistency.rb +298 -0
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
- data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
- data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
- data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
- data/lib/rigor/environment/rbs_loader.rb +674 -79
- data/lib/rigor/environment.rb +174 -24
- data/lib/rigor/flow_contribution/merger.rb +0 -2
- data/lib/rigor/flow_contribution.rb +11 -13
- data/lib/rigor/hashing/xxh3.rb +264 -0
- data/lib/rigor/inference/acceptance.rb +292 -17
- 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 +75 -30
- data/lib/rigor/inference/block_repetition.rb +71 -0
- data/lib/rigor/inference/body_fixpoint.rb +7 -6
- data/lib/rigor/inference/budget_trace.rb +3 -6
- 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 +389 -18
- data/lib/rigor/inference/closure_escape_analyzer.rb +159 -17
- data/lib/rigor/inference/content_join.rb +200 -27
- data/lib/rigor/inference/coverage_scanner.rb +4 -6
- 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/dynamic_origin.rb +1 -1
- data/lib/rigor/inference/element_read_widening.rb +180 -0
- data/lib/rigor/inference/error_info.rb +196 -0
- data/lib/rigor/inference/expression_typer.rb +2124 -468
- data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
- data/lib/rigor/inference/fork_map.rb +5 -7
- 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/hkt_reducer.rb +2 -3
- data/lib/rigor/inference/hkt_registry.rb +4 -7
- data/lib/rigor/inference/index_write_widening.rb +16 -7
- 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 +168 -21
- 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 +87 -33
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +165 -111
- 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/random_folding.rb +82 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +1046 -69
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +129 -54
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -6
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +225 -25
- data/lib/rigor/inference/method_parameter_binder.rb +12 -9
- data/lib/rigor/inference/multi_target_binder.rb +341 -55
- data/lib/rigor/inference/mutation_rejoin.rb +171 -0
- data/lib/rigor/inference/mutation_widening.rb +156 -85
- data/lib/rigor/inference/narrowing.rb +835 -162
- 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 +153 -16
- data/lib/rigor/inference/origin_lookup.rb +2 -3
- data/lib/rigor/inference/parameter_inference_collector.rb +7 -7
- data/lib/rigor/inference/pre_eval_constants.rb +6 -5
- data/lib/rigor/inference/precision_scanner.rb +3 -4
- data/lib/rigor/inference/project_method_ownership.rb +136 -0
- data/lib/rigor/inference/project_patched_methods.rb +9 -4
- data/lib/rigor/inference/project_patched_scanner.rb +10 -6
- data/lib/rigor/inference/protection_scanner.rb +1 -2
- data/lib/rigor/inference/range_constant.rb +57 -0
- data/lib/rigor/inference/rbs_type_translator.rb +49 -11
- data/lib/rigor/inference/receiver_alias.rb +93 -4
- data/lib/rigor/inference/receiver_blind_block.rb +219 -0
- data/lib/rigor/inference/refinement_mutation.rb +76 -0
- 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 +5228 -627
- data/lib/rigor/inference/statement_evaluator.rb +3452 -595
- data/lib/rigor/inference/stored_block_call.rb +54 -0
- data/lib/rigor/inference/string_mutation.rb +97 -0
- data/lib/rigor/inference/struct_fold_safety.rb +6 -7
- data/lib/rigor/inference/synthetic_method_index.rb +1 -2
- data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
- data/lib/rigor/inference/unknown_store_widening.rb +200 -0
- data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
- data/lib/rigor/inference/version_guard.rb +28 -21
- data/lib/rigor/inference/void_origin.rb +3 -3
- data/lib/rigor/inference/void_tail_summary.rb +2 -4
- data/lib/rigor/language_server/buffer_table.rb +4 -4
- data/lib/rigor/language_server/completion_provider.rb +1 -1
- data/lib/rigor/language_server/debouncer.rb +0 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +7 -6
- data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
- data/lib/rigor/language_server/folding_range_provider.rb +1 -1
- data/lib/rigor/language_server/hover_provider.rb +1 -1
- data/lib/rigor/language_server/hover_renderer.rb +4 -4
- data/lib/rigor/language_server/incremental_sync.rb +6 -6
- data/lib/rigor/language_server/project_context.rb +7 -8
- data/lib/rigor/language_server/publish_batcher.rb +2 -2
- data/lib/rigor/language_server/selection_range_provider.rb +2 -2
- data/lib/rigor/language_server/server.rb +9 -9
- data/lib/rigor/language_server/signature_help_provider.rb +1 -1
- data/lib/rigor/language_server/uri.rb +1 -1
- data/lib/rigor/mcp/server.rb +2 -1
- data/lib/rigor/plugin/base.rb +258 -26
- data/lib/rigor/plugin/box_probe.rb +91 -0
- data/lib/rigor/plugin/bundled_catalog.rb +167 -0
- data/lib/rigor/plugin/effect_attribution.rb +65 -11
- data/lib/rigor/plugin/effect_entry_points.rb +3 -3
- data/lib/rigor/plugin/fact_store.rb +6 -6
- data/lib/rigor/plugin/io_boundary.rb +93 -8
- data/lib/rigor/plugin/isolation.rb +29 -6
- data/lib/rigor/plugin/loader.rb +33 -13
- data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
- data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
- data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
- data/lib/rigor/plugin/manifest.rb +94 -11
- data/lib/rigor/plugin/registry.rb +76 -12
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +21 -4
- data/lib/rigor/plugin/template_unit.rb +196 -0
- data/lib/rigor/plugin/trust_policy.rb +2 -4
- data/lib/rigor/plugin/type_node_resolver.rb +3 -3
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/plugin_gap_advisory.rb +96 -0
- data/lib/rigor/project_environment.rb +138 -0
- data/lib/rigor/protection/analysis_guard.rb +93 -14
- data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
- data/lib/rigor/protection/dependency_closure.rb +5 -8
- data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
- data/lib/rigor/protection/discovery_seed.rb +14 -12
- data/lib/rigor/protection/kill_signature.rb +1 -4
- data/lib/rigor/protection/measurement_integrity.rb +1 -3
- data/lib/rigor/protection/mutation_cache.rb +7 -11
- data/lib/rigor/protection/mutation_scanner.rb +12 -15
- data/lib/rigor/protection/mutator.rb +2 -1
- data/lib/rigor/protection/test_suite_oracle.rb +5 -5
- data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
- data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
- data/lib/rigor/rbs_extended/reporter.rb +36 -10
- data/lib/rigor/rbs_extended.rb +88 -21
- data/lib/rigor/reflection/constant_ancestors.rb +97 -0
- data/lib/rigor/reflection/constant_path.rb +139 -0
- data/lib/rigor/reflection.rb +93 -110
- data/lib/rigor/runtime/jit.rb +6 -8
- data/lib/rigor/scope/discovery_index.rb +165 -2
- data/lib/rigor/scope.rb +1041 -32
- 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 +673 -53
- data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
- data/lib/rigor/sig_gen/layout_index.rb +3 -4
- data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
- data/lib/rigor/sig_gen/method_candidate.rb +47 -3
- data/lib/rigor/sig_gen/observation_collector.rb +18 -16
- data/lib/rigor/sig_gen/path_mapper.rb +4 -6
- data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
- data/lib/rigor/sig_gen/renderer.rb +128 -14
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +154 -0
- data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
- data/lib/rigor/sig_gen/write_result.rb +19 -3
- data/lib/rigor/sig_gen/writer.rb +189 -41
- data/lib/rigor/sig_gen.rb +3 -0
- data/lib/rigor/signature_path_audit.rb +153 -6
- data/lib/rigor/source/literals.rb +0 -23
- data/lib/rigor/source/node_children.rb +0 -2
- data/lib/rigor/source/node_locator.rb +5 -11
- data/lib/rigor/source/node_walker.rb +0 -6
- data/lib/rigor/source/parameter_envelope.rb +72 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/triage/catalogue.rb +1 -3
- data/lib/rigor/triage.rb +3 -5
- data/lib/rigor/type/accepts_result.rb +24 -6
- data/lib/rigor/type/combinator.rb +144 -5
- data/lib/rigor/type/data_class.rb +2 -2
- data/lib/rigor/type/data_instance.rb +4 -4
- data/lib/rigor/type/difference.rb +1 -0
- data/lib/rigor/type/float_range.rb +128 -0
- data/lib/rigor/type/hash_shape.rb +6 -6
- data/lib/rigor/type/integer_range.rb +13 -8
- data/lib/rigor/type/nominal.rb +8 -2
- data/lib/rigor/type/refined.rb +4 -3
- data/lib/rigor/type/struct_class.rb +3 -3
- data/lib/rigor/type/struct_instance.rb +4 -4
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/generic.rb +1 -1
- data/lib/rigor/type_node/range_literal.rb +25 -0
- data/lib/rigor/type_node/resolver_chain.rb +1 -1
- data/lib/rigor/type_node.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +10 -5
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +14 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +32 -30
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
- 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 +264 -3
- data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
- 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/analyzer.rb +0 -4
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +2 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +10 -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 +383 -7
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +322 -41
- data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +26 -14
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +290 -19
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +3 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +2 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
- data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
- 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/type_scanner.rb +8 -6
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +78 -5
- data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -3
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +12 -7
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +36 -37
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -10
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +5 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +3 -2
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +10 -17
- data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
- data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
- 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 +324 -18
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
- data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +2 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +10 -5
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +3 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +51 -6
- data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -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/analysis/reachability/scan_cache.rbs +12 -0
- data/sig/rigor/cache.rbs +2 -3
- 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 +17 -10
- data/sig/rigor/inference/optimistic_origin.rbs +10 -0
- data/sig/rigor/inference.rbs +9 -14
- 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 +4 -0
- data/sig/rigor/plugin/loader.rbs +3 -3
- data/sig/rigor/plugin/manifest.rbs +51 -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 +9 -6
- data/sig/rigor/scope.rbs +148 -12
- data/sig/rigor/sig_gen/skip_reason_catalog.rbs +21 -0
- data/sig/rigor/source.rbs +4 -4
- data/sig/rigor/testing.rbs +10 -4
- data/sig/rigor/type.rbs +33 -1
- data/sig/rigor.rbs +39 -20
- data/skills/rigor-ask/SKILL.md +16 -11
- 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 +25 -27
- 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 +15 -10
- data/skills/rigor-project-init/references/01-detect.md +13 -9
- data/skills/rigor-project-init/references/02-configure.md +43 -14
- 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-project-init/references/06-agent-contract.md +67 -0
- 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 +226 -0
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +265 -0
- data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
- data/skills/rigor-type-oracle/references/03-gap-protocol.md +125 -0
- data/skills/rigor-unused-adjudicate/SKILL.md +12 -7
- data/skills/rigor-upgrade/SKILL.md +13 -8
- metadata +133 -3
- data/lib/rigor/cli/probe_environment.rb +0 -85
- data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
|
@@ -4,6 +4,7 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../inference/mutation_widening"
|
|
6
6
|
require_relative "label_set"
|
|
7
|
+
require_relative "local_ownership"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module Effects
|
|
@@ -18,7 +19,9 @@ module Rigor
|
|
|
18
19
|
# proven lane is worse than a missing one — the proven lane is the one a verdict may read (ADR-5).
|
|
19
20
|
# 2. **Who owns the receiver?** `self` and its ivars are `mutate.self` (`mutate.static` in singleton
|
|
20
21
|
# context), a class variable is `mutate.static`, a parameter is `mutate.instance`, a frame-owned
|
|
21
|
-
# local is `mutate.local
|
|
22
|
+
# local is `mutate.local`, and so is a receiver that is itself an allocation (`{}.compare_by_identity`,
|
|
23
|
+
# `raw.dup.force_encoding(e)`) by the local rule's own witness ({LocalOwnership.allocation?}): each
|
|
24
|
+
# evaluation is a new object no caller holds yet. **Anything else answers nil**, and the caller records an
|
|
22
25
|
# `unknown-ownership` taint rather than a proven bare `mutate`: Ruby's ownership is a dataflow
|
|
23
26
|
# question, and a proven parent label on a fresh-but-unproven receiver would put findings on correct
|
|
24
27
|
# code (WD14).
|
|
@@ -26,18 +29,23 @@ module Rigor
|
|
|
26
29
|
# The only selectors a mutation may be claimed from without knowing the receiver's class.
|
|
27
30
|
UNIVERSAL_MUTATORS = %i[[]=].to_set.freeze
|
|
28
31
|
|
|
29
|
-
# `String`'s receiver-mutating surface. `Array` / `Hash` reuse the hand-audited sets the widening
|
|
30
|
-
# rules already maintain, cited rather than re-derived (ADR-103 WD3).
|
|
31
|
-
STRING_MUTATORS = %i[
|
|
32
|
-
<< concat replace insert prepend clear
|
|
33
|
-
upcase! downcase! capitalize! swapcase! reverse!
|
|
34
|
-
strip! lstrip! rstrip! chomp! chop! squeeze! succ! next!
|
|
35
|
-
sub! gsub! tr! tr_s! delete! slice! []=
|
|
36
|
-
].to_set.freeze
|
|
37
|
-
|
|
38
32
|
# `foo=`, and deliberately not `==` / `<=` / `!=` / `===`.
|
|
39
33
|
ATTRIBUTE_WRITER = /\A[a-z_][A-Za-z0-9_]*=\z/
|
|
40
34
|
|
|
35
|
+
# Every Hash method that mutates its receiver. The widening keeps two tables because they answer two different
|
|
36
|
+
# questions of a `HashShape` — `MutationWidening::HASH_MUTATORS` changes the pair set,
|
|
37
|
+
# `HashLookupMutation::MUTATORS` changes what a read of the pairs answers — but either is a write to the
|
|
38
|
+
# receiver, and that is all this asks: `h.compare_by_identity` raises `FrozenError` on a frozen hash as
|
|
39
|
+
# `h.clear` does (unless the hash already compares by identity, when it returns before the check), and a
|
|
40
|
+
# method whose one effect was `@h.compare_by_identity` did not read as mutating while this cited the first
|
|
41
|
+
# table alone.
|
|
42
|
+
#
|
|
43
|
+
# `rehash` is in neither table because it changes no read a shape can state: a shape's keys are literals, which
|
|
44
|
+
# a rebuild never merges. It still rebuilds the receiver's table in place, and it is the one name spelt here.
|
|
45
|
+
HASH_MUTATORS = (
|
|
46
|
+
Inference::MutationWidening::HASH_MUTATORS | Inference::HashLookupMutation::MUTATORS | Set[:rehash]
|
|
47
|
+
).freeze
|
|
48
|
+
|
|
41
49
|
LABELS = {
|
|
42
50
|
self_state: LabelSet.new(["mutate.self"]),
|
|
43
51
|
static: LabelSet.new(["mutate.static"]),
|
|
@@ -57,10 +65,12 @@ module Rigor
|
|
|
57
65
|
name = node.name
|
|
58
66
|
return true if UNIVERSAL_MUTATORS.include?(name) || ATTRIBUTE_WRITER.match?(name.to_s)
|
|
59
67
|
|
|
68
|
+
# The per-class sets are the hand-audited ones the widening rules maintain, cited rather than re-derived
|
|
69
|
+
# (ADR-103 WD3): a list kept here drifted from the widening's, and missed `force_encoding`.
|
|
60
70
|
case receiver_class
|
|
61
71
|
when "Array" then Inference::MutationWidening::ARRAY_MUTATORS.include?(name)
|
|
62
|
-
when "Hash" then
|
|
63
|
-
when "String" then
|
|
72
|
+
when "Hash" then HASH_MUTATORS.include?(name)
|
|
73
|
+
when "String" then Inference::StringMutation::MUTATORS.include?(name)
|
|
64
74
|
else false
|
|
65
75
|
end
|
|
66
76
|
end
|
|
@@ -78,6 +88,7 @@ module Rigor
|
|
|
78
88
|
@singleton ? :static : :self_state
|
|
79
89
|
when Prism::ClassVariableReadNode then :static
|
|
80
90
|
when Prism::LocalVariableReadNode then local_ownership(receiver.name.to_s)
|
|
91
|
+
else :local if LocalOwnership.allocation?(receiver, singleton: @singleton)
|
|
81
92
|
end
|
|
82
93
|
end
|
|
83
94
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "digest"
|
|
4
4
|
|
|
5
|
+
require_relative "callee_rule"
|
|
5
6
|
require_relative "entry_points"
|
|
6
7
|
require_relative "label_set"
|
|
7
8
|
require_relative "registry"
|
|
@@ -65,7 +66,14 @@ module Rigor
|
|
|
65
66
|
# `discharge` is the RESOLVED grant, not the manifest's request: {.build} has already demoted a
|
|
66
67
|
# non-first-party plugin's `true` to `false` and recorded a warning, so nothing downstream has to
|
|
67
68
|
# re-ask who may discharge.
|
|
68
|
-
|
|
69
|
+
# `callee` names a {CalleeRule} rule (#1048) and `responds` marks a row whose call supplies the
|
|
70
|
+
# unit's answer, so a unit rule on the same receiver stands down. `receiver` is carried because a
|
|
71
|
+
# UNIT rule has no call node to read one off. `callee_fallbacks` is the plugin's lookup-order table
|
|
72
|
+
# for the rule's answer (#1065), carried verbatim to the edge the rule records.
|
|
73
|
+
Row = Data.define(:key, :labels, :narrow, :discharge, :within, :taint, :plugin_id, :receiver,
|
|
74
|
+
:callee, :callee_fallbacks, :responds) do
|
|
75
|
+
def initialize(receiver: nil, callee: nil, callee_fallbacks: nil, responds: false, **) = super
|
|
76
|
+
|
|
69
77
|
def discharge? = discharge
|
|
70
78
|
end
|
|
71
79
|
|
|
@@ -73,15 +81,18 @@ module Rigor
|
|
|
73
81
|
# plugin pointed.
|
|
74
82
|
Edge = Data.define(:target, :receiver, :selector, :plugin_id)
|
|
75
83
|
|
|
84
|
+
# The rows whose `callee:` names a {CalleeRule::UNIT_RULES} rule — applied once per unit by
|
|
85
|
+
# {UnitScan}, from the unit's own owner and key rather than from a call node.
|
|
86
|
+
attr_reader :unit_callee_rows
|
|
87
|
+
|
|
76
88
|
def self.empty
|
|
77
89
|
@empty ||= new(contributions: [], superclasses: NO_ROWS, includes: NO_ROWS)
|
|
78
90
|
end
|
|
79
91
|
|
|
80
|
-
# @param
|
|
81
|
-
# @param superclasses [Hash{String=>String,Array<String>}] the project's as-written superclass table
|
|
92
|
+
# @param superclasses — the project's as-written superclass table
|
|
82
93
|
# (`Scope::DiscoveryIndex#discovered_superclasses`). Empty is legal and simply means no row matches
|
|
83
94
|
# through inheritance.
|
|
84
|
-
# @param includes
|
|
95
|
+
# @param includes — the project's as-written include / prepend table
|
|
85
96
|
# (`Scope::DiscoveryIndex#discovered_includes`), for the frameworks whose contract is a module
|
|
86
97
|
# rather than a base class (#456).
|
|
87
98
|
def self.build(plugin_registry, superclasses: NO_ROWS, includes: NO_ROWS)
|
|
@@ -111,6 +122,7 @@ module Rigor
|
|
|
111
122
|
@self_rows = {}
|
|
112
123
|
@result_rows = {}
|
|
113
124
|
@edges = []
|
|
125
|
+
@unit_callee_rows = []
|
|
114
126
|
@labels_by_owner = {}
|
|
115
127
|
@entry_points = []
|
|
116
128
|
@declared_ancestry = {}
|
|
@@ -144,13 +156,10 @@ module Rigor
|
|
|
144
156
|
|
|
145
157
|
# The row colouring `owner`'s `selector`, found on `owner` itself or on a project ancestor of it.
|
|
146
158
|
#
|
|
147
|
-
# @param owner
|
|
148
|
-
# @param singleton
|
|
149
|
-
# @return [Row, nil]
|
|
159
|
+
# @param owner — the receiver's class name as the syntax or the typer named it
|
|
160
|
+
# @param singleton — whether the call is `Owner.selector`
|
|
150
161
|
def class_row(owner, singleton, selector)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
bucket = @class_rows[singleton]
|
|
162
|
+
bucket = owner.nil? ? nil : @class_rows[singleton]
|
|
154
163
|
return nil if bucket.nil?
|
|
155
164
|
|
|
156
165
|
ancestry(owner).each do |candidate|
|
|
@@ -163,9 +172,7 @@ module Rigor
|
|
|
163
172
|
# The row colouring `path`'s `selector`, where `path` is a receiver expression (`"Rails.cache"`).
|
|
164
173
|
# Exact — a receiver path names one object and has no ancestry to walk.
|
|
165
174
|
def path_row(path, selector)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
@path_rows[path]&.[](selector)
|
|
175
|
+
path.nil? ? nil : @path_rows[path]&.[](selector)
|
|
169
176
|
end
|
|
170
177
|
|
|
171
178
|
# The row colouring `path`'s `selector` for a receiver rooted at implicit self (`"self.flash.now"`),
|
|
@@ -173,11 +180,8 @@ module Rigor
|
|
|
173
180
|
# `owner_class`'s project ancestry — a receiver-less `session` outside a controller is a different
|
|
174
181
|
# `session`.
|
|
175
182
|
def self_path_row(path, selector, owner_class)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
row = @self_rows[path]&.[](selector)
|
|
179
|
-
return nil if row.nil?
|
|
180
|
-
return nil unless descends_from?(owner_class, row.within)
|
|
183
|
+
row = path.nil? ? nil : @self_rows[path]&.[](selector)
|
|
184
|
+
return nil if row.nil? || !descends_from?(owner_class, row.within)
|
|
181
185
|
|
|
182
186
|
row
|
|
183
187
|
end
|
|
@@ -203,9 +207,7 @@ module Rigor
|
|
|
203
207
|
|
|
204
208
|
# Whether `class_name` is `ancestor`, or reaches it through the project's own `class … <` lines.
|
|
205
209
|
def descends_from?(class_name, ancestor)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
ancestry(class_name).include?(ancestor)
|
|
210
|
+
!class_name.nil? && ancestry(class_name).include?(ancestor)
|
|
209
211
|
end
|
|
210
212
|
|
|
211
213
|
# `registry` extended with every plugin's `effect_labels:`, each under its own owner so
|
|
@@ -227,10 +229,10 @@ module Rigor
|
|
|
227
229
|
note_root_demotion(contribution)
|
|
228
230
|
(@labels_by_owner[contribution.owner] ||= []).concat(contribution.labels)
|
|
229
231
|
contribution.attributions.each { |entry| absorb_attribution(contribution, entry) }
|
|
230
|
-
contribution.edges.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
end
|
|
232
|
+
@edges.concat(contribution.edges.map do |entry|
|
|
233
|
+
Edge.new(target: entry.target, receiver: entry.receiver, selector: entry.method,
|
|
234
|
+
plugin_id: contribution.id)
|
|
235
|
+
end)
|
|
234
236
|
@entry_points.concat(contribution.entry_points)
|
|
235
237
|
contribution.ancestry.each { |entry| absorb_ancestry(contribution, entry) }
|
|
236
238
|
end
|
|
@@ -269,8 +271,22 @@ module Rigor
|
|
|
269
271
|
def absorb_attribution(contribution, entry)
|
|
270
272
|
row = Row.new(key: entry.key, labels: LabelSet.new(entry.labels), narrow: entry.narrow,
|
|
271
273
|
discharge: discharge_granted?(contribution, entry), within: entry.within,
|
|
272
|
-
taint: entry.taint, plugin_id: contribution.id
|
|
274
|
+
taint: entry.taint, plugin_id: contribution.id, receiver: entry.receiver,
|
|
275
|
+
callee: callee_rule_for(contribution, entry), responds: entry.responds,
|
|
276
|
+
callee_fallbacks: entry.callee_fallbacks)
|
|
273
277
|
(bucket_for(entry)[entry.receiver] ||= {})[entry.method.to_s] = row
|
|
278
|
+
@unit_callee_rows << row if row.callee && CalleeRule.unit_rule?(row.callee)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# #1048 — a `callee:` naming a rule the engine does not implement is dropped with a warning rather
|
|
282
|
+
# than silently doing nothing, for the reason {#note_root_demotion} gives: a contribution that
|
|
283
|
+
# quietly evaporates reads to its author as the feature being broken.
|
|
284
|
+
def callee_rule_for(contribution, entry)
|
|
285
|
+
return entry.callee if entry.callee.nil? || CalleeRule.known?(entry.callee)
|
|
286
|
+
|
|
287
|
+
@warnings << "plugin #{contribution.id.inspect} names an unknown callee rule " \
|
|
288
|
+
"#{entry.callee.inspect} on #{entry.key}; the row contributes no edge"
|
|
289
|
+
nil
|
|
274
290
|
end
|
|
275
291
|
|
|
276
292
|
# Which index a row lands in, from its receiver spelling.
|
|
@@ -353,22 +369,16 @@ module Rigor
|
|
|
353
369
|
bucket.sort.map do |receiver, rows|
|
|
354
370
|
[receiver,
|
|
355
371
|
rows.sort.map do |selector, row|
|
|
356
|
-
[selector, row.labels.to_a, row.narrow, row.discharge, row.within, row.taint
|
|
372
|
+
[selector, row.labels.to_a, row.narrow, row.discharge, row.within, row.taint,
|
|
373
|
+
row.callee, row.callee_fallbacks, row.responds]
|
|
357
374
|
end]
|
|
358
375
|
end
|
|
359
376
|
end
|
|
360
377
|
|
|
361
378
|
def finalize
|
|
362
|
-
@class_rows.each_value { |bucket| bucket.each_value(&:freeze) }
|
|
363
|
-
@
|
|
364
|
-
@class_rows.freeze
|
|
365
|
-
@path_rows.each_value(&:freeze)
|
|
366
|
-
@path_rows.freeze
|
|
367
|
-
@self_rows.each_value(&:freeze)
|
|
368
|
-
@self_rows.freeze
|
|
369
|
-
@result_rows.each_value(&:freeze)
|
|
370
|
-
@result_rows.freeze
|
|
371
|
-
@edges.freeze
|
|
379
|
+
@class_rows.each_value { |bucket| bucket.each_value(&:freeze).freeze }
|
|
380
|
+
[@path_rows, @self_rows, @result_rows].each { |bucket| bucket.each_value(&:freeze).freeze }
|
|
381
|
+
[@class_rows, @edges, @unit_callee_rows].each(&:freeze)
|
|
372
382
|
@labels_by_owner.each_value(&:uniq!)
|
|
373
383
|
@labels_by_owner.reject! { |_, labels| labels.empty? }
|
|
374
384
|
@labels_by_owner.freeze
|
|
@@ -45,16 +45,18 @@ module Rigor
|
|
|
45
45
|
|
|
46
46
|
module_function
|
|
47
47
|
|
|
48
|
-
# @param collection
|
|
49
|
-
# @param discharge
|
|
48
|
+
# @param collection — the run's merged per-file collections
|
|
49
|
+
# @param discharge — the `effects.tolerated:` policy the undischarged lane is computed
|
|
50
50
|
# under; {Discharge.none} makes the two lanes equal.
|
|
51
|
-
# @
|
|
52
|
-
|
|
51
|
+
# @param declined_unit_keys — unit keys the run KNOWS a source file for and has no summary for
|
|
52
|
+
# (#1065): a template a plugin claimed and declined. An edge's fallback list is not tried past one
|
|
53
|
+
# of them, because the file the framework runs is the one that produced nothing.
|
|
54
|
+
def propagate(collection, discharge: Discharge.none, declined_unit_keys: nil)
|
|
53
55
|
return EffectTable.empty if collection.summaries.empty?
|
|
54
56
|
|
|
55
57
|
summaries = collection.summaries
|
|
56
58
|
state = seed(summaries, discharge)
|
|
57
|
-
edges = resolve_edges(collection, state)
|
|
59
|
+
edges = resolve_edges(collection, state, declined_unit_keys)
|
|
58
60
|
iterate(state, edges)
|
|
59
61
|
EffectTable.new(build_entries(summaries, edges, state))
|
|
60
62
|
rescue StandardError
|
|
@@ -63,12 +65,14 @@ module Rigor
|
|
|
63
65
|
|
|
64
66
|
# `{caller_key => [callee_key]}`, sorted and de-duplicated. Seeds the `unresolved-super` taint in
|
|
65
67
|
# the same pass, because whether a `super` resolved is exactly what this resolution answers.
|
|
66
|
-
def resolve_edges(collection, state)
|
|
67
|
-
index = Index.new(collection)
|
|
68
|
+
def resolve_edges(collection, state, declined_unit_keys = nil)
|
|
69
|
+
index = Index.new(collection, declined_unit_keys: declined_unit_keys)
|
|
68
70
|
collection.edges.each_with_object({}) do |(caller_key, list), out|
|
|
69
71
|
targets = list.flat_map do |edge|
|
|
70
|
-
resolved = index.
|
|
72
|
+
resolved = index.resolve(edge)
|
|
71
73
|
taint_unresolved_super(state, caller_key, edge) if edge.super_call && resolved.empty?
|
|
74
|
+
taint_unresolved_callee(state, caller_key, edge) if edge.taint_if_unresolved && resolved.empty?
|
|
75
|
+
mark_unclaimed(state, caller_key) if edge.unclaimed && !edge.super_call && !index.owner_resolved?(edge)
|
|
72
76
|
resolved
|
|
73
77
|
end.uniq.sort
|
|
74
78
|
out[caller_key] = targets.freeze unless targets.empty?
|
|
@@ -96,6 +100,47 @@ module Rigor
|
|
|
96
100
|
entry[:causes] << ["unresolved-super", edge.selector].freeze
|
|
97
101
|
end
|
|
98
102
|
|
|
103
|
+
# #1048 — a `callee:` edge a plugin's row produced, whose named unit is not in the table: the
|
|
104
|
+
# template the render site pointed at was never analysed, so the row's `template-not-analysed` cause
|
|
105
|
+
# is seeded here after all.
|
|
106
|
+
#
|
|
107
|
+
# The taint is ADDED on failure rather than subtracted on success, which is what keeps the fixpoint
|
|
108
|
+
# monotone and the answer independent of visit order. It is also why a row whose rule declined
|
|
109
|
+
# outright — a computed `render foo`, a `render json:` — never reaches this: {UnitScan} tainted that
|
|
110
|
+
# site directly and recorded no edge, so nothing about an unresolvable render changed.
|
|
111
|
+
def taint_unresolved_callee(state, caller_key, edge)
|
|
112
|
+
entry = state[caller_key]
|
|
113
|
+
return if entry.nil?
|
|
114
|
+
|
|
115
|
+
entry[:exhaustive] = false
|
|
116
|
+
# Frozen here rather than trusted from the edge: `Marshal.load` of a `Data` bypasses
|
|
117
|
+
# `initialize`, so a pooled worker's collection restores the pair unfrozen while a sequential
|
|
118
|
+
# one has the scan's frozen original. A cause travels into a `Set` shared by the whole fixpoint,
|
|
119
|
+
# and the two paths must hand it the same value. `#freeze` on an already-frozen array is free.
|
|
120
|
+
entry[:causes] << edge.taint_if_unresolved.freeze
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# #391 — an edge nothing bounded whose receiver's OWN ancestry holds no project definition: the
|
|
124
|
+
# callee's footprint was described by nobody, so the closure is "what the analyzer read", not
|
|
125
|
+
# "what the method does".
|
|
126
|
+
#
|
|
127
|
+
# The question is deliberately {Index#owner_resolved?} and NOT "did this edge resolve to nothing".
|
|
128
|
+
# An ordinary edge also joins every project subclass override of the selector (the closed-world
|
|
129
|
+
# join of ADR-103 WD4), so a receiver typed as a gem class a project subclass happens to override
|
|
130
|
+
# resolves non-empty while the receiver's own dispatch target is still undescribed — `B.run` on a
|
|
131
|
+
# `Base2 < Vendor::Client` reaches `Vendor::Client#run` at run time whatever `Sub2#run` does.
|
|
132
|
+
#
|
|
133
|
+
# Decided HERE for the same reason the `super` taint is: only the merged ancestry can say whether
|
|
134
|
+
# the edge resolves. Unlike that taint it is not a cause and does not touch exhaustiveness — every
|
|
135
|
+
# existing consumer reads exactly what it read before. Its one reader is sig-gen's emission, which
|
|
136
|
+
# must not write `%a{pure}` about a callee nobody described.
|
|
137
|
+
def mark_unclaimed(state, caller_key)
|
|
138
|
+
entry = state[caller_key]
|
|
139
|
+
return if entry.nil?
|
|
140
|
+
|
|
141
|
+
entry[:unclaimed] = true
|
|
142
|
+
end
|
|
143
|
+
|
|
99
144
|
# Causes are carried as a Set through the fixpoint and flattened back to a sorted Array in
|
|
100
145
|
# {build_entries}. A Set is what {absorb} needs: unioning one along an edge must cost the source's
|
|
101
146
|
# size and allocate NOTHING when it adds nothing, and the array-concat-and-uniq it replaces
|
|
@@ -106,7 +151,8 @@ module Rigor
|
|
|
106
151
|
proven: summary.proven,
|
|
107
152
|
undischarged: discharge.inert? ? summary.proven : discharge.undischarged(summary.bundles),
|
|
108
153
|
declared: summary.declared,
|
|
109
|
-
exhaustive: summary.exhaustive?, causes: Set.new(summary.causes)
|
|
154
|
+
exhaustive: summary.exhaustive?, causes: Set.new(summary.causes),
|
|
155
|
+
unclaimed: summary.unclaimed?
|
|
110
156
|
}
|
|
111
157
|
end
|
|
112
158
|
end
|
|
@@ -165,6 +211,10 @@ module Rigor
|
|
|
165
211
|
target[:exhaustive] = false
|
|
166
212
|
changed = true
|
|
167
213
|
end
|
|
214
|
+
if !target[:unclaimed] && source[:unclaimed]
|
|
215
|
+
target[:unclaimed] = true
|
|
216
|
+
changed = true
|
|
217
|
+
end
|
|
168
218
|
causes = target[:causes]
|
|
169
219
|
source[:causes].each { |cause| changed = true if causes.add?(cause) }
|
|
170
220
|
changed
|
|
@@ -191,25 +241,31 @@ module Rigor
|
|
|
191
241
|
declared: closed[:declared],
|
|
192
242
|
exhaustive: closed[:exhaustive],
|
|
193
243
|
causes: closed[:causes].sort_by { |cause, detail| [cause, detail.to_s] }.freeze,
|
|
194
|
-
edges: edges.fetch(key, NO_EDGES)
|
|
244
|
+
edges: edges.fetch(key, NO_EDGES),
|
|
245
|
+
unclaimed: closed[:unclaimed]
|
|
195
246
|
)
|
|
196
247
|
end
|
|
197
248
|
end
|
|
198
249
|
|
|
199
|
-
private_class_method :resolve_edges, :taint_unresolved_super, :
|
|
200
|
-
:
|
|
250
|
+
private_class_method :resolve_edges, :taint_unresolved_super, :taint_unresolved_callee,
|
|
251
|
+
:mark_unclaimed, :seed, :iterate,
|
|
252
|
+
:reverse_edges, :absorb, :join_lane, :build_entries
|
|
201
253
|
|
|
202
254
|
# The class graph a run's collections describe, and the edge resolution over it. Built once per
|
|
203
255
|
# propagation; every lookup is a Hash read.
|
|
204
256
|
class Index
|
|
205
|
-
def initialize(collection)
|
|
257
|
+
def initialize(collection, declined_unit_keys: nil)
|
|
206
258
|
@summaries = collection.summaries
|
|
259
|
+
@declined = declined_unit_keys.nil? || declined_unit_keys.empty? ? nil : declined_unit_keys.to_a.to_set
|
|
207
260
|
@superclasses = collection.superclasses
|
|
208
261
|
@includes = collection.includes
|
|
209
262
|
@classes = build_classes(collection)
|
|
210
263
|
@descendants = build_descendants(collection.superclasses)
|
|
211
264
|
@descendant_closures = {}
|
|
212
265
|
@targets = {}
|
|
266
|
+
# #391 — `{memo key => whether the receiver's own ancestry answered}`, filled by
|
|
267
|
+
# {#call_targets} so {#owner_resolved?} costs a Hash read rather than a second ancestor walk.
|
|
268
|
+
@owner_resolved = {}
|
|
213
269
|
end
|
|
214
270
|
|
|
215
271
|
# Every project method key `edge` may reach: the definition its ancestry resolves to, plus every
|
|
@@ -219,17 +275,237 @@ module Rigor
|
|
|
219
275
|
# one such tuple is asked for once per call site in the project. `ApplicationRecord#save` alone is
|
|
220
276
|
# thousands of sites on a Rails app, each of which used to re-walk the whole subclass forest.
|
|
221
277
|
def targets_for(edge)
|
|
222
|
-
@targets[
|
|
278
|
+
@targets[memo_key(edge)] ||= begin
|
|
223
279
|
separator = edge.kind == :singleton ? "." : "#"
|
|
224
|
-
|
|
280
|
+
key = memo_key(edge)
|
|
281
|
+
if edge.super_call
|
|
282
|
+
super_targets(edge, separator)
|
|
283
|
+
else
|
|
284
|
+
constructor_targets(edge, key) || call_targets(edge, key, separator)
|
|
285
|
+
end
|
|
225
286
|
end
|
|
226
287
|
end
|
|
227
288
|
|
|
289
|
+
# {#targets_for}, then — only where that answered nothing — the first of the edge's
|
|
290
|
+
# `fallback_selectors` that resolves (#1065), spelled as the same edge with that selector. A unit
|
|
291
|
+
# under the requested key therefore always wins and a fallback never joins a second target beside
|
|
292
|
+
# it: the lookup runs ONE template, and an edge to both would put a label on the caller that no
|
|
293
|
+
# execution of the render produces. Empty when every candidate fails, which is what lets the
|
|
294
|
+
# caller seed `taint_if_unresolved` exactly as before.
|
|
295
|
+
# A requested key the run **declined** — a template whose file exists and whose plugin produced no
|
|
296
|
+
# unit — stops there. "No unit answers" and "no such template" are different facts, and only the
|
|
297
|
+
# second licenses the framework's next candidate: an `_x.js.haml` beside an `_x.html.erb` is run by
|
|
298
|
+
# Action View as Haml, so joining the ERB unit would be a label no execution produces.
|
|
299
|
+
def resolve(edge)
|
|
300
|
+
resolved = targets_for(edge)
|
|
301
|
+
return resolved unless resolved.empty? && edge.fallback_selectors
|
|
302
|
+
return NO_TARGETS if declined?(edge)
|
|
303
|
+
|
|
304
|
+
fallback_targets(edge)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Whether the receiver's OWN ancestry holds a project definition of the selector — the half of
|
|
308
|
+
# {#targets_for} the closed-world subclass join hides (#391).
|
|
309
|
+
#
|
|
310
|
+
# `targets_for` answers "which project bodies may this edge reach", and for an effect closure the
|
|
311
|
+
# join is right: Ruby has no `final`, so a subclass override really can run. It is the wrong
|
|
312
|
+
# question for "is this callee described at all". A receiver typed as a class the project only
|
|
313
|
+
# subclasses dispatches into that class's own implementation, which lives in a gem or in core;
|
|
314
|
+
# that a project subclass overrides the same selector says nothing about it. Only emission asks
|
|
315
|
+
# this, and only to decline.
|
|
316
|
+
def owner_resolved?(edge)
|
|
317
|
+
targets_for(edge)
|
|
318
|
+
@owner_resolved.fetch(memo_key(edge), false)
|
|
319
|
+
end
|
|
320
|
+
|
|
228
321
|
private
|
|
229
322
|
|
|
230
|
-
def
|
|
323
|
+
def declined?(edge)
|
|
324
|
+
!@declined.nil? && @declined.include?("#{edge.receiver_class}.#{edge.selector}")
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def fallback_targets(edge)
|
|
328
|
+
edge.fallback_selectors.each do |selector|
|
|
329
|
+
resolved = targets_for(edge.with(selector: selector, fallback_selectors: nil))
|
|
330
|
+
return resolved unless resolved.empty?
|
|
331
|
+
end
|
|
332
|
+
NO_TARGETS
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def memo_key(edge)
|
|
336
|
+
[edge.receiver_class, edge.kind, edge.selector, edge.super_call, edge.constant_receiver]
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
NEW_SELECTOR = "new"
|
|
340
|
+
INITIALIZE_SELECTOR = "initialize"
|
|
341
|
+
private_constant :NEW_SELECTOR, :INITIALIZE_SELECTOR
|
|
342
|
+
|
|
343
|
+
# Class objects whose `new` is Ruby's own reflective constructor rather than a project class's
|
|
344
|
+
# (#1039). `Class.new` allocates an anonymous class and runs `Class#initialize`, NOT the
|
|
345
|
+
# `#initialize` of whatever the project happens to have named `Class`; the same holds for
|
|
346
|
+
# `Module.new`, and for `Struct.new` / `Data.define`, which build a class rather than an instance.
|
|
347
|
+
# Listed by name rather than left to the project-known guard below, because a project that reopens
|
|
348
|
+
# `Class` at all would otherwise turn every `Class.new` in it into a call on that reopening.
|
|
349
|
+
RESERVED_CONSTRUCTOR_OWNERS = %w[Class Module Struct Data].freeze
|
|
350
|
+
private_constant :RESERVED_CONSTRUCTOR_OWNERS
|
|
351
|
+
|
|
352
|
+
# Superclass spellings whose `#initialize` is Ruby's own and empty. A project chain that ends here
|
|
353
|
+
# — or ends implicitly, with no `<` at all — has no constructor body anywhere, which is what
|
|
354
|
+
# {#empty_constructor?} has to establish before it may say ∅ rather than "undescribed".
|
|
355
|
+
EMPTY_CONSTRUCTOR_ROOTS = %w[Object BasicObject].freeze
|
|
356
|
+
private_constant :EMPTY_CONSTRUCTOR_ROOTS
|
|
357
|
+
|
|
358
|
+
# #1039 — `Const.new` on a class the project defines, resolved to that class's `#initialize`.
|
|
359
|
+
#
|
|
360
|
+
# The collector records the call as `(Const, :singleton, "new")`, because that is what the typer
|
|
361
|
+
# saw; nothing in the project defines `Const.new`, so the edge resolved to nothing and every
|
|
362
|
+
# constructor body stayed out of its caller's closure. `new` is the one selector in Ruby whose
|
|
363
|
+
# dispatch target is spelled under a different key, and this is the rewrite: the same ancestor walk
|
|
364
|
+
# {#resolve_owner} performs, on the instance side, for `initialize`.
|
|
365
|
+
#
|
|
366
|
+
# Three boundaries, and each of them can only narrow:
|
|
367
|
+
#
|
|
368
|
+
# - a project `def self.new` **wins**. It is the definition `Const.new` actually reaches, and a
|
|
369
|
+
# constructor that overrides `new` is the case where `#initialize` is not the answer. Resolved
|
|
370
|
+
# through the singleton ancestry, so an inherited `self.new` wins too.
|
|
371
|
+
# - the receiver must be a class the project defines, and must not be one of
|
|
372
|
+
# {RESERVED_CONSTRUCTOR_OWNERS}. `Class.new { … }` is not a constructor call on a project class
|
|
373
|
+
# and must never reach a project `#initialize`; a gem class the project only calls `new` on is
|
|
374
|
+
# undescribed exactly as it was, and stays unclaimed.
|
|
375
|
+
# - the closed-world subclass join is **dropped for a written constant receiver, and only for one**.
|
|
376
|
+
# `Base.new` names the class object it constructs, so `Sub#initialize` cannot run and joining it
|
|
377
|
+
# would put a proven label on the caller that no execution of that site can produce — the `super`
|
|
378
|
+
# argument (see {#super_targets}) rather than the ordinary-call one. But the edge is keyed on the
|
|
379
|
+
# receiver's TYPE, and `self.class.new`, a `Singleton[Base]` local's `.new` and a receiver-less
|
|
380
|
+
# `new` in a singleton body produce the identical tuple while genuinely constructing a subclass.
|
|
381
|
+
# {FileCollection::Edge#constant_receiver} is what tells them apart, and every shape that is not
|
|
382
|
+
# a written constant keeps the join — over `Sub#initialize` and over a subclass `Sub.new` alike.
|
|
383
|
+
# - an ancestry the scanner could not read ({FileCollection::OPAQUE_ANCESTOR} — a non-constant
|
|
384
|
+
# superclass expression, an aliased `initialize`) declines, leaving the caller as unclaimed as it
|
|
385
|
+
# was before this rule existed.
|
|
386
|
+
#
|
|
387
|
+
# @return the targets, or `nil` when the rewrite does not apply and ordinary resolution should run
|
|
388
|
+
def constructor_targets(edge, memo_key)
|
|
389
|
+
return nil unless constructor_edge?(edge)
|
|
390
|
+
|
|
391
|
+
owner = resolve_owner(edge.receiver_class, "#", INITIALIZE_SELECTOR)
|
|
392
|
+
return nil unless owner || empty_constructor?(edge.receiver_class)
|
|
393
|
+
|
|
394
|
+
@owner_resolved[memo_key] = true
|
|
395
|
+
targets = owner ? [owner] : []
|
|
396
|
+
targets.concat(subclass_constructors(edge.receiver_class)) unless edge.constant_receiver
|
|
397
|
+
targets.uniq.freeze
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Whether the `new` rewrite applies to this edge at all — the guards of {#constructor_targets},
|
|
401
|
+
# each of which can only decline: a singleton `new` on a project class that is not one of Ruby's
|
|
402
|
+
# own class builders, whose singleton ancestry defines no `new` of its own, and whose constructor
|
|
403
|
+
# the scan could read both above it and (where the join applies) below it.
|
|
404
|
+
def constructor_edge?(edge)
|
|
405
|
+
edge.kind == :singleton && edge.selector == NEW_SELECTOR &&
|
|
406
|
+
!RESERVED_CONSTRUCTOR_OWNERS.include?(edge.receiver_class) &&
|
|
407
|
+
project_class?(edge.receiver_class) &&
|
|
408
|
+
resolve_owner(edge.receiver_class, ".", NEW_SELECTOR).nil? &&
|
|
409
|
+
!opaque_ancestry?(edge.receiver_class) &&
|
|
410
|
+
(edge.constant_receiver || !opaque_descendant?(edge.receiver_class))
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# Every constructor a subclass of `class_name` supplies — its own `#initialize`, and its own
|
|
414
|
+
# `.new` where it overrides one. The closed-world join of step 2, spelled for the two keys a
|
|
415
|
+
# constructor can live under.
|
|
416
|
+
def subclass_constructors(class_name)
|
|
417
|
+
descendant_closure(class_name).each_with_object([]) do |subclass, keys|
|
|
418
|
+
keys << "#{subclass}##{INITIALIZE_SELECTOR}" if @summaries.key?("#{subclass}##{INITIALIZE_SELECTOR}")
|
|
419
|
+
keys << "#{subclass}.#{NEW_SELECTOR}" if @summaries.key?("#{subclass}.#{NEW_SELECTOR}")
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# The same question DOWNWARD, and only where the closed-world join applies. A subclass whose own
|
|
424
|
+
# constructor is unreadable — `class AliasedChild < Parent; alias initialize setup` — is reached by
|
|
425
|
+
# `self.class.new` in `Parent`, and {#subclass_constructors} would find no `AliasedChild#initialize`
|
|
426
|
+
# key and say nothing. The join is the whole reason this edge may construct a subclass at all, so an
|
|
427
|
+
# unreadable one in the closure declines the edge instead.
|
|
428
|
+
def opaque_descendant?(class_name)
|
|
429
|
+
descendant_closure(class_name).any? do |subclass|
|
|
430
|
+
@includes.fetch(subclass, []).include?(FileCollection::OPAQUE_ANCESTOR) ||
|
|
431
|
+
@superclasses.fetch(subclass, []).include?(FileCollection::OPAQUE_ANCESTOR)
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# Whether anything in `class_name`'s ancestry told the scanner its constructor is not readable from
|
|
436
|
+
# the source (#1039): a superclass expression that is not a constant path, or an `alias` that makes
|
|
437
|
+
# `initialize` another method. The sentinel rides the ancestry tables, so one walk finds it wherever
|
|
438
|
+
# in the chain it was recorded.
|
|
439
|
+
def opaque_ancestry?(class_name)
|
|
440
|
+
queue = [class_name]
|
|
441
|
+
seen = Set.new
|
|
442
|
+
until queue.empty?
|
|
443
|
+
current = queue.shift
|
|
444
|
+
next unless seen.add?(current)
|
|
445
|
+
return true if current == FileCollection::OPAQUE_ANCESTOR
|
|
446
|
+
|
|
447
|
+
queue.concat(@includes.fetch(current, []))
|
|
448
|
+
queue.concat(@superclasses.fetch(current, []))
|
|
449
|
+
end
|
|
450
|
+
false
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# Whether `class_name`'s constructor is *known* to be `BasicObject#initialize`, whose footprint is
|
|
454
|
+
# ∅ (#1039).
|
|
455
|
+
#
|
|
456
|
+
# This is the design choice in the rewrite. "No `#initialize` in the ancestry" has two readings —
|
|
457
|
+
# the project defines none and none exists (a plain `class Bare; end`, constructed by Ruby's own
|
|
458
|
+
# empty constructor), or the project defines none and a gem's base class does. The first resolves
|
|
459
|
+
# to a known-empty definition: the edge contributes nothing AND leaves its caller claimed, because
|
|
460
|
+
# the callee was read — there is simply nothing in it. The second is undescribed and must keep
|
|
461
|
+
# marking its caller unclaimed. Only an ancestry that closes inside the project can tell them
|
|
462
|
+
# apart, so that is what this walks, and it answers `false` the moment the walk leaves.
|
|
463
|
+
#
|
|
464
|
+
# No summary row is invented for `BasicObject#initialize`. A row would be a *method of the
|
|
465
|
+
# project* in every table that lists them — the snapshot, `rigor effects`, the pure report — for a
|
|
466
|
+
# definition the project does not contain; the answer belongs to the edge, and the edge is already
|
|
467
|
+
# the thing {#owner_resolved?} is asked about.
|
|
468
|
+
#
|
|
469
|
+
# An `include` anywhere in the chain ends the walk conservatively. The collection's include table
|
|
470
|
+
# is a flat list of as-written *candidate spellings* (`include Foo` inside `module A` records both
|
|
471
|
+
# `A::Foo` and `Foo`), so it cannot say whether a class includes one project module or one gem
|
|
472
|
+
# module the project cannot see — and a module is free to define `initialize`. Such a class keeps
|
|
473
|
+
# exactly today's behaviour: its `new` edge resolves to nothing and its callers stay unclaimed.
|
|
474
|
+
# The table holds instance-side includes only. A module mixed into the singleton class, by
|
|
475
|
+
# `extend` or by an `include` inside `class << self`, is not in it, so a `new` such a module
|
|
476
|
+
# supplies is not seen, and the walk can answer `true` for a class whose `new` does work.
|
|
477
|
+
def empty_constructor?(class_name)
|
|
478
|
+
queue = [class_name]
|
|
479
|
+
seen = Set.new
|
|
480
|
+
until queue.empty?
|
|
481
|
+
current = queue.shift
|
|
482
|
+
next unless seen.add?(current)
|
|
483
|
+
return false unless project_class?(current)
|
|
484
|
+
return false unless @includes.fetch(current, []).empty?
|
|
485
|
+
|
|
486
|
+
parents = @superclasses.fetch(current, [])
|
|
487
|
+
next if parents.empty?
|
|
488
|
+
|
|
489
|
+
known = parents.find { |candidate| project_class?(candidate) }
|
|
490
|
+
return false if known.nil? && parents.none? { |candidate| EMPTY_CONSTRUCTOR_ROOTS.include?(candidate) }
|
|
491
|
+
|
|
492
|
+
queue << known if known
|
|
493
|
+
end
|
|
494
|
+
true
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# Whether the project defines this class at all: it defines a method on it, declares its
|
|
498
|
+
# superclass, or declares what it includes. Any one of the three is a `class` body the scanner
|
|
499
|
+
# read, which is what licenses reading its silence about `initialize` as an answer.
|
|
500
|
+
def project_class?(class_name)
|
|
501
|
+
!class_name.nil? &&
|
|
502
|
+
(@classes.include?(class_name) || @superclasses.key?(class_name) || @includes.key?(class_name))
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def call_targets(edge, memo_key, separator)
|
|
231
506
|
targets = []
|
|
232
507
|
owner = resolve_owner(edge.receiver_class, separator, edge.selector)
|
|
508
|
+
@owner_resolved[memo_key] = !owner.nil?
|
|
233
509
|
targets << owner if owner
|
|
234
510
|
descendant_closure(edge.receiver_class).each do |subclass|
|
|
235
511
|
key = "#{subclass}#{separator}#{edge.selector}"
|
|
@@ -260,7 +536,7 @@ module Rigor
|
|
|
260
536
|
|
|
261
537
|
# Ancestry order mirrors the engine's: the class itself, the modules it includes, then its
|
|
262
538
|
# superclass, recursively. Cycle-guarded, because a project may declare one. Ancestry names
|
|
263
|
-
# arrive as as-written candidate lists (see `
|
|
539
|
+
# arrive as as-written candidate lists (see `AncestryRecorder#lexical_candidates`); every candidate is
|
|
264
540
|
# enqueued and the most-qualified one comes first, so the right constant wins the race and a
|
|
265
541
|
# spelling that names nothing simply matches no key.
|
|
266
542
|
def resolve_owner(class_name, separator, selector)
|
|
@@ -273,8 +549,10 @@ module Rigor
|
|
|
273
549
|
#
|
|
274
550
|
# The includes are instance-side only. `include M` puts `M#m` between the class and its
|
|
275
551
|
# superclass, which is exactly where `super` looks, while `M.m` is a singleton method `include`
|
|
276
|
-
# never contributes
|
|
277
|
-
# a `class << self` include, neither of which is collected, so
|
|
552
|
+
# never contributes. A singleton `super` that really does reach a module went through `extend` or
|
|
553
|
+
# a `class << self` include, neither of which is collected, so the walk steps over the module: it
|
|
554
|
+
# reaches the superclass chain's definition when there is one, without the module's labels, and
|
|
555
|
+
# resolves to nothing and taints when there is not.
|
|
278
556
|
def resolve_super(class_name, separator, selector)
|
|
279
557
|
queue = separator == "#" ? @includes.fetch(class_name, []).dup : []
|
|
280
558
|
queue.concat(@superclasses.fetch(class_name, []))
|