rigortype 0.3.8 → 0.3.9
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 +1 -1
- data/data/capability_roles/capability_roles.rbs +36 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +109 -13
- data/docs/handbook/02-everyday-types.md +17 -14
- data/docs/handbook/03-narrowing.md +37 -4
- 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 +14 -1
- data/docs/handbook/11-sig-gen.md +72 -1
- 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 +32 -4
- data/docs/manual/03-configuration.md +11 -0
- data/docs/manual/04-diagnostics.md +11 -0
- data/docs/manual/07-plugins.md +15 -5
- data/docs/manual/08-skills.md +23 -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 +6 -1
- data/docs/manual/18-removing-dead-code.md +10 -8
- data/docs/manual/plugins/rigor-actionpack.md +18 -0
- data/docs/manual/plugins/rigor-activerecord.md +28 -0
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +32 -2
- data/docs/manual/plugins/rigor-rbs-inline.md +42 -1
- data/lib/rigor/analysis/baseline.rb +2 -2
- 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/ivar_write_collector.rb +10 -2
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
- data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
- data/lib/rigor/analysis/check_rules/rule_walk.rb +39 -4
- 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/unreachable_clause_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
- data/lib/rigor/analysis/check_rules.rb +200 -32
- data/lib/rigor/analysis/crash_signature.rb +8 -24
- 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 +6 -1
- data/lib/rigor/analysis/incremental.rb +16 -0
- data/lib/rigor/analysis/incremental_session.rb +81 -22
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
- data/lib/rigor/analysis/reachability/graph.rb +3 -5
- data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -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 +2 -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 +4 -1
- data/lib/rigor/analysis/run_cache_key.rb +1 -1
- 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 +177 -26
- data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -10
- data/lib/rigor/analysis/runner/pool_coordinator.rb +256 -88
- data/lib/rigor/analysis/runner/project_pre_passes.rb +12 -6
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +133 -35
- data/lib/rigor/analysis/worker_session.rb +9 -10
- data/lib/rigor/bleeding_edge.rb +12 -24
- data/lib/rigor/builtins/hkt_builtins.rb +2 -2
- data/lib/rigor/builtins/imported_refinements.rb +155 -21
- data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
- data/lib/rigor/builtins/regex_refinement.rb +4 -4
- data/lib/rigor/builtins/static_return_refinements.rb +5 -6
- data/lib/rigor/cache/annotation_location.rb +2 -4
- data/lib/rigor/cache/descriptor.rb +58 -20
- data/lib/rigor/cache/engine_source.rb +4 -4
- data/lib/rigor/cache/file_digest.rb +9 -1
- data/lib/rigor/cache/incremental_snapshot.rb +40 -6
- 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 +79 -11
- 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 +77 -32
- 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 +3 -3
- data/lib/rigor/cli/docs_command.rb +2 -2
- 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 +1 -1
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/prism_colorizer.rb +2 -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 +2 -2
- data/lib/rigor/cli/skill_command.rb +1 -1
- data/lib/rigor/cli/skill_deep_probe.rb +4 -4
- data/lib/rigor/cli/skill_describe.rb +36 -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 +10 -6
- 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 +68 -6
- data/lib/rigor/config_audit.rb +30 -5
- data/lib/rigor/configuration/severity_profile.rb +6 -6
- data/lib/rigor/configuration.rb +43 -4
- data/lib/rigor/effects/attribution.rb +1 -3
- data/lib/rigor/effects/config_envelopes.rb +6 -8
- data/lib/rigor/effects/definition_lines.rb +25 -6
- data/lib/rigor/effects/effect_table.rb +0 -1
- data/lib/rigor/effects/entry_points.rb +0 -2
- data/lib/rigor/effects/envelope_check.rb +8 -8
- data/lib/rigor/effects/envelope_index.rb +5 -8
- data/lib/rigor/effects/framework_units.rb +4 -6
- data/lib/rigor/effects/identity.rb +4 -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/method_key.rb +1 -1
- data/lib/rigor/effects/plugin_facts.rb +4 -6
- data/lib/rigor/effects/propagator.rb +2 -3
- data/lib/rigor/effects/scanner.rb +1 -1
- data/lib/rigor/effects/signature_sources.rb +3 -5
- data/lib/rigor/effects/snapshot.rb +10 -10
- data/lib/rigor/effects/snapshot_diff.rb +1 -1
- data/lib/rigor/effects/unit_scan.rb +10 -10
- data/lib/rigor/effects/unknown_label_check.rb +3 -8
- data/lib/rigor/effects/unknown_label_report.rb +3 -4
- 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 +51 -4
- 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 +388 -59
- data/lib/rigor/environment.rb +105 -20
- data/lib/rigor/flow_contribution/merger.rb +0 -2
- data/lib/rigor/flow_contribution.rb +11 -13
- data/lib/rigor/inference/acceptance.rb +153 -8
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/body_fixpoint.rb +5 -5
- data/lib/rigor/inference/budget_trace.rb +1 -5
- data/lib/rigor/inference/captured_locals.rb +2 -3
- data/lib/rigor/inference/closure_escape_analyzer.rb +2 -4
- data/lib/rigor/inference/coverage_scanner.rb +4 -6
- data/lib/rigor/inference/dynamic_origin.rb +1 -1
- data/lib/rigor/inference/element_read_widening.rb +168 -0
- data/lib/rigor/inference/expression_typer.rb +763 -143
- data/lib/rigor/inference/fork_map.rb +5 -7
- 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 +1 -5
- data/lib/rigor/inference/macro_block_self_type.rb +1 -4
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +2 -9
- 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/file_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +2 -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/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 +61 -47
- data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +165 -31
- 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/set_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -1
- 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 +4 -5
- 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 +12 -13
- data/lib/rigor/inference/method_parameter_binder.rb +4 -6
- data/lib/rigor/inference/multi_target_binder.rb +1 -3
- data/lib/rigor/inference/mutation_rejoin.rb +168 -0
- data/lib/rigor/inference/mutation_widening.rb +100 -51
- data/lib/rigor/inference/narrowing.rb +299 -46
- data/lib/rigor/inference/optimistic_origin.rb +1 -7
- data/lib/rigor/inference/origin_lookup.rb +2 -3
- data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
- data/lib/rigor/inference/pre_eval_constants.rb +6 -5
- data/lib/rigor/inference/precision_scanner.rb +3 -4
- data/lib/rigor/inference/project_patched_methods.rb +2 -2
- data/lib/rigor/inference/project_patched_scanner.rb +3 -3
- 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 +3 -3
- data/lib/rigor/inference/refinement_mutation.rb +72 -0
- data/lib/rigor/inference/scope_indexer.rb +688 -151
- data/lib/rigor/inference/statement_evaluator.rb +221 -50
- data/lib/rigor/inference/string_mutation.rb +60 -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/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 +1 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
- 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 +2 -2
- data/lib/rigor/language_server/incremental_sync.rb +6 -6
- data/lib/rigor/language_server/project_context.rb +2 -5
- 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/plugin/base.rb +90 -21
- data/lib/rigor/plugin/bundled_catalog.rb +167 -0
- data/lib/rigor/plugin/effect_attribution.rb +7 -7
- 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 +31 -12
- 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 +23 -1
- data/lib/rigor/plugin/registry.rb +42 -12
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +12 -1
- data/lib/rigor/plugin/trust_policy.rb +2 -4
- data/lib/rigor/plugin/type_node_resolver.rb +3 -3
- 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 +11 -11
- data/lib/rigor/protection/kill_signature.rb +2 -4
- data/lib/rigor/protection/measurement_integrity.rb +1 -3
- data/lib/rigor/protection/mutation_cache.rb +6 -9
- 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 +61 -21
- data/lib/rigor/reflection/constant_path.rb +126 -0
- data/lib/rigor/reflection.rb +26 -10
- data/lib/rigor/runtime/jit.rb +6 -8
- data/lib/rigor/scope/discovery_index.rb +30 -0
- data/lib/rigor/scope.rb +203 -4
- data/lib/rigor/sig_gen/generator.rb +115 -22
- 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/observation_collector.rb +17 -15
- 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 +4 -5
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +111 -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/writer.rb +23 -6
- 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 +2 -5
- 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 +68 -5
- data/lib/rigor/type/data_class.rb +2 -2
- data/lib/rigor/type/data_instance.rb +4 -4
- data/lib/rigor/type/float_range.rb +128 -0
- data/lib/rigor/type/hash_shape.rb +5 -5
- data/lib/rigor/type/integer_range.rb +13 -8
- data/lib/rigor/type/nominal.rb +8 -2
- data/lib/rigor/type/refined.rb +3 -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/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.rb +1 -0
- 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 +1 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +33 -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.rb +28 -2
- data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -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 +1 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +3 -4
- 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 +42 -7
- data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -0
- 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 +119 -14
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +1 -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 +1 -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.rb +36 -4
- data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +6 -3
- 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 +0 -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 +1 -0
- 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 +1 -0
- 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 +1 -10
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +3 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +2 -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 +1 -0
- 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.rb +128 -3
- 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 +1 -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 +1 -0
- 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 +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +5 -5
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
- data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
- data/sig/rigor/cache.rbs +2 -3
- data/sig/rigor/environment.rbs +5 -0
- data/sig/rigor/inference.rbs +3 -10
- data/sig/rigor/plugin/io_boundary.rbs +4 -0
- data/sig/rigor/plugin/manifest.rbs +2 -1
- data/sig/rigor/reflection.rbs +3 -1
- data/sig/rigor/scope.rbs +24 -3
- data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -0
- data/sig/rigor/type.rbs +27 -1
- data/sig/rigor.rbs +1 -0
- data/skills/rigor-ask/SKILL.md +8 -6
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +13 -2
- data/skills/rigor-project-init/SKILL.md +6 -3
- data/skills/rigor-project-init/references/02-configure.md +10 -6
- data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
- data/skills/rigor-type-oracle/SKILL.md +228 -0
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +261 -0
- data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
- data/skills/rigor-type-oracle/references/03-gap-protocol.md +128 -0
- data/skills/rigor-unused-adjudicate/SKILL.md +8 -5
- metadata +26 -3
- data/lib/rigor/cli/probe_environment.rb +0 -85
- data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
|
@@ -7,6 +7,7 @@ require_relative "../type"
|
|
|
7
7
|
require_relative "../source/constant_path"
|
|
8
8
|
require_relative "../source/node_children"
|
|
9
9
|
require_relative "../cache/file_digest"
|
|
10
|
+
require_relative "../analysis/check_rules/published_constant_guard"
|
|
10
11
|
require_relative "anonymous_meta_class"
|
|
11
12
|
require_relative "def_handle"
|
|
12
13
|
require_relative "index_write_widening"
|
|
@@ -46,27 +47,42 @@ module Rigor
|
|
|
46
47
|
# seed bundle through `Marshal` unchanged.
|
|
47
48
|
CONSTANT_UNPUBLISHABLE = :unpublishable
|
|
48
49
|
|
|
50
|
+
# Issue #668 — the census key a constant write through a base no name reaches is filed under:
|
|
51
|
+
# `*::LIMIT` for `k::LIMIT = 7`. `*` is not a constant character, so the key can never collide with a
|
|
52
|
+
# name a program writes, while its LAST SEGMENT is the real one — which is what the ADR-46 `constant:`
|
|
53
|
+
# edge and {Analysis::Incremental.changed_constant_publications} key on, so both keep working unchanged.
|
|
54
|
+
DYNAMIC_TARGET_PREFIX = "*::"
|
|
55
|
+
|
|
49
56
|
module_function
|
|
50
57
|
|
|
51
58
|
# Build the scope index for a Prism program subtree.
|
|
52
59
|
#
|
|
53
|
-
# @param root
|
|
60
|
+
# @param root — usually a `Prism::ProgramNode`, but any
|
|
54
61
|
# subtree the caller wants the indexer to walk works.
|
|
55
|
-
# @param default_scope
|
|
62
|
+
# @param default_scope — the scope used for the root,
|
|
56
63
|
# and the fallback returned for any Prism node not contained in
|
|
57
64
|
# `root`'s subtree.
|
|
58
|
-
# @param converged_loop_recording
|
|
65
|
+
# @param converged_loop_recording — display-path flag —
|
|
59
66
|
# when true the evaluator re-records fixpoint-tracked loop
|
|
60
67
|
# bodies from their CONVERGED bindings so per-line probes
|
|
61
68
|
# (`rigor annotate`) reflect the post-writeback state, not the
|
|
62
69
|
# cap-N intermediate constants. Off for the check path.
|
|
63
|
-
# @return
|
|
70
|
+
# @return identity-comparing
|
|
64
71
|
# table whose default value is `default_scope`.
|
|
72
|
+
# The no-op re-anchoring table (#722 residue 2): shared so the overwhelmingly common
|
|
73
|
+
# nothing-to-re-anchor walk allocates none.
|
|
74
|
+
EMPTY_RENAMES = {}.freeze
|
|
75
|
+
|
|
65
76
|
def index(root, default_scope:, converged_loop_recording: false) # rubocop:disable Metrics/AbcSize
|
|
66
77
|
# Slice A-declarations. Build the declaration overrides first so every scope handed to the StatementEvaluator
|
|
67
78
|
# already carries the table; structural sharing through `Scope#with_local` / `#with_fact` / `#with_self_type`
|
|
68
79
|
# propagates it across every derived scope.
|
|
69
|
-
|
|
80
|
+
# Issue #722 residue 2 — the project's settled declaration names are the oracle the file's own
|
|
81
|
+
# compact headers are re-anchored against. `discovered_class_sources` would be the more direct
|
|
82
|
+
# reading, but it is seeded only under `--record-dependencies`; this table is the one every run
|
|
83
|
+
# carries, and the fold has already applied its own re-anchoring to it.
|
|
84
|
+
declared_types, discovered_classes =
|
|
85
|
+
build_declaration_artifacts(root, default_scope.discovered_classes.keys.to_set)
|
|
70
86
|
# Merge the indexer's findings on top of whatever the base scope already carries so callers that seed cross-file
|
|
71
87
|
# class knowledge (e.g. the ADR-14 `SigGen::ObservationCollector` pre-walking project `lib/` before scanning
|
|
72
88
|
# `spec/`) keep their seeds alongside the per-file declarations the indexer itself discovers. Indexer-found
|
|
@@ -110,6 +126,7 @@ module Rigor
|
|
|
110
126
|
build_in_source_constants(root, seeded_scope), literal_mutations[:constants]
|
|
111
127
|
)
|
|
112
128
|
seeded_scope = seed_constant_tables(seeded_scope, default_scope, in_source_constants, root)
|
|
129
|
+
seeded_scope = seed_published_constant_ivars(seeded_scope, root)
|
|
113
130
|
|
|
114
131
|
# Slice 7 phase 12. In-source method discovery. Walks every class/module body for `Prism::DefNode` and
|
|
115
132
|
# recognised `define_method` calls and records the introduced method names. `rigor check` consults the table to
|
|
@@ -193,7 +210,11 @@ module Rigor
|
|
|
193
210
|
# #526 — this file's extends folded against the MERGED instance def-nodes (so `extend M` sees a
|
|
194
211
|
# sibling-file M through the cross-file seed). The project-wide fold in {#finalize_def_index}
|
|
195
212
|
# covers cross-file CONSUMERS; this covers the file that declares the extend.
|
|
196
|
-
|
|
213
|
+
#
|
|
214
|
+
# Issue #898 — and the same walk's table is now kept, merged over the cross-file seed the way
|
|
215
|
+
# `includes` is: `Narrowing` asks it what a class object's singleton ancestry holds.
|
|
216
|
+
file_extends, extends = merge_extend_tables(default_scope, root)
|
|
217
|
+
methods_table = fold_per_file_extends(file_extends, def_nodes, singleton_def_nodes, seeded_scope)
|
|
197
218
|
|
|
198
219
|
seeded_scope.with_discovery(
|
|
199
220
|
seeded_scope.discovery.with(
|
|
@@ -204,6 +225,7 @@ module Rigor
|
|
|
204
225
|
discovered_superclasses: superclasses,
|
|
205
226
|
discovered_header_nestings: header_nestings,
|
|
206
227
|
discovered_includes: includes,
|
|
228
|
+
discovered_extends: extends,
|
|
207
229
|
discovered_method_visibilities: method_visibilities,
|
|
208
230
|
data_member_layouts: data_member_layouts,
|
|
209
231
|
struct_member_layouts: struct_member_layouts
|
|
@@ -247,10 +269,20 @@ module Rigor
|
|
|
247
269
|
merged.freeze
|
|
248
270
|
end
|
|
249
271
|
|
|
272
|
+
# Issue #898 — one walk, two consumers: the raw per-file table the #526 method fold reads, and the
|
|
273
|
+
# same table merged over the cross-file seed for the scope. Returned as a pair so a caller cannot
|
|
274
|
+
# pair the fold with a table a different parse produced, as {#merge_ancestry_tables} is.
|
|
275
|
+
def merge_extend_tables(default_scope, root)
|
|
276
|
+
file_extends = build_discovered_extends(root)
|
|
277
|
+
merged = default_scope.discovered_extends.merge(
|
|
278
|
+
file_extends
|
|
279
|
+
) { |_class, cross_file, per_file| (cross_file + per_file).uniq }
|
|
280
|
+
[file_extends, merged]
|
|
281
|
+
end
|
|
282
|
+
|
|
250
283
|
# The per-file half of the #526 fold: mutable copies of the merged tables take the extends, and the
|
|
251
284
|
# existence table (already seeded onto the scope) is rebuilt only when the fold touched it.
|
|
252
|
-
def fold_per_file_extends(
|
|
253
|
-
extends = build_discovered_extends(root)
|
|
285
|
+
def fold_per_file_extends(extends, def_nodes, singleton_def_nodes, seeded_scope)
|
|
254
286
|
methods_table = seeded_scope.discovered_methods
|
|
255
287
|
return methods_table if extends.empty?
|
|
256
288
|
|
|
@@ -1476,7 +1508,7 @@ module Rigor
|
|
|
1476
1508
|
# is a known in-place mutator (`MutationWidening`'s tables) or a plain attribute/index writer
|
|
1477
1509
|
# (`name=` / `[]=`). Non-mutating reads never register, so `VERSION`-style constants keep their fold.
|
|
1478
1510
|
#
|
|
1479
|
-
# @return
|
|
1511
|
+
# @return `:constants` — a Set of candidate qualified names (each mutation
|
|
1480
1512
|
# contributes every lexical-resolution candidate, `A::B::C` outward to bare `C`, mirroring how the
|
|
1481
1513
|
# reads resolve); `:cvars` — `{class_name => Set[Symbol]}`.
|
|
1482
1514
|
def collect_literal_receiver_mutations(root)
|
|
@@ -1522,19 +1554,35 @@ module Rigor
|
|
|
1522
1554
|
when Prism::ConstantReadNode
|
|
1523
1555
|
constant_mutation_candidates(receiver.name.to_s, nesting, census[:constants])
|
|
1524
1556
|
when Prism::ConstantPathNode
|
|
1525
|
-
|
|
1526
|
-
# them: `Holder::TABLE[:k] = 1` inside `module Admin` mutates whatever `Holder::TABLE` resolves
|
|
1527
|
-
# to there, and since [#690](https://github.com/rigortype/rigor/issues/690) the write accumulator
|
|
1528
|
-
# keys that entry `Admin::Holder::TABLE`. Recording only the as-written name left the mutated
|
|
1529
|
-
# constant matched by nothing and its closed empty shape intact — the very fold #540 exists to
|
|
1530
|
-
# retract. Over-recording only widens, which is the direction this census is allowed to err in.
|
|
1531
|
-
full = Source::ConstantPath.qualified_name_or_nil(receiver)
|
|
1532
|
-
constant_mutation_candidates(full, nesting, census[:constants]) if full
|
|
1557
|
+
path_mutation_candidates(receiver, nesting, census[:constants])
|
|
1533
1558
|
when Prism::ClassVariableReadNode
|
|
1534
1559
|
census[:cvars][qualified_prefix.join("::")] << receiver.name unless qualified_prefix.empty?
|
|
1535
1560
|
end
|
|
1536
1561
|
end
|
|
1537
1562
|
|
|
1563
|
+
# The lexical candidates a PATH spelling reaches, for the same reason the bare arm takes them:
|
|
1564
|
+
# `Holder::TABLE[:k] = 1` inside `module Admin` mutates whatever `Holder::TABLE` resolves to there,
|
|
1565
|
+
# and since [#690](https://github.com/rigortype/rigor/issues/690) the write accumulator keys that
|
|
1566
|
+
# entry `Admin::Holder::TABLE`. Recording only the as-written name left the mutated constant matched
|
|
1567
|
+
# by nothing and its closed empty shape intact — the very fold #540 exists to retract. Over-recording
|
|
1568
|
+
# only widens, which is the direction this census is allowed to err in.
|
|
1569
|
+
#
|
|
1570
|
+
# A ROOTED receiver is the one spelling that reaches no lexical candidate at all, so widening them is
|
|
1571
|
+
# not erring in the allowed direction but discarding precision the code hands over
|
|
1572
|
+
# ([#703](https://github.com/rigortype/rigor/issues/703)): `::Table::ROWS[k] = 1` names the top-level
|
|
1573
|
+
# constant unconditionally, so the sibling `Admin::Table::ROWS` a bare spelling would also reach is
|
|
1574
|
+
# untouched and keeps its empty-shape fold. This is the exemption #690 established on the WRITE side
|
|
1575
|
+
# ({#constant_path_write_key}), which the two arms have to agree on: the strict render drops the root
|
|
1576
|
+
# marker, so the rooted and unrooted spellings are indistinguishable by name alone.
|
|
1577
|
+
def path_mutation_candidates(receiver, nesting, into)
|
|
1578
|
+
full = Source::ConstantPath.qualified_name_or_nil(receiver)
|
|
1579
|
+
return if full.nil?
|
|
1580
|
+
|
|
1581
|
+
return into << full if Source::ConstantPath.rooted?(receiver)
|
|
1582
|
+
|
|
1583
|
+
constant_mutation_candidates(full, nesting, into)
|
|
1584
|
+
end
|
|
1585
|
+
|
|
1538
1586
|
# The receiver a node mutates, or nil when the node is not a mutation.
|
|
1539
1587
|
def mutating_receiver_of(node)
|
|
1540
1588
|
case node
|
|
@@ -1605,29 +1653,117 @@ module Rigor
|
|
|
1605
1653
|
# `Scope#published_constant?`: a project-published constant the analysed file also assigns is one its
|
|
1606
1654
|
# author can see, so the truthiness rules keep firing on it. Both are seeded here, where the per-file
|
|
1607
1655
|
# table is still separable from the project seed it is about to merge over.
|
|
1656
|
+
# Issue #667 — the alias table rides the same census and the same gate, because it answers the third
|
|
1657
|
+
# half of the same question: which of THIS file's own constant names carry a value the author never
|
|
1658
|
+
# saw. One census call now feeds both sets.
|
|
1608
1659
|
def seed_constant_tables(seeded_scope, default_scope, in_source_constants, root)
|
|
1609
1660
|
merged = merge_seeded_constants(default_scope.in_source_constants, in_source_constants)
|
|
1661
|
+
published = default_scope.published_constant_names
|
|
1662
|
+
census = published.empty? ? nil : constant_write_census(root)
|
|
1610
1663
|
seeded_scope.with_discovery(
|
|
1611
|
-
seeded_scope.discovery.with(
|
|
1612
|
-
|
|
1664
|
+
seeded_scope.discovery.with(
|
|
1665
|
+
in_source_constants: merged,
|
|
1666
|
+
local_constant_names: local_constant_name_set(census, published),
|
|
1667
|
+
published_constant_alias_names: published_constant_alias_name_set(census, published)
|
|
1668
|
+
)
|
|
1613
1669
|
)
|
|
1614
1670
|
end
|
|
1615
1671
|
|
|
1616
1672
|
# The QUALIFIED names this file assigns that the project also published, frozen. Read from the same
|
|
1617
|
-
# {#
|
|
1673
|
+
# {#constant_write_census} the cross-file table is built from, not from the typed per-file
|
|
1618
1674
|
# table: the typed walk cannot see a multi-assign / `self::` / operator-write target, so deriving the
|
|
1619
1675
|
# exemption from it would be a second source of truth blind to exactly the forms the census exists to
|
|
1620
1676
|
# catch. Gated on the project having published anything at all, so a project with no cross-file value
|
|
1621
1677
|
# constants pays no extra walk and allocates nothing; the result is filtered to the published names, so
|
|
1622
1678
|
# what it holds is bounded by what the exemption can ever be asked about.
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1679
|
+
#
|
|
1680
|
+
# It reads the census's `declared` half rather than its keys ([#710](https://github.com/rigortype/rigor/issues/710)).
|
|
1681
|
+
# The two consumers want opposite answers about a write whose base nothing names: the conflict rule asks
|
|
1682
|
+
# "could another file's value for this name be wrong?" — yes, so the name stays censused — and this
|
|
1683
|
+
# exemption asks "did this file declare it?" — no, `[Foo].each { |k| k::X = 1 }` declares `Foo::X`.
|
|
1684
|
+
# Granting it un-withheld `flow.always-truthy-condition` for a name the file never wrote.
|
|
1685
|
+
def local_constant_name_set(census, published)
|
|
1686
|
+
return Scope::DiscoveryIndex::EMPTY.local_constant_names if census.nil?
|
|
1687
|
+
|
|
1688
|
+
names = census.declared.select { |name| published.include?(name.split("::").last) }
|
|
1628
1689
|
names.empty? ? Scope::DiscoveryIndex::EMPTY.local_constant_names : names.to_set.freeze
|
|
1629
1690
|
end
|
|
1630
1691
|
|
|
1692
|
+
# Issue #667 — the last segments of the constants this file assigns straight from a constant the
|
|
1693
|
+
# project published and this file does NOT itself declare. `MODE2 = AppConfig::MODE` renames a foreign
|
|
1694
|
+
# declaration, so the local-declaration exemption releasing `MODE2` is the wrong answer: the author can
|
|
1695
|
+
# see the alias but not the value. The source's own foreignness is asked against the census's
|
|
1696
|
+
# `declared` half — the same suffix relation `Scope#published_constant?` uses — so a same-file
|
|
1697
|
+
# `MODE2 = MODE1` pair stays fully reportable.
|
|
1698
|
+
def published_constant_alias_name_set(census, published)
|
|
1699
|
+
return Scope::DiscoveryIndex::EMPTY.published_constant_alias_names if census.nil?
|
|
1700
|
+
|
|
1701
|
+
names = census.aliases.filter_map do |target, source|
|
|
1702
|
+
next unless published.include?(source.split("::").last)
|
|
1703
|
+
next if census_declares_constant?(census.declared, source)
|
|
1704
|
+
|
|
1705
|
+
target.split("::").last
|
|
1706
|
+
end
|
|
1707
|
+
names.empty? ? Scope::DiscoveryIndex::EMPTY.published_constant_alias_names : names.to_set.freeze
|
|
1708
|
+
end
|
|
1709
|
+
|
|
1710
|
+
def census_declares_constant?(declared, reference)
|
|
1711
|
+
return true if declared.include?(reference)
|
|
1712
|
+
|
|
1713
|
+
suffix = "::#{reference}"
|
|
1714
|
+
declared.any? { |name| name.end_with?(suffix) }
|
|
1715
|
+
end
|
|
1716
|
+
|
|
1717
|
+
# Issue #667 — `{class name => Set[ivar name]}` for an ivar whose class-ivar seed is a copy of a
|
|
1718
|
+
# foreign published constant (`@mode = AppConfig::MODE` in `initialize`). The cross-method half of the
|
|
1719
|
+
# copy provenance: the write and the `@mode == :production` that reads it sit in different method
|
|
1720
|
+
# bodies, so no flow edge connects them and the fact has to be censused per class, exactly as the
|
|
1721
|
+
# ADR-58 nil seed it rides beside is. Runs only when the project published something.
|
|
1722
|
+
def seed_published_constant_ivars(seeded_scope, root)
|
|
1723
|
+
return seeded_scope if seeded_scope.published_constant_names.empty?
|
|
1724
|
+
|
|
1725
|
+
table = {}
|
|
1726
|
+
walk_published_constant_ivars(root, [], seeded_scope, table)
|
|
1727
|
+
return seeded_scope if table.empty?
|
|
1728
|
+
|
|
1729
|
+
seeded_scope.with_discovery(
|
|
1730
|
+
seeded_scope.discovery.with(published_constant_ivars: table.transform_values(&:freeze).freeze)
|
|
1731
|
+
)
|
|
1732
|
+
end
|
|
1733
|
+
|
|
1734
|
+
def walk_published_constant_ivars(node, qualified_prefix, scope, table)
|
|
1735
|
+
return unless node.is_a?(Prism::Node)
|
|
1736
|
+
|
|
1737
|
+
case node
|
|
1738
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
1739
|
+
child_prefix = Source::ConstantPath.declaration_prefix(qualified_prefix, node.constant_path)
|
|
1740
|
+
if child_prefix
|
|
1741
|
+
walk_published_constant_ivars(node.body, child_prefix, scope, table) if node.body
|
|
1742
|
+
return
|
|
1743
|
+
end
|
|
1744
|
+
when Prism::DefNode
|
|
1745
|
+
# A `def self.…` body writes the singleton's ivars, which take no instance seed at all.
|
|
1746
|
+
return unless node.receiver.nil?
|
|
1747
|
+
when Prism::InstanceVariableWriteNode
|
|
1748
|
+
record_published_constant_ivar(node, qualified_prefix, scope, table)
|
|
1749
|
+
end
|
|
1750
|
+
|
|
1751
|
+
node.rigor_each_child { |child| walk_published_constant_ivars(child, qualified_prefix, scope, table) }
|
|
1752
|
+
end
|
|
1753
|
+
|
|
1754
|
+
# The class-ivar accumulator (already seeded when this runs) is the pre-gate: an ivar whose seed is not
|
|
1755
|
+
# a `Type::Constant` can never fold a predicate, so there is nothing for the mark to withhold and the
|
|
1756
|
+
# guard's one interprocedural hop is not worth paying for it.
|
|
1757
|
+
def record_published_constant_ivar(node, qualified_prefix, scope, table)
|
|
1758
|
+
return if qualified_prefix.empty?
|
|
1759
|
+
|
|
1760
|
+
class_name = qualified_prefix.join("::")
|
|
1761
|
+
return unless scope.class_ivars_for(class_name)[node.name].is_a?(Type::Constant)
|
|
1762
|
+
return unless Analysis::CheckRules::PublishedConstantGuard.rooted?(node.value, scope)
|
|
1763
|
+
|
|
1764
|
+
(table[class_name] ||= Set.new) << node.name
|
|
1765
|
+
end
|
|
1766
|
+
|
|
1631
1767
|
def merge_seeded_constants(seeded, per_file)
|
|
1632
1768
|
return per_file if seeded.nil? || seeded.empty?
|
|
1633
1769
|
|
|
@@ -1707,19 +1843,40 @@ module Rigor
|
|
|
1707
1843
|
# there is still the enclosing declaration's and every walk below this stays as it was).
|
|
1708
1844
|
#
|
|
1709
1845
|
# {OPAQUE_SELF} where the new `self` reaches no class or module name: a `class_eval` receiver that is
|
|
1710
|
-
# not a static constant path, an {OPAQUE_SELF_BLOCK_CALLS} body, and
|
|
1711
|
-
# `Module.new` / `Struct.new` / `Data.define` block
|
|
1712
|
-
#
|
|
1713
|
-
#
|
|
1714
|
-
#
|
|
1715
|
-
|
|
1846
|
+
# not a static constant path, an {OPAQUE_SELF_BLOCK_CALLS} body, and a `Class.new { … }` /
|
|
1847
|
+
# `Module.new` / `Struct.new` / `Data.define` block no enclosing constant write names. Declining there
|
|
1848
|
+
# costs a resolution the engine never had; naming the LEXICAL enclosure instead is the guess that fires,
|
|
1849
|
+
# because it is the one name Ruby is guaranteed NOT to have written.
|
|
1850
|
+
#
|
|
1851
|
+
# `meta_owner` is the name such a write DOES give the block's class, threaded one hop down by the
|
|
1852
|
+
# caller ({#meta_new_block_owner}) ([#710](https://github.com/rigortype/rigor/issues/710)). It is
|
|
1853
|
+
# supplied only by the publication census; the typed walk returns at a `ConstantWriteNode` without
|
|
1854
|
+
# descending into its rvalue, so no `self::` write inside one reaches that walk at all.
|
|
1855
|
+
def rebound_block_self(node, qualified_prefix, default_scope = nil, meta_owner = nil)
|
|
1716
1856
|
return nil unless node.is_a?(Prism::CallNode) && node.block.is_a?(Prism::BlockNode)
|
|
1717
|
-
return OPAQUE_SELF if OPAQUE_SELF_BLOCK_CALLS.include?(node.name)
|
|
1857
|
+
return OPAQUE_SELF if OPAQUE_SELF_BLOCK_CALLS.include?(node.name)
|
|
1858
|
+
return meta_owner || OPAQUE_SELF if meta_new_constant_rvalue?(node)
|
|
1718
1859
|
return nil unless SELF_REBINDING_EVAL_CALLS.include?(node.name)
|
|
1719
1860
|
|
|
1720
1861
|
eval_receiver_self(node.receiver, qualified_prefix, default_scope)
|
|
1721
1862
|
end
|
|
1722
1863
|
|
|
1864
|
+
# Issue #710 — the qualified name a `Klass = Class.new { … }` gives the block's class, or nil when
|
|
1865
|
+
# `node` is not that form. Ruby names the constructed class after the constant it is first assigned to,
|
|
1866
|
+
# so `self::X = 7` in the block writes `Klass::X` and nothing about it is opaque. The recognition is
|
|
1867
|
+
# {#meta_new_block_body}'s, shared with the block-as-method walk.
|
|
1868
|
+
#
|
|
1869
|
+
# A constant PATH write (`N::Made = Class.new { … }`) keeps the opaque answer even though that recognition
|
|
1870
|
+
# now takes it ([#703](https://github.com/rigortype/rigor/issues/703)). This census keys a path write AS
|
|
1871
|
+
# WRITTEN ({#constant_path_write_name}) rather than through the nesting, so naming the block's class here
|
|
1872
|
+
# would publish a `self::X = …` inside it under a name Ruby does not give it; declining suppresses the
|
|
1873
|
+
# name instead, which is the gradual direction. Moving it belongs with moving the census's own path key.
|
|
1874
|
+
def meta_new_block_owner(node, qualified_prefix)
|
|
1875
|
+
return nil unless node.is_a?(Prism::ConstantWriteNode) && meta_new_block_body(node)
|
|
1876
|
+
|
|
1877
|
+
qualified_write_name(qualified_prefix, node.name.to_s)
|
|
1878
|
+
end
|
|
1879
|
+
|
|
1723
1880
|
# The `self` a `Recv.class_eval { … }` block runs under. An implicit or literal `self` receiver leaves
|
|
1724
1881
|
# `self` where it was — nil, so nothing about the walk changes. A static constant path resolves the way
|
|
1725
1882
|
# a path write's own namespace resolves ({#resolved_write_namespace}); anything else is opaque. A nil
|
|
@@ -1944,7 +2101,7 @@ module Rigor
|
|
|
1944
2101
|
end
|
|
1945
2102
|
end
|
|
1946
2103
|
|
|
1947
|
-
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
2104
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
1948
2105
|
# Combined `walk_methods` + `walk_def_nodes` descent. The two walks had identical class / module / singleton-class
|
|
1949
2106
|
# / meta-block traversals and both stopped at `DefNode`; the only divergences are leaf actions (recorded into the
|
|
1950
2107
|
# right accumulator) and the original `walk_methods` returning at `AliasMethodNode` (its symbol-only children
|
|
@@ -1972,9 +2129,9 @@ module Rigor
|
|
|
1972
2129
|
return
|
|
1973
2130
|
end
|
|
1974
2131
|
end
|
|
1975
|
-
when Prism::ConstantWriteNode
|
|
1976
|
-
|
|
1977
|
-
|
|
2132
|
+
when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode
|
|
2133
|
+
child_prefix = meta_new_body_prefix(node, qualified_prefix)
|
|
2134
|
+
if child_prefix
|
|
1978
2135
|
record_meta_members(node.value, child_prefix, methods_acc)
|
|
1979
2136
|
walk_methods_and_def_nodes(meta_new_block_body(node), child_prefix, false, methods_acc, def_nodes_acc,
|
|
1980
2137
|
source_path)
|
|
@@ -2087,7 +2244,7 @@ module Rigor
|
|
|
2087
2244
|
Source::ConstantPath.qualified_name(expression.target)
|
|
2088
2245
|
end
|
|
2089
2246
|
end
|
|
2090
|
-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
2247
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
2091
2248
|
|
|
2092
2249
|
# v0.1.2 — when a `Const = Data.define(*sym) do ... end` / `Const = Struct.new(*sym) do ... end` constant write
|
|
2093
2250
|
# carries a block, the block body holds method overrides whose canonical class is `Const`. Survey item (e)
|
|
@@ -2095,8 +2252,15 @@ module Rigor
|
|
|
2095
2252
|
# ADR-16 Tier A "block-as-method" idiom at constant-write position. Returns the block body node (a
|
|
2096
2253
|
# `Prism::StatementsNode`) when the rvalue matches; nil otherwise. Used by `walk_methods` / `walk_def_nodes` to
|
|
2097
2254
|
# push `Const` onto the qualified prefix before recursing.
|
|
2255
|
+
#
|
|
2256
|
+
# Issue [#703](https://github.com/rigortype/rigor/issues/703) — a PATH write is the same idiom.
|
|
2257
|
+
# `Holder::Thing = Struct.new(:a) do … end` inside a module is ordinary Ruby, and declining it here lost the
|
|
2258
|
+
# factory registration, the member layout and the block's own defs together: the constant answered
|
|
2259
|
+
# `singleton(Struct)` and its instance `Struct`, a class RBS knows, so every member read and every override
|
|
2260
|
+
# call fired `call.undefined-method` on code Ruby runs. The name the write gives the class is
|
|
2261
|
+
# {#meta_new_child_prefix}'s, so every walk that pushes it agrees.
|
|
2098
2262
|
def meta_new_block_body(node)
|
|
2099
|
-
return nil unless node.is_a?(Prism::ConstantWriteNode)
|
|
2263
|
+
return nil unless node.is_a?(Prism::ConstantWriteNode) || node.is_a?(Prism::ConstantPathWriteNode)
|
|
2100
2264
|
|
|
2101
2265
|
rvalue = node.value
|
|
2102
2266
|
return nil unless rvalue.is_a?(Prism::CallNode)
|
|
@@ -2108,6 +2272,36 @@ module Rigor
|
|
|
2108
2272
|
rvalue.block&.body
|
|
2109
2273
|
end
|
|
2110
2274
|
|
|
2275
|
+
# The qualified prefix a meta-new constant write names its class under, given the lexical prefix the write
|
|
2276
|
+
# sits in — the segments every scope-free pre-pass pushes before descending into the block, and the key
|
|
2277
|
+
# {#record_meta_new_constant?} files the discovered class under. Nil for a target that is neither form.
|
|
2278
|
+
#
|
|
2279
|
+
# A PATH write takes exactly what a compact `class Holder::Thing` header at the same position takes
|
|
2280
|
+
# ({Source::ConstantPath.declaration_prefix}), rooted reset included. These pre-passes run before any scope
|
|
2281
|
+
# exists, so the namespace cannot be resolved the way {#constant_path_write_key} resolves it for the typed
|
|
2282
|
+
# table — but the two land on the same name anyway, because registering `Admin::Holder::Thing` here is what
|
|
2283
|
+
# makes `Admin::Holder` a namespace {#resolved_write_namespace} knows.
|
|
2284
|
+
#
|
|
2285
|
+
# Where the namespace is NOT under the enclosure — a top-level `Loner` written as `Loner::Made = Struct.new(…)`
|
|
2286
|
+
# inside `module Admin` — that qualification is a guess, and {#synthesize_namespace_prefixes} then answers a
|
|
2287
|
+
# read of `Loner` there with the `Admin::Loner` it invented. That is the same answer the equivalent
|
|
2288
|
+
# `class Loner::Made` header has always produced, from the same two functions: the two spellings of one
|
|
2289
|
+
# declaration stay consistent rather than one of them carrying a second approximation of its own.
|
|
2290
|
+
def meta_new_child_prefix(node, qualified_prefix)
|
|
2291
|
+
case node
|
|
2292
|
+
when Prism::ConstantWriteNode
|
|
2293
|
+
qualified_prefix + [node.name.to_s]
|
|
2294
|
+
when Prism::ConstantPathWriteNode
|
|
2295
|
+
Source::ConstantPath.declaration_prefix(qualified_prefix, node.target)
|
|
2296
|
+
end
|
|
2297
|
+
end
|
|
2298
|
+
|
|
2299
|
+
# {#meta_new_child_prefix} for the walks that only care about a write carrying a BLOCK — nil where the rvalue
|
|
2300
|
+
# opens none, so a block-less `Thing = Struct.new(:a)` keeps falling through to the ordinary child descent.
|
|
2301
|
+
def meta_new_body_prefix(node, qualified_prefix)
|
|
2302
|
+
meta_new_block_body(node) && meta_new_child_prefix(node, qualified_prefix)
|
|
2303
|
+
end
|
|
2304
|
+
|
|
2111
2305
|
# `class Foo < Data.define(:a, :b)` / `class Bar < Struct.new(:x)` synthesizes reader methods (`a`, `b`, `x`) on
|
|
2112
2306
|
# the subclass that no `def` / `attr_*` declares. Register them in the discovered-methods existence table so an
|
|
2113
2307
|
# implicit-self read of a member inside the class body is known to exist — both for the existing undefined-method
|
|
@@ -2283,9 +2477,9 @@ module Rigor
|
|
|
2283
2477
|
return
|
|
2284
2478
|
end
|
|
2285
2479
|
end
|
|
2286
|
-
when Prism::ConstantWriteNode
|
|
2287
|
-
|
|
2288
|
-
|
|
2480
|
+
when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode
|
|
2481
|
+
child_prefix = meta_new_body_prefix(node, qualified_prefix)
|
|
2482
|
+
if child_prefix
|
|
2289
2483
|
walk_singleton_body(meta_new_block_body(node), child_prefix, false, accumulator)
|
|
2290
2484
|
return
|
|
2291
2485
|
end
|
|
@@ -2450,7 +2644,7 @@ module Rigor
|
|
|
2450
2644
|
# the enclosing cref — `[]` for the first spelling, `["Admin"]` for the second. A class reopened under two
|
|
2451
2645
|
# spellings keeps the LAST one walked, matching how `superclasses` itself merges.
|
|
2452
2646
|
#
|
|
2453
|
-
# @return
|
|
2647
|
+
# @return })] `[superclasses, header_nestings]`
|
|
2454
2648
|
def build_superclass_tables(root, source_path = nil)
|
|
2455
2649
|
accumulator = { superclasses: {}, header_nestings: {} }
|
|
2456
2650
|
walk_class_superclasses(root, [], accumulator, source_path)
|
|
@@ -2489,7 +2683,8 @@ module Rigor
|
|
|
2489
2683
|
# prefix without resetting Ruby's nesting ([#708](https://github.com/rigortype/rigor/issues/708)).
|
|
2490
2684
|
def record_declaration_ancestry(node, nesting, child_prefix, accumulator)
|
|
2491
2685
|
full = child_prefix.join("::")
|
|
2492
|
-
|
|
2686
|
+
names = declared_ancestor_names(node)
|
|
2687
|
+
add_header_nesting(accumulator[:header_nestings], full, nesting, names) if names
|
|
2493
2688
|
return unless node.is_a?(Prism::ClassNode)
|
|
2494
2689
|
|
|
2495
2690
|
accumulator[:superclasses][full] = recorded_ancestor_name(node.superclass) if node.superclass
|
|
@@ -2513,8 +2708,10 @@ module Rigor
|
|
|
2513
2708
|
"::#{name}"
|
|
2514
2709
|
end
|
|
2515
2710
|
|
|
2516
|
-
#
|
|
2517
|
-
# in its own body. Only
|
|
2711
|
+
# The ancestor names this declaration SITE writes — the superclass in its header, and every mixin
|
|
2712
|
+
# argument in its own body — or nil when the site writes none. Only a site that writes one
|
|
2713
|
+
# contributes a header nesting, and the names are what key it: Ruby resolves each ancestor name in
|
|
2714
|
+
# the cref of the site that WROTE it, and a class's sites need not agree on that cref.
|
|
2518
2715
|
#
|
|
2519
2716
|
# Issue #708's review: a per-CLASS union of every site's chain is wrong for a rooted reopen inside
|
|
2520
2717
|
# another namespace. `class ::Foo; def extra = 1; end` written in `module Outer` contributed `["Outer"]`
|
|
@@ -2522,71 +2719,128 @@ module Rigor
|
|
|
2522
2719
|
# Ruby never looks at, ahead of the right one, on a site that named no ancestor at all. A site that
|
|
2523
2720
|
# writes no ancestor name has no ancestor for its cref to govern, so it has nothing to say here.
|
|
2524
2721
|
#
|
|
2722
|
+
# Issue #728: dropping those sites was necessary and not sufficient. When the rooted reopen genuinely
|
|
2723
|
+
# names a mixin — `class ::Foo; include Helper; end` in `module Outer` — the site is recorded, and a
|
|
2724
|
+
# per-class union then hands its `["Outer"]` to `Foo`'s superclass `Base` as well, which is the same
|
|
2725
|
+
# `Outer::Base` on the same repro. Keying by the written name is what separates them: `Base` resolves
|
|
2726
|
+
# in the empty chain of the top-level site that wrote `< Base`, `Helper` in the `["Outer"]` chain of
|
|
2727
|
+
# the site that wrote `include Helper`, which is what Ruby does for each independently.
|
|
2728
|
+
#
|
|
2525
2729
|
# This keeps what the union was FOR. It was added because last-writer-wins let one rails TEST file
|
|
2526
2730
|
# reopening `ActiveRecord::Relation` compactly beat the library declaration, costing the class all nine
|
|
2527
|
-
# of its `include`d modules; that reopen writes no ancestor name either, so it
|
|
2731
|
+
# of its `include`d modules; that reopen writes no ancestor name either, so it contributes nothing
|
|
2528
2732
|
# rather than winning — the protection comes from the recording rule instead of from a merge rule.
|
|
2529
|
-
# Two sites that BOTH write ancestor names still union, which is the conservative reading of an
|
|
2530
|
-
# ambiguity one table cannot represent.
|
|
2531
2733
|
#
|
|
2532
2734
|
# The mixin scan counts only a call whose `self` is the declaration itself, so it stops at every
|
|
2533
2735
|
# construct that rebinds `self`: a nested `class` / `module` (that body's calls belong to the nested
|
|
2534
2736
|
# class), a `class << self` body, and a block `#rebound_block_self` classifies as rebinding — the
|
|
2535
2737
|
# `Class.new` / `Module.new` / `Struct.new` / `Data.define` and `class_eval` family. An `include`
|
|
2536
2738
|
# written in any of those attaches to something other than this class, so treating it as this site's
|
|
2537
|
-
# ancestor name reinstates exactly the defect above with a different spelling.
|
|
2739
|
+
# ancestor name reinstates exactly the defect above with a different spelling. {#walk_class_includes}
|
|
2740
|
+
# classifies the same shapes the same way since [#749](https://github.com/rigortype/rigor/pull/749),
|
|
2741
|
+
# so the two walks agree on which site owns a mixin.
|
|
2538
2742
|
#
|
|
2539
|
-
# It stays structural rather than exhaustive: `self.include M` and `send(:include, M)`
|
|
2540
|
-
#
|
|
2541
|
-
#
|
|
2542
|
-
#
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2743
|
+
# It stays structural rather than exhaustive: `self.include M` and `send(:include, M)` are recorded by
|
|
2744
|
+
# neither walk, so the class's ancestry agrees. `Recv.class_eval { include M }` is the one shape where
|
|
2745
|
+
# they diverge in the surviving direction — the includes walk names `Recv`, this one cannot see the
|
|
2746
|
+
# site from `Recv`'s own declaration — and that is what {#add_header_nesting}'s unkeyed entry answers.
|
|
2747
|
+
def declared_ancestor_names(node)
|
|
2748
|
+
names = []
|
|
2749
|
+
superclass = node.is_a?(Prism::ClassNode) ? node.superclass : nil
|
|
2750
|
+
if superclass
|
|
2751
|
+
recorded = recorded_ancestor_name(superclass)
|
|
2752
|
+
names << recorded if recorded
|
|
2753
|
+
end
|
|
2548
2754
|
body = node.body
|
|
2549
|
-
body ?
|
|
2755
|
+
mixin = body ? collect_mixin_names(body, names) : false
|
|
2756
|
+
return names if superclass || mixin
|
|
2757
|
+
|
|
2758
|
+
nil
|
|
2550
2759
|
end
|
|
2551
2760
|
|
|
2552
|
-
|
|
2761
|
+
# Appends each mixin argument this body names to `names`, and answers whether it writes a mixin call
|
|
2762
|
+
# at all — a `include some_method` writes one while naming nothing renderable, and the site still owns
|
|
2763
|
+
# a chain for the names it does write. Arguments are rendered exactly as {#record_mixin_call} renders
|
|
2764
|
+
# them, so a name recorded here is the same String the includes table stores and the resolver asks for.
|
|
2765
|
+
def collect_mixin_names(node, names)
|
|
2553
2766
|
return false unless node.is_a?(Prism::Node)
|
|
2554
2767
|
return false if node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode) ||
|
|
2555
2768
|
node.is_a?(Prism::SingletonClassNode)
|
|
2556
|
-
return true if node.is_a?(Prism::CallNode) && node.receiver.nil? && MIXIN_CALL_NAMES.include?(node.name)
|
|
2557
2769
|
|
|
2770
|
+
found = mixin_call?(node)
|
|
2771
|
+
record_mixin_names(node, names) if found
|
|
2558
2772
|
rebinds = rebound_block_self(node, EMPTY_PREFIX)
|
|
2559
2773
|
node.rigor_each_child do |child|
|
|
2560
2774
|
next if rebinds && child.is_a?(Prism::BlockNode)
|
|
2561
2775
|
|
|
2562
|
-
|
|
2776
|
+
found = true if collect_mixin_names(child, names)
|
|
2563
2777
|
end
|
|
2564
|
-
|
|
2778
|
+
found
|
|
2565
2779
|
end
|
|
2566
2780
|
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
# chain alone drops a candidate the other one had — and dropping a candidate drops an ancestor edge,
|
|
2571
|
-
# the false-positive direction. Rails is the corpus case: `activerecord` declares
|
|
2572
|
-
# `ActiveRecord::Relation` inside `module ActiveRecord` and one test file reopens it as the compact
|
|
2573
|
-
# `class ActiveRecord::Relation`, so last-writer-wins cost the library class nine included modules.
|
|
2574
|
-
# The union is the conservative reading of an ambiguity the per-class table cannot represent: it
|
|
2575
|
-
# degrades such a class to the pre-fix candidate list rather than to a shorter one.
|
|
2576
|
-
def add_header_nesting(table, name, entries)
|
|
2577
|
-
existing = table[name]
|
|
2578
|
-
return table[name] = entries.freeze if existing.nil?
|
|
2579
|
-
return if entries.all? { |entry| existing.include?(entry) }
|
|
2781
|
+
def mixin_call?(node)
|
|
2782
|
+
node.is_a?(Prism::CallNode) && node.receiver.nil? && MIXIN_CALL_NAMES.include?(node.name)
|
|
2783
|
+
end
|
|
2580
2784
|
|
|
2581
|
-
|
|
2785
|
+
def record_mixin_names(node, names)
|
|
2786
|
+
node.arguments&.arguments&.each do |arg|
|
|
2787
|
+
mod = Source::ConstantPath.qualified_name(arg)
|
|
2788
|
+
names << mod if mod
|
|
2789
|
+
end
|
|
2790
|
+
end
|
|
2791
|
+
|
|
2792
|
+
# Adds one declaration site's header nesting to the per-class table, under each ancestor NAME the site
|
|
2793
|
+
# wrote plus one unkeyed entry.
|
|
2794
|
+
#
|
|
2795
|
+
# Issue #728 — the keyed entries are the answer proper: `Foo`'s superclass `Base` and the `Helper` a
|
|
2796
|
+
# rooted reopen of `Foo` includes are resolved in different crefs by Ruby, and a per-class chain can
|
|
2797
|
+
# only give them the same one. Two sites that write the SAME name still union, most-qualified first,
|
|
2798
|
+
# since one table cannot say which of two spellings a later reader meant.
|
|
2799
|
+
#
|
|
2800
|
+
# The unkeyed entry ({Scope::DiscoveryIndex::UNKEYED_HEADER_NESTING}) is that union over every
|
|
2801
|
+
# ancestor-naming site, and it
|
|
2802
|
+
# answers an ancestor name no site recorded under its own key — a mixin the includes walk attributes
|
|
2803
|
+
# to a class from OUTSIDE its declaration (`Recv.class_eval { include M }`), or a dynamic `include`
|
|
2804
|
+
# argument this walk cannot render. It is the pre-#728 answer, so such a name is unchanged rather than
|
|
2805
|
+
# degraded, and a shorter list there would drop an ancestor edge, the false-positive direction.
|
|
2806
|
+
def add_header_nesting(table, name, entries, ancestor_names)
|
|
2807
|
+
bucket = { Scope::DiscoveryIndex::UNKEYED_HEADER_NESTING => entries }
|
|
2808
|
+
ancestor_names.each { |raw| bucket[raw] = entries }
|
|
2809
|
+
merge_header_nesting_bucket(table, name, bucket)
|
|
2582
2810
|
end
|
|
2583
2811
|
|
|
2584
2812
|
# {#add_header_nesting} over a whole table, for the per-file and cross-file merges.
|
|
2585
2813
|
def merge_header_nestings(target, incoming)
|
|
2586
|
-
incoming.each { |name,
|
|
2814
|
+
incoming.each { |name, bucket| merge_header_nesting_bucket(target, name, bucket) }
|
|
2587
2815
|
target
|
|
2588
2816
|
end
|
|
2589
2817
|
|
|
2818
|
+
# Merges one class's bucket into the table WITHOUT mutating what is already there: a per-file table is
|
|
2819
|
+
# merged into a shallow `dup` of the cross-file seed, whose buckets the seed still owns.
|
|
2820
|
+
def merge_header_nesting_bucket(table, name, incoming)
|
|
2821
|
+
existing = table[name]
|
|
2822
|
+
return table[name] = frozen_bucket(incoming) if existing.nil?
|
|
2823
|
+
|
|
2824
|
+
merged = existing.dup
|
|
2825
|
+
incoming.each do |raw, entries|
|
|
2826
|
+
previous = merged[raw]
|
|
2827
|
+
merged[raw] = previous.nil? ? entries.freeze : union_header_nesting(previous, entries)
|
|
2828
|
+
end
|
|
2829
|
+
table[name] = merged.freeze
|
|
2830
|
+
end
|
|
2831
|
+
|
|
2832
|
+
def frozen_bucket(bucket)
|
|
2833
|
+
return bucket if bucket.frozen?
|
|
2834
|
+
|
|
2835
|
+
bucket.each_value(&:freeze).freeze
|
|
2836
|
+
end
|
|
2837
|
+
|
|
2838
|
+
def union_header_nesting(existing, entries)
|
|
2839
|
+
return existing if entries.all? { |entry| existing.include?(entry) }
|
|
2840
|
+
|
|
2841
|
+
(existing | entries).sort_by { |entry| [-entry.split("::").size, entry] }.freeze
|
|
2842
|
+
end
|
|
2843
|
+
|
|
2590
2844
|
# #319 — `Class.new(Parent) do ... end` names its superclass in the first positional. Recording it under the
|
|
2591
2845
|
# call site's anonymous name keeps `Parent`'s surface reachable from the block body (whose `self_type` is now
|
|
2592
2846
|
# `Singleton[<anonymous>]`) and from an instance of the resulting class, so giving the anonymous class an
|
|
@@ -2639,8 +2893,9 @@ module Rigor
|
|
|
2639
2893
|
walk_data_member_layouts(node.body, child_prefix, accumulator) if node.body
|
|
2640
2894
|
return
|
|
2641
2895
|
end
|
|
2642
|
-
when Prism::ConstantWriteNode
|
|
2643
|
-
|
|
2896
|
+
when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode
|
|
2897
|
+
child_prefix = meta_new_child_prefix(node, qualified_prefix)
|
|
2898
|
+
record_data_member_layout(accumulator, child_prefix, node.value) if child_prefix
|
|
2644
2899
|
end
|
|
2645
2900
|
|
|
2646
2901
|
node.rigor_each_child do |child|
|
|
@@ -2689,8 +2944,9 @@ module Rigor
|
|
|
2689
2944
|
walk_struct_member_layouts(node.body, child_prefix, accumulator) if node.body
|
|
2690
2945
|
return
|
|
2691
2946
|
end
|
|
2692
|
-
when Prism::ConstantWriteNode
|
|
2693
|
-
|
|
2947
|
+
when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode
|
|
2948
|
+
child_prefix = meta_new_child_prefix(node, qualified_prefix)
|
|
2949
|
+
record_struct_member_layout(accumulator, child_prefix, node.value) if child_prefix
|
|
2694
2950
|
end
|
|
2695
2951
|
|
|
2696
2952
|
node.rigor_each_child do |child|
|
|
@@ -2755,7 +3011,8 @@ module Rigor
|
|
|
2755
3011
|
# Issue #728 — `class << self; include M; end` mixes M into the SINGLETON: it contributes class
|
|
2756
3012
|
# methods, not the instance surface this table feeds. Descending with no owner keeps any nested
|
|
2757
3013
|
# declaration walked (a `ClassNode` child sets its own owner) while the mixin calls in the
|
|
2758
|
-
# singleton body record nothing, which is what `extends` is for
|
|
3014
|
+
# singleton body record nothing, which is what `extends` is for — and since #915 the extend walk
|
|
3015
|
+
# does take them, so the form is recorded rather than dropped.
|
|
2759
3016
|
current_class = nil
|
|
2760
3017
|
when Prism::CallNode
|
|
2761
3018
|
record_mixin_call(node, current_class, accumulator)
|
|
@@ -2808,13 +3065,16 @@ module Rigor
|
|
|
2808
3065
|
# order-sensitivity (only SUBSEQUENT defs become module functions) is deliberately over-approximated
|
|
2809
3066
|
# — the extra names only suppress `undefined-method` and enable inference on calls that raise at
|
|
2810
3067
|
# runtime, both the ADR-5-safe direction.
|
|
3068
|
+
#
|
|
3069
|
+
# Issue #915 — plus `class << self; include M; end`, the same singleton ancestor spelled through the
|
|
3070
|
+
# singleton-class body. It folds like an `extend` because Ruby makes it one.
|
|
2811
3071
|
def build_discovered_extends(root)
|
|
2812
3072
|
accumulator = {}
|
|
2813
3073
|
walk_class_extends(root, [], nil, accumulator)
|
|
2814
3074
|
accumulator.transform_values { |mods| mods.uniq.freeze }.freeze
|
|
2815
3075
|
end
|
|
2816
3076
|
|
|
2817
|
-
def walk_class_extends(node, qualified_prefix, current_class, accumulator)
|
|
3077
|
+
def walk_class_extends(node, qualified_prefix, current_class, accumulator, in_singleton: false)
|
|
2818
3078
|
return unless node.is_a?(Prism::Node)
|
|
2819
3079
|
|
|
2820
3080
|
case node
|
|
@@ -2825,18 +3085,35 @@ module Rigor
|
|
|
2825
3085
|
walk_class_extends(node.body, child_prefix, full, accumulator) if node.body
|
|
2826
3086
|
return
|
|
2827
3087
|
end
|
|
3088
|
+
when Prism::SingletonClassNode
|
|
3089
|
+
# Issue #915 — `class << self` opens the enclosing declaration's OWN singleton, so an `include`
|
|
3090
|
+
# written in it is the same singleton ancestor an `extend` in the class body would add. Only the
|
|
3091
|
+
# `self` form is followed: `class << obj` names something this walk cannot resolve to a class.
|
|
3092
|
+
# Nested declarations inside the body take the `ClassNode` branch above and reset the flag with
|
|
3093
|
+
# their own owner, so the marker cannot leak past the singleton body it belongs to.
|
|
3094
|
+
in_singleton = node.expression.is_a?(Prism::SelfNode)
|
|
3095
|
+
current_class = nil unless in_singleton
|
|
2828
3096
|
when Prism::CallNode
|
|
2829
|
-
record_extend_call(node, current_class, accumulator)
|
|
3097
|
+
record_extend_call(node, current_class, accumulator, in_singleton: in_singleton)
|
|
2830
3098
|
end
|
|
2831
3099
|
|
|
2832
3100
|
node.rigor_each_child do |child|
|
|
2833
|
-
walk_class_extends(child, qualified_prefix, current_class, accumulator)
|
|
3101
|
+
walk_class_extends(child, qualified_prefix, current_class, accumulator, in_singleton: in_singleton)
|
|
2834
3102
|
end
|
|
2835
3103
|
end
|
|
2836
3104
|
|
|
2837
|
-
|
|
3105
|
+
# Inside a `class << self` body the mixin calls are the singleton-side ones and `extend` is not:
|
|
3106
|
+
# `class << self; extend M; end` puts M on the singleton's OWN singleton, one level further out than
|
|
3107
|
+
# anything this table describes, and `module_function` in a singleton body is not the scope toggle
|
|
3108
|
+
# {#build_discovered_extends} over-approximates. So the two arms are disjoint rather than additive.
|
|
3109
|
+
def record_extend_call(node, current_class, accumulator, in_singleton: false)
|
|
2838
3110
|
return unless current_class && node.receiver.nil?
|
|
2839
3111
|
|
|
3112
|
+
if in_singleton
|
|
3113
|
+
record_extend_targets(node, current_class, accumulator) if MIXIN_CALL_NAMES.include?(node.name)
|
|
3114
|
+
return
|
|
3115
|
+
end
|
|
3116
|
+
|
|
2840
3117
|
case node.name
|
|
2841
3118
|
when :extend then record_extend_targets(node, current_class, accumulator)
|
|
2842
3119
|
when :module_function
|
|
@@ -2918,9 +3195,9 @@ module Rigor
|
|
|
2918
3195
|
return current_visibility
|
|
2919
3196
|
end
|
|
2920
3197
|
end
|
|
2921
|
-
when Prism::ConstantWriteNode
|
|
2922
|
-
|
|
2923
|
-
|
|
3198
|
+
when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode
|
|
3199
|
+
child_prefix = meta_new_body_prefix(node, qualified_prefix)
|
|
3200
|
+
if child_prefix
|
|
2924
3201
|
walk_method_visibilities(meta_new_block_body(node), child_prefix, false, :public, accumulator)
|
|
2925
3202
|
return current_visibility
|
|
2926
3203
|
end
|
|
@@ -3190,9 +3467,7 @@ module Rigor
|
|
|
3190
3467
|
# `Singleton[Object]` fallback anyway. The residual leak is `Class`-only (`M.new`, `M.superclass`), which mistypes
|
|
3191
3468
|
# only code that raises `NoMethodError` at runtime.
|
|
3192
3469
|
#
|
|
3193
|
-
# @param paths
|
|
3194
|
-
# @param buffer [Rigor::Analysis::BufferBinding, nil]
|
|
3195
|
-
# @return [Hash{String => Rigor::Type::Singleton}]
|
|
3470
|
+
# @param paths — project file paths.
|
|
3196
3471
|
def discovered_classes_for_paths(paths, buffer: nil)
|
|
3197
3472
|
accumulator = {}
|
|
3198
3473
|
paths.each do |path|
|
|
@@ -3220,9 +3495,8 @@ module Rigor
|
|
|
3220
3495
|
# monkey-patch on a core/stdlib/gem class is called cross-file (ADR-17). First write wins, matching `def_nodes`'
|
|
3221
3496
|
# own merge order.
|
|
3222
3497
|
#
|
|
3223
|
-
# @param paths
|
|
3224
|
-
# @
|
|
3225
|
-
# @return [Hash{Symbol => Hash}]
|
|
3498
|
+
# @param paths — project file paths.
|
|
3499
|
+
# @return
|
|
3226
3500
|
# `{ def_nodes:, def_sources:, superclasses:, includes:, class_sources: }`
|
|
3227
3501
|
def discovered_def_index_for_paths(paths, buffer: nil)
|
|
3228
3502
|
acc = new_def_index_accumulator
|
|
@@ -3246,7 +3520,7 @@ module Rigor
|
|
|
3246
3520
|
# all of them (recon §2 dedup). A per-file live index (built + folded, exactly as
|
|
3247
3521
|
# {#discovered_project_index_incremental}'s changed-file branch) yields the live def nodes the signature
|
|
3248
3522
|
# reads their parameter structure from.
|
|
3249
|
-
# @return
|
|
3523
|
+
# @return `{ def_index:, code_fingerprints:, declaration_signatures: }`.
|
|
3250
3524
|
def scan_summary_for_paths(paths, buffer: nil)
|
|
3251
3525
|
acc = new_def_index_accumulator
|
|
3252
3526
|
code_fingerprints = {}
|
|
@@ -3436,9 +3710,8 @@ module Rigor
|
|
|
3436
3710
|
# rescue's real target) contributes nothing to either table. The subset-scoped callers
|
|
3437
3711
|
# ({IncrementalSession}, `coverage --protection`) keep calling the individual methods unchanged.
|
|
3438
3712
|
#
|
|
3439
|
-
# @param paths
|
|
3440
|
-
# @
|
|
3441
|
-
# @return [Hash{Symbol => Object}] `{ classes: Hash, def_index: Hash }`.
|
|
3713
|
+
# @param paths — project file paths.
|
|
3714
|
+
# @return `{ classes: Hash, def_index: Hash }`.
|
|
3442
3715
|
def discovered_project_index_for_paths(paths, buffer: nil)
|
|
3443
3716
|
classes = {}
|
|
3444
3717
|
acc = new_def_index_accumulator
|
|
@@ -3451,7 +3724,28 @@ module Rigor
|
|
|
3451
3724
|
# Skip files that fail to parse or read; the per-file analyzer surfaces the parse error separately.
|
|
3452
3725
|
next
|
|
3453
3726
|
end
|
|
3454
|
-
|
|
3727
|
+
finalize_project_index(classes, acc)
|
|
3728
|
+
end
|
|
3729
|
+
|
|
3730
|
+
# The shared tail of the two whole-project discovery walks: settles the def-index and re-anchors the
|
|
3731
|
+
# `classes` table it accumulated beside it.
|
|
3732
|
+
def finalize_project_index(classes, acc)
|
|
3733
|
+
def_index = finalize_def_index(acc)
|
|
3734
|
+
{ classes: rename_compact_classes(classes, def_index[:compact_header_renames]).freeze,
|
|
3735
|
+
def_index: def_index }
|
|
3736
|
+
end
|
|
3737
|
+
|
|
3738
|
+
# Issue #722 residue 2 — the `classes` table is accumulated beside the def-index rather than inside it,
|
|
3739
|
+
# so it is re-anchored here from the renames the fold settled. The value is rebuilt, not moved: it is a
|
|
3740
|
+
# `Singleton[…]` of the key, and a moved key with the old singleton inside would type `Outer::Leaf` as
|
|
3741
|
+
# `Wrap::Outer::Leaf` at every reference.
|
|
3742
|
+
def rename_compact_classes(classes, renames)
|
|
3743
|
+
return classes if renames.nil? || renames.empty?
|
|
3744
|
+
|
|
3745
|
+
classes.each_with_object({}) do |(name, type), out|
|
|
3746
|
+
renamed = rename_compact_name(renames, name)
|
|
3747
|
+
out[renamed] = renamed == name ? type : Type::Combinator.singleton_of(renamed)
|
|
3748
|
+
end
|
|
3455
3749
|
end
|
|
3456
3750
|
|
|
3457
3751
|
# ADR-85 WD2 — the incremental cross-file discovery pass. The bundle-driven twin of
|
|
@@ -3468,10 +3762,9 @@ module Rigor
|
|
|
3468
3762
|
# lazily. On a cold run (`seed_bundles` empty) every file is re-walked, so the index is entirely live —
|
|
3469
3763
|
# identical to {#discovered_project_index_for_paths} — while the bundles are built for the next run.
|
|
3470
3764
|
#
|
|
3471
|
-
# @param paths
|
|
3472
|
-
# @param seed_bundles
|
|
3473
|
-
# @
|
|
3474
|
-
# @return [Hash{Symbol => Object}] `{ classes:, def_index:, bundles: }`.
|
|
3765
|
+
# @param paths — project file paths, in canonical order.
|
|
3766
|
+
# @param seed_bundles — the prior run's per-file bundles, keyed by logical path.
|
|
3767
|
+
# @return `{ classes:, def_index:, bundles: }`.
|
|
3475
3768
|
def discovered_project_index_incremental(paths, seed_bundles:, buffer: nil)
|
|
3476
3769
|
classes = {}
|
|
3477
3770
|
acc = new_def_index_accumulator
|
|
@@ -3500,7 +3793,7 @@ module Rigor
|
|
|
3500
3793
|
# Skip files that fail to parse / read; the per-file analyzer surfaces the parse error separately.
|
|
3501
3794
|
next
|
|
3502
3795
|
end
|
|
3503
|
-
|
|
3796
|
+
finalize_project_index(classes, acc).merge(bundles: bundles)
|
|
3504
3797
|
end
|
|
3505
3798
|
|
|
3506
3799
|
# Builds ONE file's isolated def-index contribution (live `Prism::DefNode`s) by folding it into a fresh
|
|
@@ -3567,6 +3860,9 @@ module Rigor
|
|
|
3567
3860
|
accumulate_module_lists(acc[:includes], file_index[:includes])
|
|
3568
3861
|
accumulate_module_lists(acc[:extends], file_index[:extends] || {})
|
|
3569
3862
|
file_index[:class_sources].each { |cn, files| (acc[:class_sources][cn] ||= Set.new).merge(files) }
|
|
3863
|
+
# Issue #722 residue 2 — a pre-#722 seed bundle carries no candidates; the SCHEMA bump makes such a
|
|
3864
|
+
# blob a cold rebuild, but default so any in-flight fold stays total.
|
|
3865
|
+
acc[:compact_headers].merge!(file_index[:compact_headers] || {})
|
|
3570
3866
|
acc[:data_member_layouts].merge!(file_index[:data_member_layouts])
|
|
3571
3867
|
acc[:struct_member_layouts].merge!(file_index[:struct_member_layouts])
|
|
3572
3868
|
end
|
|
@@ -3605,6 +3901,9 @@ module Rigor
|
|
|
3605
3901
|
method_visibilities: file_index[:method_visibilities],
|
|
3606
3902
|
methods: file_index[:methods],
|
|
3607
3903
|
class_source_names: file_index[:class_sources].keys,
|
|
3904
|
+
# Issue #722 residue 2 — plain data, so the bundle stays Marshal-clean and a warm incremental file
|
|
3905
|
+
# re-anchors its compact headers the way a cold walk of it does.
|
|
3906
|
+
compact_headers: file_index[:compact_headers],
|
|
3608
3907
|
# Issue #644 — the file's publication census (`name => [literal] | :unpublishable`; the path is
|
|
3609
3908
|
# the bundle key). Plain data, so the bundle stays Marshal-clean.
|
|
3610
3909
|
constant_writes: file_index[:constant_writes].transform_values { |by_path| by_path.values.first },
|
|
@@ -3632,6 +3931,7 @@ module Rigor
|
|
|
3632
3931
|
method_visibilities: bundle[:method_visibilities],
|
|
3633
3932
|
methods: bundle[:methods],
|
|
3634
3933
|
class_sources: bundle[:class_source_names].to_h { |name| [name, Set[path]] },
|
|
3934
|
+
compact_headers: bundle[:compact_headers] || {},
|
|
3635
3935
|
# Issue #644 — a pre-#644 bundle carries no census; the SCHEMA bump makes such a blob a cold
|
|
3636
3936
|
# rebuild, but default so any in-flight fold stays total.
|
|
3637
3937
|
constant_writes: (bundle[:constant_writes] || {}).transform_values { |descriptor| { path => descriptor } },
|
|
@@ -3669,12 +3969,19 @@ module Rigor
|
|
|
3669
3969
|
singleton_def_nodes: {}, def_sources: {}, singleton_def_sources: {},
|
|
3670
3970
|
superclasses: {}, header_nestings: {}, includes: {}, extends: {}, method_visibilities: {}, methods: {},
|
|
3671
3971
|
class_sources: {},
|
|
3972
|
+
# Issue #722 residue 2 — compact-header re-anchor candidates, adjudicated in {#finalize_def_index}.
|
|
3973
|
+
compact_headers: {},
|
|
3672
3974
|
constant_writes: {},
|
|
3673
3975
|
data_member_layouts: {}, struct_member_layouts: {} }
|
|
3674
3976
|
end
|
|
3675
3977
|
|
|
3676
3978
|
# Post-processes and freezes a fully-folded def-index accumulator.
|
|
3677
3979
|
def finalize_def_index(acc)
|
|
3980
|
+
# Issue #722 residue 2 — settle the compact-header keys FIRST: every whole-project pass below reads
|
|
3981
|
+
# class-keyed tables, and the renames are only knowable now that `class_sources` spans the project.
|
|
3982
|
+
renames = compact_header_renames(acc[:compact_headers] || {}, acc[:class_sources].keys.to_set)
|
|
3983
|
+
apply_compact_header_renames!(acc, renames) unless renames.empty?
|
|
3984
|
+
acc[:compact_header_renames] = renames
|
|
3678
3985
|
# Issue #644 — resolve the cross-file constant-reassignment rule here, where the whole project's
|
|
3679
3986
|
# write census is known, and turn the surviving literals into their published `Type::Constant`.
|
|
3680
3987
|
acc[:constant_values], acc[:constant_sources] = finalize_constant_writes(acc[:constant_writes])
|
|
@@ -3733,7 +4040,8 @@ module Rigor
|
|
|
3733
4040
|
merge_header_nestings(acc[:header_nestings], header_nestings)
|
|
3734
4041
|
accumulate_module_lists(acc[:includes], includes)
|
|
3735
4042
|
accumulate_module_lists(acc[:extends], build_discovered_extends(root))
|
|
3736
|
-
record_class_sources(acc[:class_sources], path, root, superclasses, includes, file_def_nodes
|
|
4043
|
+
record_class_sources(acc[:class_sources], path, root, superclasses, includes, file_def_nodes,
|
|
4044
|
+
acc[:compact_headers])
|
|
3737
4045
|
merge_constant_literal_tables(acc, root, path)
|
|
3738
4046
|
merge_class_keyed_index_tables(acc, root, file_methods)
|
|
3739
4047
|
merge_member_layout_tables(acc, root)
|
|
@@ -3773,9 +4081,9 @@ module Rigor
|
|
|
3773
4081
|
# ancestry edges. {Scope#superclass_of} / {Scope#includes_of} record this set when resolving the edge during
|
|
3774
4082
|
# dependency recording (ADR-46). The class-declaration walk (`collect_class_decls`) catches bodyless / def-less
|
|
3775
4083
|
# reopenings the other three builders miss.
|
|
3776
|
-
def record_class_sources(class_sources, path, root, superclasses, includes, file_def_nodes)
|
|
4084
|
+
def record_class_sources(class_sources, path, root, superclasses, includes, file_def_nodes, compacts = nil)
|
|
3777
4085
|
names = Set.new
|
|
3778
|
-
collect_class_decls(root, [], decls = {})
|
|
4086
|
+
collect_class_decls(root, [], decls = {}, compacts)
|
|
3779
4087
|
names.merge(decls.keys)
|
|
3780
4088
|
names.merge(superclasses.keys)
|
|
3781
4089
|
names.merge(includes.keys)
|
|
@@ -3823,10 +4131,25 @@ module Rigor
|
|
|
3823
4131
|
#
|
|
3824
4132
|
# A name written TWICE in one file is unpublishable too, which is what makes a conditional
|
|
3825
4133
|
# `X = :a if c` / `X = :b` pair gradual rather than pinned to whichever arm the walk saw first.
|
|
3826
|
-
def constant_writes_for_file(root)
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
4134
|
+
def constant_writes_for_file(root) = constant_write_census(root).writes
|
|
4135
|
+
|
|
4136
|
+
# The tables one file's publication census fills in a single walk, carried together so the walk's two
|
|
4137
|
+
# `self`-tracking parameters stay within the signature budget. `writes` is the census itself; `seen`
|
|
4138
|
+
# the names already recorded, so a repeat write retracts whatever either rvalue was; `declared` the
|
|
4139
|
+
# subset whose write NAMES its target, which is the only half {Scope#local_constant_names} may exempt
|
|
4140
|
+
# ([#710](https://github.com/rigortype/rigor/issues/710)).
|
|
4141
|
+
# `aliases` is `{qualified target name => the constant reference its rvalue names}` for the one
|
|
4142
|
+
# unpublishable rvalue shape whose provenance is still knowable ([#667](https://github.com/rigortype/rigor/issues/667)):
|
|
4143
|
+
# a plain `MODE2 = AppConfig::MODE`. The census cannot publish a VALUE for it — resolving the source
|
|
4144
|
+
# is the typed walk's job, not this syntactic one — but it can record that the name is a rename of
|
|
4145
|
+
# another, which is all the withholding guard needs.
|
|
4146
|
+
CensusTables = Data.define(:writes, :seen, :declared, :aliases)
|
|
4147
|
+
private_constant :CensusTables
|
|
4148
|
+
|
|
4149
|
+
def constant_write_census(root)
|
|
4150
|
+
tables = CensusTables.new(writes: {}, seen: Set.new, declared: Set.new, aliases: {})
|
|
4151
|
+
walk_constant_write_census(root, [], tables)
|
|
4152
|
+
tables
|
|
3830
4153
|
end
|
|
3831
4154
|
|
|
3832
4155
|
# Mirrors {#walk_constant_writes}'s traversal for the forms it shares — the lexical class/module prefix
|
|
@@ -3835,60 +4158,66 @@ module Rigor
|
|
|
3835
4158
|
# `||=` constant write, a `ConstantTargetNode` under a `MultiWriteNode` (`A, B = :x, :y`), a `self::X =`
|
|
3836
4159
|
# whose target renders no qualified name, and the inner write of a chain (`C = D = :x`), which is why a
|
|
3837
4160
|
# `ConstantWriteNode` descends into its own rvalue instead of returning.
|
|
3838
|
-
def walk_constant_write_census(node, qualified_prefix,
|
|
4161
|
+
def walk_constant_write_census(node, qualified_prefix, tables, self_owner = nil, meta_owner = nil)
|
|
3839
4162
|
return unless node.is_a?(Prism::Node)
|
|
3840
4163
|
|
|
3841
4164
|
case node
|
|
3842
4165
|
when Prism::ClassNode, Prism::ModuleNode
|
|
3843
4166
|
child_prefix = Source::ConstantPath.declaration_prefix(qualified_prefix, node.constant_path)
|
|
3844
4167
|
if child_prefix
|
|
3845
|
-
walk_constant_write_census(node.body, child_prefix,
|
|
4168
|
+
walk_constant_write_census(node.body, child_prefix, tables) if node.body
|
|
3846
4169
|
return
|
|
3847
4170
|
end
|
|
3848
4171
|
else
|
|
3849
|
-
census_constant_write(node, qualified_prefix,
|
|
4172
|
+
census_constant_write(node, qualified_prefix, tables, self_owner)
|
|
3850
4173
|
end
|
|
3851
4174
|
|
|
3852
|
-
rebound = rebound_block_self(node, qualified_prefix)
|
|
4175
|
+
rebound = rebound_block_self(node, qualified_prefix, nil, meta_owner)
|
|
4176
|
+
# A `ConstantWriteNode`'s only child is its rvalue, so this reaches exactly the call whose block the
|
|
4177
|
+
# constant names — and nil everywhere else, leaving every other descent as it was.
|
|
4178
|
+
child_meta_owner = meta_new_block_owner(node, qualified_prefix)
|
|
3853
4179
|
node.rigor_each_child do |child|
|
|
3854
4180
|
owner = rebound && child.is_a?(Prism::BlockNode) ? rebound : self_owner
|
|
3855
|
-
walk_constant_write_census(child, qualified_prefix,
|
|
4181
|
+
walk_constant_write_census(child, qualified_prefix, tables, owner, child_meta_owner)
|
|
3856
4182
|
end
|
|
3857
4183
|
end
|
|
3858
4184
|
|
|
3859
4185
|
# Censuses `node` when it is any constant-assigning form. Only a plain `ConstantWriteNode` /
|
|
3860
4186
|
# `ConstantPathWriteNode` can contribute a VALUE; every other form is recorded unpublishable, which
|
|
3861
4187
|
# suppresses publication of the name exactly as a second file's write would.
|
|
3862
|
-
def census_constant_write(node, qualified_prefix,
|
|
4188
|
+
def census_constant_write(node, qualified_prefix, tables, self_owner = nil)
|
|
3863
4189
|
case node
|
|
3864
4190
|
when Prism::ConstantWriteNode
|
|
3865
4191
|
record_constant_write_census(qualified_write_name(qualified_prefix, node.name.to_s),
|
|
3866
|
-
constant_literal_value(node.value),
|
|
4192
|
+
constant_literal_value(node.value), tables,
|
|
4193
|
+
alias_of: constant_alias_source(node.value))
|
|
3867
4194
|
when Prism::ConstantPathWriteNode
|
|
3868
4195
|
record_constant_write_census(constant_path_write_name(node.target, qualified_prefix, self_owner),
|
|
3869
|
-
constant_path_write_literal(node, self_owner),
|
|
4196
|
+
constant_path_write_literal(node, self_owner), tables,
|
|
4197
|
+
nameable: nameable_write_target?(node.target, self_owner),
|
|
4198
|
+
alias_of: constant_alias_source(node.value))
|
|
3870
4199
|
when Prism::ConstantOperatorWriteNode, Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode
|
|
3871
|
-
record_constant_write_census(qualified_write_name(qualified_prefix, node.name.to_s), nil,
|
|
4200
|
+
record_constant_write_census(qualified_write_name(qualified_prefix, node.name.to_s), nil, tables)
|
|
3872
4201
|
when Prism::ConstantPathOperatorWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathAndWriteNode
|
|
3873
4202
|
record_constant_write_census(constant_path_write_name(node.target, qualified_prefix, self_owner),
|
|
3874
|
-
nil,
|
|
4203
|
+
nil, tables, nameable: nameable_write_target?(node.target, self_owner))
|
|
3875
4204
|
when Prism::MultiWriteNode
|
|
3876
|
-
census_multi_write_constants(node, qualified_prefix,
|
|
4205
|
+
census_multi_write_constants(node, qualified_prefix, tables, self_owner)
|
|
3877
4206
|
end
|
|
3878
4207
|
end
|
|
3879
4208
|
|
|
3880
4209
|
# `A, B = :x, :y` and `A, *rest = …`. A destructured element's value is a projection of the right-hand
|
|
3881
4210
|
# side, which this syntactic walk does not evaluate, so every constant target is censused unpublishable.
|
|
3882
|
-
def census_multi_write_constants(node, qualified_prefix,
|
|
4211
|
+
def census_multi_write_constants(node, qualified_prefix, tables, self_owner = nil)
|
|
3883
4212
|
targets = node.lefts + node.rights
|
|
3884
4213
|
targets << node.rest if node.rest
|
|
3885
4214
|
targets.each do |target|
|
|
3886
4215
|
case target
|
|
3887
4216
|
when Prism::ConstantTargetNode
|
|
3888
|
-
record_constant_write_census(qualified_write_name(qualified_prefix, target.name.to_s), nil,
|
|
4217
|
+
record_constant_write_census(qualified_write_name(qualified_prefix, target.name.to_s), nil, tables)
|
|
3889
4218
|
when Prism::ConstantPathTargetNode
|
|
3890
4219
|
record_constant_write_census(constant_path_write_name(target, qualified_prefix, self_owner),
|
|
3891
|
-
nil,
|
|
4220
|
+
nil, tables, nameable: nameable_write_target?(target, self_owner))
|
|
3892
4221
|
end
|
|
3893
4222
|
end
|
|
3894
4223
|
end
|
|
@@ -3901,25 +4230,37 @@ module Rigor
|
|
|
3901
4230
|
# censuses key a path write differently until that is settled.
|
|
3902
4231
|
# `self::BAR = …` names whatever `self` is at that point, which the walk carries in `self_owner`
|
|
3903
4232
|
# ([#705](https://github.com/rigortype/rigor/issues/705)) — the enclosing lexical namespace in an
|
|
3904
|
-
# ordinary body, the receiver inside a `class_eval` block
|
|
4233
|
+
# ordinary body, the receiver inside a `class_eval` block, and the constant a `Klass = Class.new { … }`
|
|
4234
|
+
# assigns the block's class to ({#meta_new_block_owner}). Where that `self` is {OPAQUE_SELF}, and for
|
|
3905
4235
|
# any other dynamic receiver (`klass::BAR = …`), the target is "some class, then `::BAR`" and the name
|
|
3906
|
-
# falls back to the
|
|
3907
|
-
# a
|
|
3908
|
-
#
|
|
4236
|
+
# falls back to the WILDCARD {DYNAMIC_TARGET_PREFIX} key, which {#finalize_constant_writes} expands
|
|
4237
|
+
# into a retraction of every censused name with that last segment
|
|
4238
|
+
# ([#668](https://github.com/rigortype/rigor/issues/668)). The bare last segment — the pre-#668
|
|
4239
|
+
# fallback — was the single name such a write can never create, so filing it there suppressed the one
|
|
4240
|
+
# name the form cannot touch and left published every name it can. The VALUE is what such a write must
|
|
4241
|
+
# not contribute either ({#constant_path_write_literal}), and the fallback name is withheld from the
|
|
4242
|
+
# local-declaration exemption ({#local_constant_name_set}).
|
|
3909
4243
|
def constant_path_write_name(target, qualified_prefix, self_owner = nil)
|
|
3910
4244
|
full = Source::ConstantPath.qualified_name_or_nil(target)
|
|
3911
4245
|
return full if full
|
|
3912
4246
|
|
|
3913
4247
|
base = target.name&.to_s
|
|
3914
4248
|
return nil if base.nil?
|
|
3915
|
-
return base unless target.parent.is_a?(Prism::SelfNode)
|
|
4249
|
+
return dynamic_target_key(base) unless target.parent.is_a?(Prism::SelfNode)
|
|
3916
4250
|
|
|
3917
|
-
self_write_name(qualified_prefix, self_owner, base) || base
|
|
4251
|
+
self_write_name(qualified_prefix, self_owner, base) || dynamic_target_key(base)
|
|
4252
|
+
end
|
|
4253
|
+
|
|
4254
|
+
def dynamic_target_key(segment) = "#{DYNAMIC_TARGET_PREFIX}#{segment}"
|
|
4255
|
+
|
|
4256
|
+
# The last segment a wildcard census key stands for, or nil for an ordinary qualified name.
|
|
4257
|
+
def dynamic_target_segment(name)
|
|
4258
|
+
name.delete_prefix(DYNAMIC_TARGET_PREFIX) if name.start_with?(DYNAMIC_TARGET_PREFIX)
|
|
3918
4259
|
end
|
|
3919
4260
|
|
|
3920
4261
|
# The publishable literal a path write contributes — none, whenever its base is not statically
|
|
3921
4262
|
# nameable ([#705](https://github.com/rigortype/rigor/issues/705)). `[Foo].each { |k| k::X = 1 }`
|
|
3922
|
-
# renders
|
|
4263
|
+
# renders under the wildcard key, and publishing `1` there handed every reader of a project `X` a value
|
|
3923
4264
|
# the program never has: `X == 2` folded to `false`, and in the WRITING file — where
|
|
3924
4265
|
# `Scope#local_constant_names` exempts the name from #644's withholding guard —
|
|
3925
4266
|
# `flow.always-truthy-condition` then fired on correct code.
|
|
@@ -3950,10 +4291,40 @@ module Rigor
|
|
|
3950
4291
|
# Records one censused write. `seen` carries the names this file has already written, so a repeat write
|
|
3951
4292
|
# retracts the publishable descriptor whatever either rvalue was — neither arm of an in-file
|
|
3952
4293
|
# reassignment is ever published, and the name still counts as written.
|
|
3953
|
-
|
|
4294
|
+
#
|
|
4295
|
+
# `nameable` is false only for the wildcard fallback {#constant_path_write_name} takes when nothing
|
|
4296
|
+
# names the write's base. Such a key belongs in `writes` — another file's value for a name it reaches
|
|
4297
|
+
# is not to be trusted — and NOT in `declared`, which answers the opposite question
|
|
4298
|
+
# ([#710](https://github.com/rigortype/rigor/issues/710)). The two are separate tables rather than one
|
|
4299
|
+
# richer descriptor because a name can be written both ways in one file, and then the file DID declare
|
|
4300
|
+
# it however the two writes are ordered.
|
|
4301
|
+
def record_constant_write_census(full, literal, tables, nameable: true, alias_of: nil)
|
|
3954
4302
|
return if full.nil?
|
|
3955
4303
|
|
|
3956
|
-
|
|
4304
|
+
tables.declared << full if nameable
|
|
4305
|
+
first_write = tables.seen.add?(full)
|
|
4306
|
+
tables.writes[full] = first_write ? (literal || CONSTANT_UNPUBLISHABLE) : CONSTANT_UNPUBLISHABLE
|
|
4307
|
+
record_constant_alias_census(full, alias_of, tables, first_write && nameable)
|
|
4308
|
+
end
|
|
4309
|
+
|
|
4310
|
+
# Issue #667 — a name written TWICE is not an alias of anything the walk can name, exactly as it is
|
|
4311
|
+
# unpublishable above; the `delete` is what retracts a first write's record when the second arrives.
|
|
4312
|
+
def record_constant_alias_census(full, alias_of, tables, keep)
|
|
4313
|
+
if keep && alias_of
|
|
4314
|
+
tables.aliases[full] = alias_of
|
|
4315
|
+
else
|
|
4316
|
+
tables.aliases.delete(full)
|
|
4317
|
+
end
|
|
4318
|
+
end
|
|
4319
|
+
|
|
4320
|
+
# The constant reference an rvalue names, or nil when it names none. `AppConfig::MODE` renders
|
|
4321
|
+
# qualified; a path that renders no qualified name falls back to its own last segment, which is all
|
|
4322
|
+
# `Scope#published_constant?` can be asked about anyway.
|
|
4323
|
+
def constant_alias_source(rvalue)
|
|
4324
|
+
case rvalue
|
|
4325
|
+
when Prism::ConstantReadNode then rvalue.name.to_s
|
|
4326
|
+
when Prism::ConstantPathNode then Source::ConstantPath.qualified_name_or_nil(rvalue) || rvalue.name&.to_s
|
|
4327
|
+
end
|
|
3957
4328
|
end
|
|
3958
4329
|
|
|
3959
4330
|
# The frozen-scalar literal fold: `[value]` for a publishable rvalue, nil to decline. The one-element
|
|
@@ -3983,20 +4354,51 @@ module Rigor
|
|
|
3983
4354
|
# fold time) so the fold stays order-independent, which is what lets the ADR-85 incremental path re-fold
|
|
3984
4355
|
# a changed file's contribution in place.
|
|
3985
4356
|
#
|
|
3986
|
-
#
|
|
4357
|
+
# A {DYNAMIC_TARGET_PREFIX} key is not a name and never publishes; it is expanded by
|
|
4358
|
+
# {#apply_dynamic_target_writes} into a retraction over the names it could have written.
|
|
4359
|
+
#
|
|
4360
|
+
# @return `[{name => Type}, {name => Set[path]}]`.
|
|
3987
4361
|
def finalize_constant_writes(writes)
|
|
3988
4362
|
values = {}
|
|
3989
4363
|
sources = {}
|
|
4364
|
+
dynamic = {}
|
|
3990
4365
|
writes.each do |name, by_path|
|
|
3991
|
-
|
|
4366
|
+
segment = dynamic_target_segment(name)
|
|
4367
|
+
next dynamic[segment] = by_path.keys.to_set if segment
|
|
4368
|
+
|
|
4369
|
+
sources[name] = by_path.keys.to_set
|
|
3992
4370
|
next unless by_path.size == 1
|
|
3993
4371
|
|
|
3994
4372
|
descriptor = by_path.values.first
|
|
3995
4373
|
values[name] = Type::Combinator.constant_of(descriptor.first) if descriptor.is_a?(Array)
|
|
3996
4374
|
end
|
|
4375
|
+
apply_dynamic_target_writes(values, sources, dynamic)
|
|
3997
4376
|
[values, sources]
|
|
3998
4377
|
end
|
|
3999
4378
|
|
|
4379
|
+
# Issue #668 — expands each wildcard census key. `k::LIMIT = 7` can create `Anything::LIMIT`, so it
|
|
4380
|
+
# retracts every censused name whose last segment is `LIMIT` and joins the writing file to that name's
|
|
4381
|
+
# attribution; the bare `LIMIT` is retracted along with the rest, being the one name the analyzer
|
|
4382
|
+
# cannot rule out either (`k` may be `Object`). Over-retracting makes readers gradual, which is the
|
|
4383
|
+
# direction the census's whole decline list takes; leaving a name published that a dynamic write may
|
|
4384
|
+
# already have replaced is the single failure the census exists to prevent.
|
|
4385
|
+
#
|
|
4386
|
+
# The wildcard key survives into the attribution so a producer with no censused name of its own — a
|
|
4387
|
+
# `pre_eval:` publication for a file outside `paths:` — can still see the conflict
|
|
4388
|
+
# ({Analysis::Runner#publishable_pre_eval_constants}).
|
|
4389
|
+
def apply_dynamic_target_writes(values, sources, dynamic)
|
|
4390
|
+
return if dynamic.empty?
|
|
4391
|
+
|
|
4392
|
+
sources.each do |name, paths|
|
|
4393
|
+
writers = dynamic[name.split("::").last]
|
|
4394
|
+
next if writers.nil?
|
|
4395
|
+
|
|
4396
|
+
paths.merge(writers)
|
|
4397
|
+
values.delete(name)
|
|
4398
|
+
end
|
|
4399
|
+
dynamic.each { |segment, paths| sources[dynamic_target_key(segment)] = paths }
|
|
4400
|
+
end
|
|
4401
|
+
|
|
4000
4402
|
# Merges one file's `class → method → DefNode` map into the cross-file `def_nodes` index and records each method's
|
|
4001
4403
|
# first- seen `"path:line"` definition site in `def_sources` (ADR-17 — the un-registered-project-patch signal
|
|
4002
4404
|
# `call.undefined-method` and `rigor triage` key on).
|
|
@@ -4012,7 +4414,7 @@ module Rigor
|
|
|
4012
4414
|
|
|
4013
4415
|
# Cross-file counterpart of `record_declarations` — registers every `class` / `module` declaration under its
|
|
4014
4416
|
# qualified name and descends into the body (so `module Foo; class Bar` registers both `Foo` and `Foo::Bar`).
|
|
4015
|
-
def collect_class_decls(node, qualified_prefix, accumulator)
|
|
4417
|
+
def collect_class_decls(node, qualified_prefix, accumulator, compacts = nil)
|
|
4016
4418
|
return unless node.is_a?(Prism::Node)
|
|
4017
4419
|
|
|
4018
4420
|
case node
|
|
@@ -4021,13 +4423,119 @@ module Rigor
|
|
|
4021
4423
|
if child_prefix
|
|
4022
4424
|
full = child_prefix.join("::")
|
|
4023
4425
|
accumulator[full] = Type::Combinator.singleton_of(full)
|
|
4024
|
-
|
|
4426
|
+
record_compact_header(compacts, qualified_prefix, node.constant_path, full) if compacts
|
|
4427
|
+
return collect_class_decls(node.body, child_prefix, accumulator, compacts) if node.body
|
|
4025
4428
|
end
|
|
4026
4429
|
when Prism::ConstantWriteNode
|
|
4027
4430
|
record_class_new_constant_decl(node, qualified_prefix, accumulator)
|
|
4028
4431
|
end
|
|
4029
4432
|
|
|
4030
|
-
node.rigor_each_child { |child| collect_class_decls(child, qualified_prefix, accumulator) }
|
|
4433
|
+
node.rigor_each_child { |child| collect_class_decls(child, qualified_prefix, accumulator, compacts) }
|
|
4434
|
+
end
|
|
4435
|
+
|
|
4436
|
+
# Issue [#722](https://github.com/rigortype/rigor/issues/722) residue 2 — records what a COMPACT header
|
|
4437
|
+
# written inside an enclosing namespace would name if its leading segment fell through to the top level.
|
|
4438
|
+
# `class Outer::Leaf` inside `module Wrap` is keyed `Wrap::Outer::Leaf` by the per-node
|
|
4439
|
+
# {Source::ConstantPath.declaration_prefix}, but Ruby resolves the leading `Outer` by ORDINARY constant
|
|
4440
|
+
# lookup: it reopens `::Outer::Leaf` unless `Wrap::Outer` exists. Whether it exists is a whole-project
|
|
4441
|
+
# fact, so the candidate is recorded here and adjudicated once the fold knows every declared name
|
|
4442
|
+
# ({#compact_header_renames}).
|
|
4443
|
+
#
|
|
4444
|
+
# @param compacts — `{recorded name => [namespace the header needs, name it falls through to]}`.
|
|
4445
|
+
def record_compact_header(compacts, qualified_prefix, constant_path, full)
|
|
4446
|
+
return if qualified_prefix.empty?
|
|
4447
|
+
return unless constant_path.is_a?(Prism::ConstantPathNode)
|
|
4448
|
+
return if Source::ConstantPath.rooted?(constant_path)
|
|
4449
|
+
|
|
4450
|
+
rendered = Source::ConstantPath.qualified_name(constant_path)
|
|
4451
|
+
return unless rendered&.include?("::")
|
|
4452
|
+
|
|
4453
|
+
compacts[full] = [(qualified_prefix + [rendered.split("::").first]).join("::"), rendered]
|
|
4454
|
+
end
|
|
4455
|
+
|
|
4456
|
+
# Issue #722 residue 2 — the whole-project adjudication of {#record_compact_header}'s candidates:
|
|
4457
|
+
# `{name the walk recorded => name Ruby actually reopens}`.
|
|
4458
|
+
#
|
|
4459
|
+
# A candidate moves only when the project ANSWERS BOTH halves of Ruby's lookup: the namespace the
|
|
4460
|
+
# nesting would supply (`Wrap::Outer`) is declared nowhere, and the top level the segment falls through
|
|
4461
|
+
# to (`Outer`) IS declared. The second half is the false-positive bound, not a convenience — a
|
|
4462
|
+
# `Wrap::Outer` that merely never appears in project source (a gem, an RBS-only class) is a namespace
|
|
4463
|
+
# that DOES exist at runtime, and re-anchoring on its absence would answer with a wrong class where
|
|
4464
|
+
# today's mis-keying only answers with silence.
|
|
4465
|
+
#
|
|
4466
|
+
# `declared_names` is the RAW declared set (`class_sources`), never the table
|
|
4467
|
+
# {#synthesize_namespace_prefixes} has run over: that synthesis invents `Wrap::Outer` from
|
|
4468
|
+
# `Wrap::Outer::Leaf` itself, so consulting it would make every candidate look already-resolved.
|
|
4469
|
+
def compact_header_renames(compacts, declared_names)
|
|
4470
|
+
compacts.each_with_object({}) do |(recorded, (required_namespace, reanchored)), out|
|
|
4471
|
+
next if declared_names.include?(required_namespace)
|
|
4472
|
+
next unless declared_names.include?(reanchored.split("::").first)
|
|
4473
|
+
|
|
4474
|
+
out[recorded] = reanchored
|
|
4475
|
+
end
|
|
4476
|
+
end
|
|
4477
|
+
|
|
4478
|
+
# Rewrites `name` when it is, or is nested under, a re-anchored compact header. Nested declarations
|
|
4479
|
+
# ride the same move: `class Outer::Leaf` inside `module Wrap` carrying a `class Inner` filed the inner
|
|
4480
|
+
# class under `Wrap::Outer::Leaf::Inner`, and it reopens `::Outer::Leaf::Inner` for the same reason.
|
|
4481
|
+
def rename_compact_name(renames, name)
|
|
4482
|
+
return name if renames.empty?
|
|
4483
|
+
|
|
4484
|
+
string = name.to_s
|
|
4485
|
+
renamed = renames[string]
|
|
4486
|
+
return renamed if renamed
|
|
4487
|
+
|
|
4488
|
+
renames.each do |recorded, reanchored|
|
|
4489
|
+
return "#{reanchored}#{string[recorded.length..]}" if string.start_with?("#{recorded}::")
|
|
4490
|
+
end
|
|
4491
|
+
string
|
|
4492
|
+
end
|
|
4493
|
+
|
|
4494
|
+
# Re-keys one class-keyed table. A rename lands a declaration on a key the table may ALREADY hold —
|
|
4495
|
+
# which is the whole point: `class Outer::Leaf` written twice, once at the top level and once inside
|
|
4496
|
+
# `module Wrap`, is one class with both bodies. So the arriving contribution is COMBINED with the
|
|
4497
|
+
# sitting one under the table's own fold semantics rather than replacing it; a plain `transform_keys`
|
|
4498
|
+
# dropped the top-level body's methods outright.
|
|
4499
|
+
def rekey_class_table(table, renames)
|
|
4500
|
+
table.each_with_object({}) do |(name, value), out|
|
|
4501
|
+
renamed = rename_compact_name(renames, name)
|
|
4502
|
+
sitting = out[renamed]
|
|
4503
|
+
out[renamed] = sitting.nil? ? value : combine_rekeyed_entries(sitting, value)
|
|
4504
|
+
end
|
|
4505
|
+
end
|
|
4506
|
+
|
|
4507
|
+
# The per-shape combine {#rekey_class_table} applies. Every class-keyed table's value is a Hash of
|
|
4508
|
+
# per-member entries, a Set or Array of names, or a single scalar fact (a superclass name, a member
|
|
4509
|
+
# layout); the first two union, and a scalar keeps the entry already sitting, matching the first-wins
|
|
4510
|
+
# fold `class_sources` and the `"path:line"` source tables use.
|
|
4511
|
+
def combine_rekeyed_entries(sitting, arriving)
|
|
4512
|
+
case sitting
|
|
4513
|
+
when Hash then sitting.merge(arriving)
|
|
4514
|
+
when Set, Array then sitting | arriving
|
|
4515
|
+
else sitting
|
|
4516
|
+
end
|
|
4517
|
+
end
|
|
4518
|
+
|
|
4519
|
+
# Applies {#compact_header_renames} to every class-keyed table of a folded def-index accumulator, plus
|
|
4520
|
+
# the nesting chains whose entries are class names. Runs before {#finalize_def_index}'s own whole-project
|
|
4521
|
+
# passes so those see the settled keys.
|
|
4522
|
+
def apply_compact_header_renames!(acc, renames)
|
|
4523
|
+
%i[def_nodes singleton_def_nodes def_sources singleton_def_sources superclasses header_nestings
|
|
4524
|
+
includes extends method_visibilities methods class_sources data_member_layouts
|
|
4525
|
+
struct_member_layouts constant_writes].each do |key|
|
|
4526
|
+
acc[key] = rekey_class_table(acc[key], renames)
|
|
4527
|
+
end
|
|
4528
|
+
# A `header_nestings` value is a BUCKET (`raw header → chain`, plus the unkeyed union), not a
|
|
4529
|
+
# chain: mapping the bucket itself turned every entry into a `[raw, chain]` pair rendered as a
|
|
4530
|
+
# string, and the next per-file merge (`merge_header_nesting_bucket`) and every
|
|
4531
|
+
# `Scope#recorded_header_nesting` lookup then raised on an Array where a Hash was expected — an
|
|
4532
|
+
# internal analyzer error on every file of a project with one compact header (#984).
|
|
4533
|
+
acc[:header_nestings] = acc[:header_nestings].transform_values do |bucket|
|
|
4534
|
+
bucket.transform_values { |chain| chain.map { |entry| rename_compact_name(renames, entry) } }
|
|
4535
|
+
end
|
|
4536
|
+
acc[:def_nestings].transform_values! do |chain|
|
|
4537
|
+
chain&.map { |entry| rename_compact_name(renames, entry) }
|
|
4538
|
+
end
|
|
4031
4539
|
end
|
|
4032
4540
|
|
|
4033
4541
|
# T1 (template-corpora survey) — record a class-creating constant write (`Const = Class.new(Super)`, the bare
|
|
@@ -4100,13 +4608,30 @@ module Rigor
|
|
|
4100
4608
|
# remain real references (resolved through the ordinary lexical walk), so we annotate ONLY the topmost path node.
|
|
4101
4609
|
# Nested declarations contribute their fully qualified path: `class A::B; class C; ...` produces `A::B` for the
|
|
4102
4610
|
# outer and `A::B::C` for the inner.
|
|
4103
|
-
|
|
4611
|
+
# @param declared_names — the project's RAW declared-class names, the oracle
|
|
4612
|
+
# {#compact_header_renames} adjudicates this file's compact headers against.
|
|
4613
|
+
def build_declaration_artifacts(root, declared_names = nil)
|
|
4104
4614
|
identity_table = {}.compare_by_identity
|
|
4105
4615
|
discovered = {}
|
|
4106
|
-
record_declarations(root, [], identity_table, discovered
|
|
4616
|
+
record_declarations(root, [], identity_table, discovered,
|
|
4617
|
+
file_compact_header_renames(root, declared_names))
|
|
4107
4618
|
[identity_table.freeze, synthesize_namespace_prefixes(discovered).freeze]
|
|
4108
4619
|
end
|
|
4109
4620
|
|
|
4621
|
+
# Issue #722 residue 2 — this file's share of the fold's re-anchoring, adjudicated against the project
|
|
4622
|
+
# names the fold has already settled. Empty (so the per-node answer stands) whenever the caller has no
|
|
4623
|
+
# project table to consult: a `Scope.empty` probe sees one file and cannot know what else declares
|
|
4624
|
+
# `Wrap::Outer`, and guessing there is the false positive the whole rule is bounded to avoid.
|
|
4625
|
+
def file_compact_header_renames(root, declared_names)
|
|
4626
|
+
return EMPTY_RENAMES if declared_names.nil? || declared_names.empty?
|
|
4627
|
+
|
|
4628
|
+
compacts = {}
|
|
4629
|
+
collect_class_decls(root, [], {}, compacts)
|
|
4630
|
+
return EMPTY_RENAMES if compacts.empty?
|
|
4631
|
+
|
|
4632
|
+
compact_header_renames(compacts, declared_names)
|
|
4633
|
+
end
|
|
4634
|
+
|
|
4110
4635
|
# Issue #528 — every proper prefix of a discovered COMPACT class name is a namespace module that
|
|
4111
4636
|
# provably exists at runtime, even when no `module X` declaration is anywhere in the source
|
|
4112
4637
|
# (Zeitwerk derives it from the directory: mastodon writes `class Api::V1::AccountsController`
|
|
@@ -4132,30 +4657,35 @@ module Rigor
|
|
|
4132
4657
|
discovered.merge(additions)
|
|
4133
4658
|
end
|
|
4134
4659
|
|
|
4135
|
-
def record_declarations(node, qualified_prefix, identity_table, discovered)
|
|
4660
|
+
def record_declarations(node, qualified_prefix, identity_table, discovered, renames = EMPTY_RENAMES)
|
|
4136
4661
|
return unless node.is_a?(Prism::Node)
|
|
4137
4662
|
|
|
4138
4663
|
case node
|
|
4139
4664
|
when Prism::ModuleNode, Prism::ClassNode
|
|
4140
|
-
return if record_class_or_module?(node, qualified_prefix, identity_table, discovered)
|
|
4141
|
-
when Prism::ConstantWriteNode
|
|
4665
|
+
return if record_class_or_module?(node, qualified_prefix, identity_table, discovered, renames)
|
|
4666
|
+
when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode
|
|
4142
4667
|
return if record_meta_new_constant?(node, qualified_prefix, identity_table, discovered)
|
|
4143
4668
|
end
|
|
4144
4669
|
|
|
4145
4670
|
node.rigor_each_child do |child|
|
|
4146
|
-
record_declarations(child, qualified_prefix, identity_table, discovered)
|
|
4671
|
+
record_declarations(child, qualified_prefix, identity_table, discovered, renames)
|
|
4147
4672
|
end
|
|
4148
4673
|
end
|
|
4149
4674
|
|
|
4150
|
-
def record_class_or_module?(node, qualified_prefix, identity_table, discovered)
|
|
4675
|
+
def record_class_or_module?(node, qualified_prefix, identity_table, discovered, renames = EMPTY_RENAMES)
|
|
4151
4676
|
child_prefix = Source::ConstantPath.declaration_prefix(qualified_prefix, node.constant_path)
|
|
4152
4677
|
return false unless child_prefix
|
|
4153
4678
|
|
|
4154
|
-
|
|
4679
|
+
# Issue #722 residue 2 — the per-file twin of the fold's re-anchoring. The file's own declaration
|
|
4680
|
+
# table merges OVER the project seed, so without this a compact header would re-publish the key the
|
|
4681
|
+
# fold already moved and a reference written INSIDE the enclosing namespace would resolve to it
|
|
4682
|
+
# again. The adjudication itself is the project's: `renames` arrives already settled.
|
|
4683
|
+
full = rename_compact_name(renames, child_prefix.join("::"))
|
|
4684
|
+
child_prefix = full.split("::")
|
|
4155
4685
|
singleton = Type::Combinator.singleton_of(full)
|
|
4156
4686
|
identity_table[node.constant_path] = singleton
|
|
4157
4687
|
discovered[full] = singleton
|
|
4158
|
-
record_declarations(node.body, child_prefix, identity_table, discovered) if node.body
|
|
4688
|
+
record_declarations(node.body, child_prefix, identity_table, discovered, renames) if node.body
|
|
4159
4689
|
true
|
|
4160
4690
|
end
|
|
4161
4691
|
|
|
@@ -4168,13 +4698,20 @@ module Rigor
|
|
|
4168
4698
|
# - `Const = Data.define(*Symbol) [do ... end]`
|
|
4169
4699
|
# - `Const = Struct.new(*Symbol [, keyword_init: ...]) [do ... end]`
|
|
4170
4700
|
#
|
|
4701
|
+
# Issue [#703](https://github.com/rigortype/rigor/issues/703) — and their `Holder::Const = …` spellings, which
|
|
4702
|
+
# name a class just as unconditionally. Until this arm took them, `Holder::Thing.new(1)` fell to the default
|
|
4703
|
+
# `Class#new` envelope on `singleton(Struct)` and handed every reader a bare `Struct`.
|
|
4704
|
+
#
|
|
4171
4705
|
# The block body, if present, is recursed into so any nested class/module declarations in the override block (rare
|
|
4172
4706
|
# but legal) still feed the discovered table.
|
|
4173
4707
|
def record_meta_new_constant?(node, qualified_prefix, identity_table, discovered)
|
|
4174
4708
|
factory_call = resolve_meta_factory_call(node.value)
|
|
4175
4709
|
return false unless factory_call
|
|
4176
4710
|
|
|
4177
|
-
|
|
4711
|
+
child_prefix = meta_new_child_prefix(node, qualified_prefix)
|
|
4712
|
+
return false if child_prefix.nil?
|
|
4713
|
+
|
|
4714
|
+
full = child_prefix.join("::")
|
|
4178
4715
|
discovered[full] = Type::Combinator.singleton_of(full)
|
|
4179
4716
|
record_declarations(node.value, qualified_prefix, identity_table, discovered)
|
|
4180
4717
|
true
|