rigortype 0.2.7 → 0.2.8
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/docs/manual/04-diagnostics.md +7 -4
- data/docs/manual/06-baseline.md +5 -2
- data/docs/manual/15-type-protection-coverage.md +6 -4
- data/lib/rigor/analysis/baseline.rb +56 -96
- data/lib/rigor/analysis/buffer_binding.rb +13 -20
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +29 -48
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +35 -54
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +23 -37
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +16 -23
- data/lib/rigor/analysis/check_rules/rule_walk.rb +48 -71
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +19 -28
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +48 -67
- data/lib/rigor/analysis/check_rules.rb +174 -274
- data/lib/rigor/analysis/dependency_recorder.rb +26 -38
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +13 -25
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +17 -30
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +15 -26
- data/lib/rigor/analysis/dependency_source_inference/index.rb +45 -76
- data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +23 -40
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +50 -83
- data/lib/rigor/analysis/dependency_source_inference.rb +6 -10
- data/lib/rigor/analysis/diagnostic.rb +48 -75
- data/lib/rigor/analysis/erb_template_detector.rb +13 -18
- data/lib/rigor/analysis/fact_store.rb +12 -22
- data/lib/rigor/analysis/incremental.rb +47 -63
- data/lib/rigor/analysis/incremental_session.rb +74 -100
- data/lib/rigor/analysis/project_scan.rb +12 -23
- data/lib/rigor/analysis/result.rb +3 -5
- data/lib/rigor/analysis/rule_catalog.rb +67 -95
- data/lib/rigor/analysis/run_stats.rb +38 -61
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +101 -160
- data/lib/rigor/analysis/runner/pool_coordinator.rb +121 -188
- data/lib/rigor/analysis/runner/project_pre_passes.rb +62 -96
- data/lib/rigor/analysis/runner/run_snapshots.rb +12 -18
- data/lib/rigor/analysis/runner.rb +220 -333
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +30 -43
- data/lib/rigor/analysis/worker_session.rb +64 -102
- data/lib/rigor/ast/type_node.rb +6 -8
- data/lib/rigor/ast.rb +6 -10
- data/lib/rigor/bleeding_edge.rb +27 -35
- data/lib/rigor/builtins/hkt_builtins.rb +7 -14
- data/lib/rigor/builtins/imported_refinements.rb +106 -149
- data/lib/rigor/builtins/predefined_constant_refinements.rb +51 -65
- data/lib/rigor/builtins/regex_refinement.rb +32 -43
- data/lib/rigor/builtins/static_return_refinements.rb +57 -81
- data/lib/rigor/cache/descriptor.rb +58 -94
- data/lib/rigor/cache/incremental_snapshot.rb +32 -48
- data/lib/rigor/cache/rbs_cache_producer.rb +10 -15
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +10 -16
- data/lib/rigor/cache/rbs_class_type_param_names.rb +10 -16
- data/lib/rigor/cache/rbs_constant_table.rb +8 -13
- data/lib/rigor/cache/rbs_descriptor.rb +8 -13
- data/lib/rigor/cache/rbs_environment.rb +11 -19
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +11 -19
- data/lib/rigor/cache/rbs_known_class_names.rb +8 -12
- data/lib/rigor/cache/store.rb +96 -151
- data/lib/rigor/cli/annotate_command.rb +51 -88
- data/lib/rigor/cli/baseline_command.rb +10 -17
- data/lib/rigor/cli/check_command.rb +121 -199
- data/lib/rigor/cli/check_runner_factory.rb +8 -14
- data/lib/rigor/cli/ci_detector.rb +9 -14
- data/lib/rigor/cli/command.rb +8 -14
- data/lib/rigor/cli/coverage_command.rb +19 -26
- data/lib/rigor/cli/coverage_mutation.rb +14 -19
- data/lib/rigor/cli/coverage_report.rb +0 -2
- data/lib/rigor/cli/coverage_scan.rb +7 -11
- data/lib/rigor/cli/diagnostic_formats.rb +43 -62
- data/lib/rigor/cli/diff_command.rb +10 -20
- data/lib/rigor/cli/docs_command.rb +26 -40
- data/lib/rigor/cli/doctor_command.rb +8 -9
- data/lib/rigor/cli/explain_command.rb +4 -7
- data/lib/rigor/cli/fused_protection_renderer.rb +4 -5
- data/lib/rigor/cli/fused_protection_report.rb +6 -8
- data/lib/rigor/cli/lsp_command.rb +11 -19
- data/lib/rigor/cli/mcp_command.rb +4 -6
- data/lib/rigor/cli/mutation_protection_renderer.rb +3 -4
- data/lib/rigor/cli/mutation_protection_report.rb +6 -9
- data/lib/rigor/cli/options.rb +11 -19
- data/lib/rigor/cli/plugin_command.rb +18 -30
- data/lib/rigor/cli/plugins_command.rb +29 -51
- data/lib/rigor/cli/plugins_renderer.rb +12 -20
- data/lib/rigor/cli/prism_colorizer.rb +13 -19
- data/lib/rigor/cli/protection_renderer.rb +5 -7
- data/lib/rigor/cli/protection_report.rb +27 -17
- data/lib/rigor/cli/renderable.rb +4 -6
- data/lib/rigor/cli/show_bleedingedge_command.rb +5 -8
- data/lib/rigor/cli/sig_gen_command.rb +14 -26
- data/lib/rigor/cli/skill_command.rb +30 -47
- data/lib/rigor/cli/skill_describe.rb +40 -64
- data/lib/rigor/cli/trace_command.rb +9 -16
- data/lib/rigor/cli/trace_renderer.rb +35 -51
- data/lib/rigor/cli/triage_command.rb +6 -10
- data/lib/rigor/cli/triage_renderer.rb +1 -2
- data/lib/rigor/cli/type_of_command.rb +14 -23
- data/lib/rigor/cli/type_of_renderer.rb +3 -5
- data/lib/rigor/cli/type_scan_command.rb +6 -10
- data/lib/rigor/cli/type_scan_renderer.rb +3 -5
- data/lib/rigor/cli/type_scan_report.rb +2 -3
- data/lib/rigor/cli/upgrade_command.rb +2 -3
- data/lib/rigor/cli.rb +16 -28
- data/lib/rigor/config_audit.rb +35 -44
- data/lib/rigor/configuration/dependencies.rb +41 -70
- data/lib/rigor/configuration/severity_profile.rb +28 -42
- data/lib/rigor/configuration.rb +150 -241
- data/lib/rigor/environment/bundle_sig_discovery.rb +74 -118
- data/lib/rigor/environment/class_registry.rb +19 -26
- data/lib/rigor/environment/constant_type_cache_holder.rb +14 -21
- data/lib/rigor/environment/hkt_registry_holder.rb +6 -12
- data/lib/rigor/environment/lockfile_resolver.rb +31 -47
- data/lib/rigor/environment/rbs_collection_discovery.rb +39 -63
- data/lib/rigor/environment/rbs_coverage_report.rb +24 -42
- data/lib/rigor/environment/rbs_loader.rb +420 -518
- data/lib/rigor/environment/reflection.rb +28 -53
- data/lib/rigor/environment/reporters.rb +13 -24
- data/lib/rigor/environment.rb +176 -291
- data/lib/rigor/flow_contribution/conflict.rb +16 -27
- data/lib/rigor/flow_contribution/element.rb +7 -12
- data/lib/rigor/flow_contribution/fact.rb +33 -57
- data/lib/rigor/flow_contribution/merge_result.rb +6 -9
- data/lib/rigor/flow_contribution/merger.rb +32 -47
- data/lib/rigor/flow_contribution.rb +37 -55
- data/lib/rigor/inference/acceptance.rb +133 -219
- data/lib/rigor/inference/block_parameter_binder.rb +52 -82
- data/lib/rigor/inference/body_fixpoint.rb +22 -30
- data/lib/rigor/inference/budget_trace.rb +43 -63
- data/lib/rigor/inference/builtins/array_catalog.rb +4 -6
- data/lib/rigor/inference/builtins/comparable_catalog.rb +4 -6
- data/lib/rigor/inference/builtins/complex_catalog.rb +13 -19
- data/lib/rigor/inference/builtins/date_catalog.rb +38 -67
- data/lib/rigor/inference/builtins/encoding_catalog.rb +22 -36
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +4 -6
- data/lib/rigor/inference/builtins/exception_catalog.rb +38 -57
- data/lib/rigor/inference/builtins/hash_catalog.rb +9 -14
- data/lib/rigor/inference/builtins/method_catalog.rb +29 -48
- data/lib/rigor/inference/builtins/numeric_catalog.rb +13 -18
- data/lib/rigor/inference/builtins/pathname_catalog.rb +8 -13
- data/lib/rigor/inference/builtins/proc_catalog.rb +38 -62
- data/lib/rigor/inference/builtins/random_catalog.rb +21 -32
- data/lib/rigor/inference/builtins/range_catalog.rb +12 -21
- data/lib/rigor/inference/builtins/rational_catalog.rb +9 -15
- data/lib/rigor/inference/builtins/re_catalog.rb +29 -48
- data/lib/rigor/inference/builtins/set_catalog.rb +18 -27
- data/lib/rigor/inference/builtins/string_catalog.rb +12 -19
- data/lib/rigor/inference/builtins/struct_catalog.rb +18 -30
- data/lib/rigor/inference/builtins/time_catalog.rb +28 -44
- data/lib/rigor/inference/closure_escape_analyzer.rb +44 -67
- data/lib/rigor/inference/coverage_scanner.rb +13 -18
- data/lib/rigor/inference/def_return_typer.rb +8 -14
- data/lib/rigor/inference/dynamic_origin.rb +23 -20
- data/lib/rigor/inference/expression_typer.rb +774 -1152
- data/lib/rigor/inference/fallback.rb +8 -12
- data/lib/rigor/inference/fallback_tracer.rb +4 -10
- data/lib/rigor/inference/flow_tracer.rb +25 -36
- data/lib/rigor/inference/hkt_body.rb +45 -68
- data/lib/rigor/inference/hkt_body_parser.rb +33 -52
- data/lib/rigor/inference/hkt_reducer.rb +38 -59
- data/lib/rigor/inference/hkt_registry.rb +50 -76
- data/lib/rigor/inference/indexed_narrowing.rb +55 -81
- data/lib/rigor/inference/macro_block_self_type.rb +21 -34
- data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +11 -18
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +49 -83
- data/lib/rigor/inference/method_dispatcher/call_context.rb +16 -23
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +13 -22
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +307 -454
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +24 -34
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +22 -37
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +52 -81
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +51 -78
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +60 -97
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +23 -34
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +18 -26
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +37 -71
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +114 -182
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +122 -193
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +15 -24
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +64 -93
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +31 -45
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -9
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +356 -511
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +22 -32
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +15 -21
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +66 -82
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +10 -15
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -13
- data/lib/rigor/inference/method_dispatcher.rb +378 -524
- data/lib/rigor/inference/method_parameter_binder.rb +81 -114
- data/lib/rigor/inference/multi_target_binder.rb +51 -68
- data/lib/rigor/inference/mutation_widening.rb +115 -158
- data/lib/rigor/inference/narrowing.rb +509 -727
- data/lib/rigor/inference/origin_lookup.rb +38 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +100 -100
- data/lib/rigor/inference/precision_scanner.rb +26 -35
- data/lib/rigor/inference/project_patched_methods.rb +20 -32
- data/lib/rigor/inference/project_patched_scanner.rb +23 -37
- data/lib/rigor/inference/protection_scanner.rb +16 -18
- data/lib/rigor/inference/rbs_type_translator.rb +49 -66
- data/lib/rigor/inference/scope_indexer.rb +473 -817
- data/lib/rigor/inference/statement_evaluator.rb +706 -1111
- data/lib/rigor/inference/struct_fold_safety.rb +34 -48
- data/lib/rigor/inference/synthetic_method.rb +9 -16
- data/lib/rigor/inference/synthetic_method_index.rb +20 -35
- data/lib/rigor/inference/synthetic_method_scanner.rb +63 -103
- data/lib/rigor/language_server/buffer_resolution.rb +6 -9
- data/lib/rigor/language_server/buffer_table.rb +11 -18
- data/lib/rigor/language_server/completion_provider.rb +69 -116
- data/lib/rigor/language_server/debouncer.rb +16 -25
- data/lib/rigor/language_server/diagnostic_publisher.rb +26 -40
- data/lib/rigor/language_server/document_symbol_provider.rb +14 -19
- data/lib/rigor/language_server/folding_range_provider.rb +11 -19
- data/lib/rigor/language_server/hover_provider.rb +8 -11
- data/lib/rigor/language_server/hover_renderer.rb +56 -93
- data/lib/rigor/language_server/loop.rb +14 -19
- data/lib/rigor/language_server/project_context.rb +46 -74
- data/lib/rigor/language_server/selection_range_provider.rb +9 -12
- data/lib/rigor/language_server/server.rb +55 -83
- data/lib/rigor/language_server/signature_help_provider.rb +29 -46
- data/lib/rigor/language_server/synchronized_writer.rb +4 -7
- data/lib/rigor/language_server/uri.rb +8 -13
- data/lib/rigor/language_server.rb +4 -6
- data/lib/rigor/mcp/loop.rb +2 -3
- data/lib/rigor/mcp/server.rb +4 -7
- data/lib/rigor/mcp.rb +3 -6
- data/lib/rigor/plugin/access_denied_error.rb +5 -8
- data/lib/rigor/plugin/additional_initializer.rb +21 -31
- data/lib/rigor/plugin/base.rb +335 -518
- data/lib/rigor/plugin/blueprint.rb +14 -23
- data/lib/rigor/plugin/box.rb +18 -29
- data/lib/rigor/plugin/fact_store.rb +16 -26
- data/lib/rigor/plugin/inflector.rb +37 -53
- data/lib/rigor/plugin/io_boundary.rb +33 -56
- data/lib/rigor/plugin/isolation.rb +42 -55
- data/lib/rigor/plugin/load_error.rb +10 -15
- data/lib/rigor/plugin/loader.rb +30 -49
- data/lib/rigor/plugin/macro/block_as_method.rb +20 -32
- data/lib/rigor/plugin/macro/heredoc_template.rb +35 -58
- data/lib/rigor/plugin/macro/nested_class_template.rb +22 -36
- data/lib/rigor/plugin/macro/trait_registry.rb +34 -51
- data/lib/rigor/plugin/macro.rb +10 -15
- data/lib/rigor/plugin/manifest.rb +85 -144
- data/lib/rigor/plugin/node_context.rb +14 -22
- data/lib/rigor/plugin/node_rule_walk.rb +49 -74
- data/lib/rigor/plugin/protocol_contract.rb +25 -39
- data/lib/rigor/plugin/registry.rb +132 -205
- data/lib/rigor/plugin/services.rb +21 -33
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -19
- data/lib/rigor/plugin/trust_policy.rb +24 -38
- data/lib/rigor/plugin/type_node_resolver.rb +15 -24
- data/lib/rigor/protection/diagnostic_oracle.rb +11 -13
- data/lib/rigor/protection/mutation_scanner.rb +27 -35
- data/lib/rigor/protection/mutator.rb +50 -70
- data/lib/rigor/protection/test_suite_oracle.rb +20 -27
- data/lib/rigor/rbs_extended/conformance_checker.rb +48 -68
- data/lib/rigor/rbs_extended/hkt_directives.rb +28 -54
- data/lib/rigor/rbs_extended/reporter.rb +24 -40
- data/lib/rigor/rbs_extended.rb +107 -197
- data/lib/rigor/reflection.rb +68 -86
- data/lib/rigor/scope/discovery_index.rb +14 -19
- data/lib/rigor/scope.rb +255 -310
- data/lib/rigor/sig_gen/classification.rb +6 -10
- data/lib/rigor/sig_gen/generator.rb +197 -323
- data/lib/rigor/sig_gen/layout_index.rb +12 -20
- data/lib/rigor/sig_gen/method_candidate.rb +12 -17
- data/lib/rigor/sig_gen/observation_collector.rb +38 -70
- data/lib/rigor/sig_gen/observed_call.rb +13 -23
- data/lib/rigor/sig_gen/path_mapper.rb +17 -29
- data/lib/rigor/sig_gen/renderer.rb +7 -13
- data/lib/rigor/sig_gen/type_elaborator.rb +15 -28
- data/lib/rigor/sig_gen/write_result.rb +8 -16
- data/lib/rigor/sig_gen/writer.rb +95 -174
- data/lib/rigor/sig_gen.rb +3 -6
- data/lib/rigor/signature_path_audit.rb +24 -30
- data/lib/rigor/source/constant_path.rb +10 -14
- data/lib/rigor/source/literals.rb +31 -45
- data/lib/rigor/source/node_locator.rb +9 -11
- data/lib/rigor/source/node_walker.rb +9 -13
- data/lib/rigor/source.rb +3 -4
- data/lib/rigor/testing.rb +16 -20
- data/lib/rigor/triage/catalogue.rb +38 -62
- data/lib/rigor/triage.rb +31 -52
- data/lib/rigor/trinary.rb +9 -13
- data/lib/rigor/type/acceptance_router.rb +4 -6
- data/lib/rigor/type/accepts_result.rb +10 -14
- data/lib/rigor/type/app.rb +19 -27
- data/lib/rigor/type/bot.rb +4 -6
- data/lib/rigor/type/bound_method.rb +10 -15
- data/lib/rigor/type/combinator.rb +165 -257
- data/lib/rigor/type/constant.rb +23 -34
- data/lib/rigor/type/data_class.rb +10 -15
- data/lib/rigor/type/data_instance.rb +14 -20
- data/lib/rigor/type/difference.rb +21 -32
- data/lib/rigor/type/dynamic.rb +3 -5
- data/lib/rigor/type/hash_shape.rb +32 -18
- data/lib/rigor/type/integer_range.rb +11 -16
- data/lib/rigor/type/intersection.rb +27 -42
- data/lib/rigor/type/nominal.rb +10 -15
- data/lib/rigor/type/plain_lattice.rb +9 -13
- data/lib/rigor/type/refined.rb +67 -114
- data/lib/rigor/type/singleton.rb +4 -6
- data/lib/rigor/type/struct_class.rb +11 -16
- data/lib/rigor/type/struct_instance.rb +15 -21
- data/lib/rigor/type/tuple.rb +14 -19
- data/lib/rigor/type/union.rb +30 -42
- data/lib/rigor/type_node/generic.rb +14 -26
- data/lib/rigor/type_node/identifier.rb +12 -19
- data/lib/rigor/type_node.rb +3 -12
- data/lib/rigor/value_semantics.rb +16 -21
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +18 -30
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +16 -29
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +12 -23
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +22 -39
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +26 -46
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +51 -87
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +10 -17
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +26 -39
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +118 -176
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +30 -51
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +57 -92
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +71 -108
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +13 -22
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +15 -25
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +8 -13
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +16 -24
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +30 -47
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +79 -129
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +36 -57
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +27 -41
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +16 -24
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +148 -235
- data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +3 -5
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +8 -14
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +9 -15
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +4 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +23 -41
- data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +3 -5
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +6 -10
- data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +8 -11
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +30 -45
- data/plugins/rigor-devise/lib/rigor-devise.rb +3 -5
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +43 -69
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +26 -35
- data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +3 -5
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +30 -47
- data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +3 -5
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +67 -113
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +29 -46
- data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +3 -5
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -20
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +14 -23
- data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +2 -3
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +22 -37
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +28 -46
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +13 -17
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -47
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +52 -80
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +21 -31
- data/plugins/rigor-graphql/lib/rigor-graphql.rb +3 -5
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +11 -17
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +20 -34
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +45 -75
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +22 -36
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +22 -41
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +17 -32
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +6 -12
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +4 -8
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +17 -32
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -18
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +58 -92
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +20 -34
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +20 -31
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +49 -82
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +55 -92
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +51 -90
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +17 -30
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +38 -70
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +42 -74
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +280 -497
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +54 -89
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +32 -54
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +8 -13
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +9 -16
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +24 -39
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +13 -22
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +34 -56
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +22 -42
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +43 -75
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +27 -39
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +14 -19
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +20 -34
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +28 -47
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +21 -37
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +13 -22
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +18 -31
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +6 -10
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +14 -29
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +23 -41
- data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +3 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +23 -38
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +57 -96
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +22 -35
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +41 -69
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +6 -10
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +24 -41
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +29 -50
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +55 -88
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +159 -262
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +21 -29
- data/plugins/rigor-statesman/lib/rigor-statesman.rb +3 -5
- data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +23 -37
- data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +4 -6
- data/sig/rigor/scope.rbs +6 -0
- metadata +2 -1
|
@@ -12,21 +12,16 @@ require_relative "struct_fold_safety"
|
|
|
12
12
|
|
|
13
13
|
module Rigor
|
|
14
14
|
module Inference
|
|
15
|
-
# Builds a per-node scope index for a Prism program by running
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# the
|
|
19
|
-
#
|
|
20
|
-
# ancestor's recorded scope, so a downstream caller that looks up
|
|
21
|
-
# the scope for any Prism node in the tree always gets the scope
|
|
22
|
-
# that was effectively visible at that point.
|
|
15
|
+
# Builds a per-node scope index for a Prism program by running `Rigor::Inference::StatementEvaluator` over the root
|
|
16
|
+
# and recording the entry scope visible at every node. Expression-interior nodes the evaluator does not specialise
|
|
17
|
+
# (call receivers, arguments, array/hash elements, ...) inherit their nearest statement-y ancestor's recorded scope,
|
|
18
|
+
# so a downstream caller that looks up the scope for any Prism node in the tree always gets the scope that was
|
|
19
|
+
# effectively visible at that point.
|
|
23
20
|
#
|
|
24
|
-
# The CLI commands `rigor type-of` and `rigor type-scan` consume
|
|
25
|
-
# the
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# empty scope and miss the constant-folding / dispatch precision
|
|
29
|
-
# that Slice 3 phase 2's StatementEvaluator unlocks.
|
|
21
|
+
# The CLI commands `rigor type-of` and `rigor type-scan` consume the index so that local-variable bindings
|
|
22
|
+
# established earlier in the program are visible to the typer when probing later nodes. Without the index, both
|
|
23
|
+
# commands would type every node under an empty scope and miss the constant-folding / dispatch precision that Slice
|
|
24
|
+
# 3 phase 2's StatementEvaluator unlocks.
|
|
30
25
|
#
|
|
31
26
|
# The returned object is an identity-comparing Hash:
|
|
32
27
|
#
|
|
@@ -35,11 +30,9 @@ module Rigor
|
|
|
35
30
|
# index[some_prism_node] #=> the Rigor::Scope visible at that node
|
|
36
31
|
# ```
|
|
37
32
|
#
|
|
38
|
-
# Nodes that are not part of the program subtree (e.g. synthesised
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
# callers MUST treat it as read-only; the indexer itself never
|
|
42
|
-
# exposes a way to update it past construction.
|
|
33
|
+
# Nodes that are not part of the program subtree (e.g. synthesised virtual nodes that the caller looks up after the
|
|
34
|
+
# fact) yield the `default_scope`. The returned Hash is mutable in principle but callers MUST treat it as read-only;
|
|
35
|
+
# the indexer itself never exposes a way to update it past construction.
|
|
43
36
|
# rubocop:disable Metrics/ModuleLength
|
|
44
37
|
module ScopeIndexer
|
|
45
38
|
module_function
|
|
@@ -59,98 +52,68 @@ module Rigor
|
|
|
59
52
|
# @return [Hash{Prism::Node => Rigor::Scope}] identity-comparing
|
|
60
53
|
# table whose default value is `default_scope`.
|
|
61
54
|
def index(root, default_scope:, converged_loop_recording: false) # rubocop:disable Metrics/AbcSize
|
|
62
|
-
# Slice A-declarations. Build the declaration overrides
|
|
63
|
-
#
|
|
64
|
-
# already carries the table; structural sharing through
|
|
65
|
-
# `Scope#with_local` / `#with_fact` / `#with_self_type`
|
|
55
|
+
# Slice A-declarations. Build the declaration overrides first so every scope handed to the StatementEvaluator
|
|
56
|
+
# already carries the table; structural sharing through `Scope#with_local` / `#with_fact` / `#with_self_type`
|
|
66
57
|
# propagates it across every derived scope.
|
|
67
58
|
declared_types, discovered_classes = build_declaration_artifacts(root)
|
|
68
|
-
# Merge the indexer's findings on top of whatever the
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
# `lib/` before scanning `spec/`) keep their seeds
|
|
73
|
-
# alongside the per-file declarations the indexer
|
|
74
|
-
# itself discovers. Indexer-found entries win on
|
|
75
|
-
# collision — same-file declarations are the most
|
|
76
|
-
# specific authority.
|
|
59
|
+
# Merge the indexer's findings on top of whatever the base scope already carries so callers that seed cross-file
|
|
60
|
+
# class knowledge (e.g. the ADR-14 `SigGen::ObservationCollector` pre-walking project `lib/` before scanning
|
|
61
|
+
# `spec/`) keep their seeds alongside the per-file declarations the indexer itself discovers. Indexer-found
|
|
62
|
+
# entries win on collision — same-file declarations are the most specific authority.
|
|
77
63
|
merged_classes = default_scope.discovered_classes.merge(discovered_classes)
|
|
78
64
|
seeded_scope = default_scope.with_discovery(
|
|
79
65
|
default_scope.discovery.with(declared_types: declared_types,
|
|
80
66
|
discovered_classes: merged_classes)
|
|
81
67
|
)
|
|
82
68
|
|
|
83
|
-
# Slice 7 phase 2. Pre-pass over every class/module body
|
|
84
|
-
#
|
|
85
|
-
# declared_types so the rvalue typer in the pre-pass can
|
|
86
|
-
# see declaration overrides.
|
|
69
|
+
# Slice 7 phase 2. Pre-pass over every class/module body to collect the per-class ivar accumulator. Seeded after
|
|
70
|
+
# declared_types so the rvalue typer in the pre-pass can see declaration overrides.
|
|
87
71
|
class_ivars = build_class_ivar_index(root, seeded_scope)
|
|
88
72
|
seeded_scope = seeded_scope.with_discovery(seeded_scope.discovery.with(class_ivars: class_ivars))
|
|
89
73
|
|
|
90
|
-
# Slice 7 phase 6. Same pre-pass shape for cvars (per
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
# so reads at the top level (and in CLI probes that do
|
|
94
|
-
# not enter a method body) observe the precise type
|
|
95
|
-
# without consulting the accumulator on every lookup.
|
|
74
|
+
# Slice 7 phase 6. Same pre-pass shape for cvars (per class) and globals (program-wide). Globals are also
|
|
75
|
+
# materialised into the top-level scope's `globals` map so reads at the top level (and in CLI probes that do not
|
|
76
|
+
# enter a method body) observe the precise type without consulting the accumulator on every lookup.
|
|
96
77
|
class_cvars = build_class_cvar_index(root, seeded_scope)
|
|
97
78
|
seeded_scope = seeded_scope.with_discovery(seeded_scope.discovery.with(class_cvars: class_cvars))
|
|
98
79
|
program_globals = build_program_global_index(root, seeded_scope)
|
|
99
80
|
seeded_scope = seeded_scope.with_discovery(seeded_scope.discovery.with(program_globals: program_globals))
|
|
100
81
|
program_globals.each { |name, type| seeded_scope = seeded_scope.with_global(name, type) }
|
|
101
82
|
|
|
102
|
-
# Slice 7 phase 9. In-source constant value tracking.
|
|
103
|
-
#
|
|
104
|
-
# the
|
|
105
|
-
#
|
|
106
|
-
# `self_type`, so the rvalue typer sees in-class
|
|
107
|
-
# references resolve correctly. Multiple writes to the
|
|
108
|
-
# same qualified name union via `Type::Combinator.union`.
|
|
83
|
+
# Slice 7 phase 9. In-source constant value tracking. Walks every ConstantWriteNode/ConstantPathWriteNode in the
|
|
84
|
+
# program and types its rvalue under a scope that carries the surrounding qualified prefix as `self_type`, so
|
|
85
|
+
# the rvalue typer sees in-class references resolve correctly. Multiple writes to the same qualified name union
|
|
86
|
+
# via `Type::Combinator.union`.
|
|
109
87
|
in_source_constants = build_in_source_constants(root, seeded_scope)
|
|
110
88
|
seeded_scope = seeded_scope.with_discovery(
|
|
111
89
|
seeded_scope.discovery.with(in_source_constants: in_source_constants)
|
|
112
90
|
)
|
|
113
91
|
|
|
114
|
-
# Slice 7 phase 12. In-source method discovery. Walks
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
# table
|
|
119
|
-
#
|
|
120
|
-
# UNDER the cross-file pre-pass seed; details: merge_project_method_indexes.
|
|
121
|
-
# One combined descent yields both the discovered-methods existence
|
|
122
|
-
# table and the instance def-node table — see
|
|
123
|
-
# {#build_methods_and_def_nodes}. `seed_discovered_methods` seeds the
|
|
124
|
-
# former onto the scope and returns the def-node table for
|
|
92
|
+
# Slice 7 phase 12. In-source method discovery. Walks every class/module body for `Prism::DefNode` and
|
|
93
|
+
# recognised `define_method` calls and records the introduced method names. `rigor check` consults the table to
|
|
94
|
+
# suppress false positives for methods the user has defined but no RBS sig describes. Merged UNDER the
|
|
95
|
+
# cross-file pre-pass seed; details: merge_project_method_indexes. One combined descent yields both the
|
|
96
|
+
# discovered-methods existence table and the instance def-node table — see {#build_methods_and_def_nodes}.
|
|
97
|
+
# `seed_discovered_methods` seeds the former onto the scope and returns the def-node table for
|
|
125
98
|
# `merge_project_method_indexes` below.
|
|
126
99
|
seeded_scope, file_def_nodes = seed_discovered_methods(seeded_scope, default_scope, root)
|
|
127
100
|
|
|
128
|
-
# v0.0.2 #5 + ADR-24 slice 2 — record per-instance-method
|
|
129
|
-
#
|
|
130
|
-
# class
|
|
131
|
-
#
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
# `def.method-visibility-mismatch` and ADR-35
|
|
135
|
-
# `def.override-visibility-reduced` CheckRules consult the
|
|
136
|
-
# table. Seeded inside `merge_project_method_indexes` so the
|
|
137
|
-
# per-file visibilities merge OVER the cross-file project seed
|
|
138
|
-
# rather than overwriting it.
|
|
101
|
+
# v0.0.2 #5 + ADR-24 slice 2 — record per-instance-method def nodes, the class -> superclass map, and the
|
|
102
|
+
# class/module -> included-modules map, each merged under the cross-file pre-pass seed (see below). v0.1.2 —
|
|
103
|
+
# per-class table of method visibilities (`:public` / `:private` / `:protected`). The
|
|
104
|
+
# `def.method-visibility-mismatch` and ADR-35 `def.override-visibility-reduced` CheckRules consult the table.
|
|
105
|
+
# Seeded inside `merge_project_method_indexes` so the per-file visibilities merge OVER the cross-file project
|
|
106
|
+
# seed rather than overwriting it.
|
|
139
107
|
seeded_scope = merge_project_method_indexes(seeded_scope, default_scope, root, file_def_nodes)
|
|
140
108
|
|
|
141
109
|
table = {}.compare_by_identity
|
|
142
110
|
table.default = seeded_scope
|
|
143
111
|
|
|
144
|
-
# Last-visit-wins, not first: when `StatementEvaluator`
|
|
145
|
-
#
|
|
146
|
-
#
|
|
147
|
-
#
|
|
148
|
-
#
|
|
149
|
-
# retry` is observed). The diagnostic layer reads
|
|
150
|
-
# `table[node]` to type predicates; the second pass's
|
|
151
|
-
# entry is the one that reflects all flow-derived
|
|
152
|
-
# rebinds, so it MUST overwrite the first.
|
|
153
|
-
# ADR-48 Struct slice 3 — install the top-level fold-safe-local set so
|
|
112
|
+
# Last-visit-wins, not first: when `StatementEvaluator` internally re-evaluates a subtree (notably
|
|
113
|
+
# `eval_begin`'s retry-edge widening pass), the LATER visit carries the corrected entry scope (e.g. a `tries`
|
|
114
|
+
# widened to `Nominal[Integer]` after the rescue body's `tries += 1; retry` is observed). The diagnostic layer
|
|
115
|
+
# reads `table[node]` to type predicates; the second pass's entry is the one that reflects all flow-derived
|
|
116
|
+
# rebinds, so it MUST overwrite the first. ADR-48 Struct slice 3 — install the top-level fold-safe-local set so
|
|
154
117
|
# a member read off a mutation-free top-level struct binding folds.
|
|
155
118
|
seeded_scope = seed_struct_fold_safe(seeded_scope, root)
|
|
156
119
|
|
|
@@ -162,12 +125,10 @@ module Rigor
|
|
|
162
125
|
table
|
|
163
126
|
end
|
|
164
127
|
|
|
165
|
-
# Runs the combined methods/def-nodes descent (one walk of the file),
|
|
166
|
-
#
|
|
167
|
-
#
|
|
168
|
-
#
|
|
169
|
-
# def-node table into {#merge_project_method_indexes} without walking
|
|
170
|
-
# the file a second time.
|
|
128
|
+
# Runs the combined methods/def-nodes descent (one walk of the file), seeds the discovered-methods existence table
|
|
129
|
+
# onto `seeded_scope` (merged UNDER the cross-file pre-pass seed `default_scope` carries), and returns `[scope,
|
|
130
|
+
# file_def_nodes]` so the caller can thread the def-node table into {#merge_project_method_indexes} without
|
|
131
|
+
# walking the file a second time.
|
|
171
132
|
def seed_discovered_methods(seeded_scope, default_scope, root)
|
|
172
133
|
file_methods, file_def_nodes = build_methods_and_def_nodes(root)
|
|
173
134
|
discovered_methods = deep_merge_class_methods(default_scope.discovered_methods, file_methods)
|
|
@@ -175,9 +136,8 @@ module Rigor
|
|
|
175
136
|
[scope, file_def_nodes]
|
|
176
137
|
end
|
|
177
138
|
|
|
178
|
-
# ADR-48 Struct slice 3 — installs the top-level fold-safe-local set
|
|
179
|
-
#
|
|
180
|
-
# receivers are resolved through the side-table the seeded scope carries.
|
|
139
|
+
# ADR-48 Struct slice 3 — installs the top-level fold-safe-local set ({Inference::StructFoldSafety}). Struct
|
|
140
|
+
# member layouts of constant receivers are resolved through the side-table the seeded scope carries.
|
|
181
141
|
def seed_struct_fold_safe(seeded_scope, root)
|
|
182
142
|
seeded_scope.with_struct_fold_safe(
|
|
183
143
|
StructFoldSafety.fold_safe_locals(
|
|
@@ -186,14 +146,10 @@ module Rigor
|
|
|
186
146
|
)
|
|
187
147
|
end
|
|
188
148
|
|
|
189
|
-
# v0.0.2 #5 + ADR-24 slice 2 — seeds the three
|
|
190
|
-
#
|
|
191
|
-
#
|
|
192
|
-
#
|
|
193
|
-
# map. Each per-file table is merged UNDER the cross-file
|
|
194
|
-
# `discovered_def_index_for_paths` seed carried on
|
|
195
|
-
# `default_scope` — same-file declarations win per entry,
|
|
196
|
-
# the cross-file seed supplies sibling-file ancestors.
|
|
149
|
+
# v0.0.2 #5 + ADR-24 slice 2 — seeds the three project-method indexes onto `seeded_scope`: the per-instance-method
|
|
150
|
+
# def-node table, the class -> superclass map, and the class/module -> included-modules map. Each per-file table
|
|
151
|
+
# is merged UNDER the cross-file `discovered_def_index_for_paths` seed carried on `default_scope` — same-file
|
|
152
|
+
# declarations win per entry, the cross-file seed supplies sibling-file ancestors.
|
|
197
153
|
def merge_project_method_indexes(seeded_scope, default_scope, root, file_def_nodes)
|
|
198
154
|
def_nodes = default_scope.discovered_def_nodes.merge(
|
|
199
155
|
file_def_nodes
|
|
@@ -207,14 +163,13 @@ module Rigor
|
|
|
207
163
|
includes = default_scope.discovered_includes.merge(
|
|
208
164
|
build_discovered_includes(root)
|
|
209
165
|
) { |_class, cross_file, per_file| (cross_file + per_file).uniq }
|
|
210
|
-
# ADR-35 — per-file visibilities merged OVER the cross-file
|
|
211
|
-
#
|
|
212
|
-
# sibling-file ancestors are preserved from the project seed).
|
|
166
|
+
# ADR-35 — per-file visibilities merged OVER the cross-file seed (the current file is authoritative for its own
|
|
167
|
+
# classes; sibling-file ancestors are preserved from the project seed).
|
|
213
168
|
method_visibilities = default_scope.discovered_method_visibilities.merge(
|
|
214
169
|
build_discovered_method_visibilities(root)
|
|
215
170
|
) { |_class, cross_file, per_file| cross_file.merge(per_file) }
|
|
216
|
-
# ADR-48 — per-file Data + Struct member layouts merged OVER the
|
|
217
|
-
#
|
|
171
|
+
# ADR-48 — per-file Data + Struct member layouts merged OVER the cross-file seed (same-file declaration is
|
|
172
|
+
# authoritative).
|
|
218
173
|
data_member_layouts, struct_member_layouts = merge_member_layouts(default_scope, root)
|
|
219
174
|
|
|
220
175
|
seeded_scope.with_discovery(
|
|
@@ -230,10 +185,9 @@ module Rigor
|
|
|
230
185
|
)
|
|
231
186
|
end
|
|
232
187
|
|
|
233
|
-
# ADR-48 — the per-file Data + Struct member-layout tables, each merged
|
|
234
|
-
#
|
|
235
|
-
#
|
|
236
|
-
# under the method-size budget.
|
|
188
|
+
# ADR-48 — the per-file Data + Struct member-layout tables, each merged OVER the cross-file seed so a same-file
|
|
189
|
+
# declaration wins for its own classes. Returned as a pair to keep {#merge_project_method_indexes} under the
|
|
190
|
+
# method-size budget.
|
|
237
191
|
def merge_member_layouts(default_scope, root)
|
|
238
192
|
[
|
|
239
193
|
default_scope.data_member_layouts.merge(build_data_member_layouts(root)),
|
|
@@ -241,29 +195,21 @@ module Rigor
|
|
|
241
195
|
]
|
|
242
196
|
end
|
|
243
197
|
|
|
244
|
-
# Slice 7 phase 2. Builds the class-level ivar accumulator
|
|
245
|
-
#
|
|
246
|
-
#
|
|
247
|
-
#
|
|
248
|
-
#
|
|
249
|
-
#
|
|
250
|
-
# typed with NO local bindings — the pre-pass lacks
|
|
251
|
-
# statement-level threading — so `@x = 1` records
|
|
252
|
-
# `Constant[1]` but `@x = some_local + 1` records
|
|
253
|
-
# `Dynamic[Top]` (since `some_local` is unbound at
|
|
254
|
-
# pre-pass time). Multiple writes to the same ivar union
|
|
255
|
-
# via `Type::Combinator.union`.
|
|
198
|
+
# Slice 7 phase 2. Builds the class-level ivar accumulator by walking every `Prism::ClassNode` /
|
|
199
|
+
# `Prism::ModuleNode` body, descending into each nested `Prism::DefNode`, and typing every
|
|
200
|
+
# `Prism::InstanceVariableWriteNode` rvalue under a scope that carries the appropriate `self_type` for that def
|
|
201
|
+
# (singleton vs instance). The rvalue is typed with NO local bindings — the pre-pass lacks statement-level
|
|
202
|
+
# threading — so `@x = 1` records `Constant[1]` but `@x = some_local + 1` records `Dynamic[Top]` (since
|
|
203
|
+
# `some_local` is unbound at pre-pass time). Multiple writes to the same ivar union via `Type::Combinator.union`.
|
|
256
204
|
def build_class_ivar_index(root, default_scope)
|
|
257
205
|
accumulator = {}
|
|
258
206
|
mutated_ivars = {}
|
|
259
207
|
read_before_write = {}
|
|
260
208
|
init_writes = {}
|
|
261
|
-
# WD3 — per-class summary of `{class_name => {method_name =>
|
|
262
|
-
#
|
|
263
|
-
#
|
|
264
|
-
#
|
|
265
|
-
# unconditional same-class method call (`mask!`) credits the
|
|
266
|
-
# overwrite. Built once per program here, memoised by class.
|
|
209
|
+
# WD3 — per-class summary of `{class_name => {method_name => Set<ivar names definitely assigned non-nil on every
|
|
210
|
+
# completing path>}}`, consulted by `dead_transient_nil_writes` so a ctor that reassigns `@x` indirectly through
|
|
211
|
+
# an unconditional same-class method call (`mask!`) credits the overwrite. Built once per program here, memoised
|
|
212
|
+
# by class.
|
|
267
213
|
method_assign_effects = build_method_assign_effects(root)
|
|
268
214
|
walk_class_ivars(root, [], default_scope, accumulator, mutated_ivars,
|
|
269
215
|
read_before_write, init_writes, method_assign_effects)
|
|
@@ -272,21 +218,15 @@ module Rigor
|
|
|
272
218
|
accumulator.transform_values(&:freeze).freeze
|
|
273
219
|
end
|
|
274
220
|
|
|
275
|
-
# B2.3 — finalize the read-before-write nil contribution.
|
|
276
|
-
#
|
|
277
|
-
# observed a read-before-write AND no `initialize` write
|
|
278
|
-
# exists for that ivar, contribute `Constant[nil]` to the
|
|
221
|
+
# B2.3 — finalize the read-before-write nil contribution. For each class, for each ivar where SOME method body
|
|
222
|
+
# observed a read-before-write AND no `initialize` write exists for that ivar, contribute `Constant[nil]` to the
|
|
279
223
|
# class-wide accumulator.
|
|
280
224
|
#
|
|
281
|
-
# The `initialize` filter is the soundness gate: Ruby
|
|
282
|
-
#
|
|
283
|
-
#
|
|
284
|
-
#
|
|
285
|
-
#
|
|
286
|
-
# AST-order coincidence. Without this filter a normal
|
|
287
|
-
# `def initialize; @x = ... end` / `def use; @x.foo end`
|
|
288
|
-
# class would have `@x` widened with nil, producing FPs
|
|
289
|
-
# at every `@x.foo` call.
|
|
225
|
+
# The `initialize` filter is the soundness gate: Ruby semantics guarantee `initialize` runs first (via
|
|
226
|
+
# `Class.new`), so a write there reaches every other method body's read. Read-before-write in a non-init method is
|
|
227
|
+
# then NOT a nil-at-runtime case — it's just AST-order coincidence. Without this filter a normal `def initialize;
|
|
228
|
+
# @x = ... end` / `def use; @x.foo end` class would have `@x` widened with nil, producing FPs at every `@x.foo`
|
|
229
|
+
# call.
|
|
290
230
|
def contribute_read_before_write_nil!(accumulator, read_before_write, init_writes)
|
|
291
231
|
nil_t = Type::Combinator.constant_of(nil)
|
|
292
232
|
read_before_write.each do |class_name, ivar_set|
|
|
@@ -295,8 +235,7 @@ module Rigor
|
|
|
295
235
|
next if per_class.nil?
|
|
296
236
|
|
|
297
237
|
ivar_set.each do |ivar_name|
|
|
298
|
-
# Soundness gates (in order):
|
|
299
|
-
# (1) `initialize` writes the ivar → it's set
|
|
238
|
+
# Soundness gates (in order): (1) `initialize` writes the ivar → it's set
|
|
300
239
|
# before any other method runs, so the
|
|
301
240
|
# read-before-write in a sibling method is
|
|
302
241
|
# NOT a runtime nil case.
|
|
@@ -315,28 +254,18 @@ module Rigor
|
|
|
315
254
|
end
|
|
316
255
|
end
|
|
317
256
|
|
|
318
|
-
# Walks the post-collected accumulator and widens any Tuple
|
|
319
|
-
#
|
|
320
|
-
#
|
|
321
|
-
# comes from `gather_ivar_writes` recording every
|
|
322
|
-
# `@ivar.<method>(...)` call whose method is in
|
|
323
|
-
# `MutationWidening::ARRAY_MUTATORS` or `HASH_MUTATORS`.
|
|
257
|
+
# Walks the post-collected accumulator and widens any Tuple / HashShape entry for an ivar that observed a mutator
|
|
258
|
+
# call anywhere in the same class body. The mutation evidence comes from `gather_ivar_writes` recording every
|
|
259
|
+
# `@ivar.<method>(...)` call whose method is in `MutationWidening::ARRAY_MUTATORS` or `HASH_MUTATORS`.
|
|
324
260
|
#
|
|
325
|
-
# The widening uses `MutationWidening.widen_for_mutator` —
|
|
326
|
-
#
|
|
327
|
-
#
|
|
328
|
-
#
|
|
329
|
-
#
|
|
330
|
-
#
|
|
331
|
-
# body in the class — closing the cross-method gap noted in
|
|
332
|
-
# ROADMAP § Future cycles / Type-language / engine
|
|
333
|
-
# ("Tuple / HashShape widening for ivar-seeded literals
|
|
334
|
-
# after mutation"; Redmine 6.1.2
|
|
335
|
-
# `Redmine::Views::Builders::Structure` is the canonical
|
|
336
|
-
# worked site).
|
|
261
|
+
# The widening uses `MutationWidening.widen_for_mutator` — the same primitive
|
|
262
|
+
# `Inference::StatementEvaluator#eval_call` applies for per-method-body widening on a local / ivar receiver. The
|
|
263
|
+
# class-level pass extends that primitive's reach so a `Tuple`-seeded ivar in `initialize` is observed as
|
|
264
|
+
# `Nominal[Array]` at the entry of every OTHER method body in the class — closing the cross-method gap noted in
|
|
265
|
+
# ROADMAP § Future cycles / Type-language / engine ("Tuple / HashShape widening for ivar-seeded literals after
|
|
266
|
+
# mutation"; Redmine 6.1.2 `Redmine::Views::Builders::Structure` is the canonical worked site).
|
|
337
267
|
#
|
|
338
|
-
# Always-safe: the widening can only LOSE precision; the
|
|
339
|
-
# underlying nominal (`Array` / `Hash`) and the element
|
|
268
|
+
# Always-safe: the widening can only LOSE precision; the underlying nominal (`Array` / `Hash`) and the element
|
|
340
269
|
# union are preserved.
|
|
341
270
|
def widen_mutated_ivar_entries!(accumulator, mutated_ivars)
|
|
342
271
|
accumulator.each do |class_name, ivars|
|
|
@@ -352,21 +281,13 @@ module Rigor
|
|
|
352
281
|
end
|
|
353
282
|
end
|
|
354
283
|
|
|
355
|
-
# Walks a class-ivar accumulator entry (which may be a
|
|
356
|
-
# `
|
|
357
|
-
#
|
|
358
|
-
#
|
|
359
|
-
#
|
|
360
|
-
#
|
|
361
|
-
#
|
|
362
|
-
# HashShape → Hash) AND the element types to
|
|
363
|
-
# `Dynamic[Top]`. The justification — once any method
|
|
364
|
-
# mutates the ivar, its post-mutation contents are
|
|
365
|
-
# statically unknown across method boundaries, so
|
|
366
|
-
# preserving the seed-write's element precision would be
|
|
367
|
-
# an unsound over-claim (e.g. `@struct = [{}]; somewhere:
|
|
368
|
-
# @struct << []` makes the next read's element no longer
|
|
369
|
-
# `Constant[{}]`).
|
|
284
|
+
# Walks a class-ivar accumulator entry (which may be a `Union` of multiple write rvalues) and widens any `Tuple`
|
|
285
|
+
# or `HashShape` member whose corresponding mutator family was observed against the ivar somewhere in the class.
|
|
286
|
+
# Class-level widening is more aggressive than the per-method-body `MutationWidening` primitive: it widens both
|
|
287
|
+
# the SHAPE carrier (Tuple → Array, HashShape → Hash) AND the element types to `Dynamic[Top]`. The justification —
|
|
288
|
+
# once any method mutates the ivar, its post-mutation contents are statically unknown across method boundaries, so
|
|
289
|
+
# preserving the seed-write's element precision would be an unsound over-claim (e.g. `@struct = [{}]; somewhere:
|
|
290
|
+
# @struct << []` makes the next read's element no longer `Constant[{}]`).
|
|
370
291
|
def widen_type_for_observed_mutators(type, observed_methods)
|
|
371
292
|
members = type.is_a?(Type::Union) ? type.members : [type]
|
|
372
293
|
widened = members.map { |m| widen_member_for_observed_mutators(m, observed_methods) }
|
|
@@ -399,21 +320,13 @@ module Rigor
|
|
|
399
320
|
if name
|
|
400
321
|
child_prefix = qualified_prefix + [name]
|
|
401
322
|
if node.body
|
|
402
|
-
# Class-body level `@x = nil` writes don't
|
|
403
|
-
#
|
|
404
|
-
# class
|
|
405
|
-
#
|
|
406
|
-
#
|
|
407
|
-
#
|
|
408
|
-
#
|
|
409
|
-
# read-before-write nil contribution because the
|
|
410
|
-
# seed already reflects the author's acknowledged
|
|
411
|
-
# nullability via the def-body writes' union.
|
|
412
|
-
# Without this exemption, code that explicitly
|
|
413
|
-
# `@x = nil`s at class-body level then writes
|
|
414
|
-
# `@x = SomeClass.new` inside an instance method
|
|
415
|
-
# gains an unjustified nil widening at every
|
|
416
|
-
# read.
|
|
323
|
+
# Class-body level `@x = nil` writes don't initialise instance ivars at runtime (the class's own singleton
|
|
324
|
+
# ivars and the instance's ivars are separate stores), but they signal "the author KNOWS @x could be nil"
|
|
325
|
+
# and extend the B2.3 soundness gate: an ivar with a class-body write is exempted from the
|
|
326
|
+
# read-before-write nil contribution because the seed already reflects the author's acknowledged
|
|
327
|
+
# nullability via the def-body writes' union. Without this exemption, code that explicitly `@x = nil`s at
|
|
328
|
+
# class-body level then writes `@x = SomeClass.new` inside an instance method gains an unjustified nil
|
|
329
|
+
# widening at every read.
|
|
417
330
|
collect_class_body_ivar_writes(node.body, child_prefix.join("::"), init_writes) if init_writes
|
|
418
331
|
walk_class_ivars(node.body, child_prefix, default_scope, accumulator,
|
|
419
332
|
mutated_ivars, read_before_write, init_writes, method_assign_effects)
|
|
@@ -454,43 +367,30 @@ module Rigor
|
|
|
454
367
|
end
|
|
455
368
|
body_scope = default_scope.with_self_type(self_type)
|
|
456
369
|
|
|
457
|
-
# C2 — transient `@x = nil` dead-write elimination. When a
|
|
458
|
-
#
|
|
459
|
-
#
|
|
460
|
-
#
|
|
461
|
-
#
|
|
462
|
-
#
|
|
463
|
-
#
|
|
464
|
-
#
|
|
465
|
-
# class-ivar union, which then poisons reads in OTHER methods
|
|
466
|
-
# (e.g. ipaddr `IN4MASK ^ @mask_addr` rejects the resulting
|
|
467
|
-
# `Integer | nil`). The set holds the `object_id`s of the
|
|
468
|
-
# transient write nodes to skip; soundness is post-domination
|
|
469
|
-
# at the top statement level, so dropping the nil never hides
|
|
470
|
-
# a real runtime-nil read.
|
|
370
|
+
# C2 — transient `@x = nil` dead-write elimination. When a method body opens with an unconditional `@x = nil`
|
|
371
|
+
# (defensive init) and then *definitely* reassigns `@x` to a non-nil value on every completing path (a later
|
|
372
|
+
# unconditional statement-level write, OR an `if/else` whose both branches write `@x`), the opening nil is dead
|
|
373
|
+
# — it can never be observed at method exit. Recording it anyway folds a spurious `nil` constituent into the
|
|
374
|
+
# flow-insensitive class-ivar union, which then poisons reads in OTHER methods (e.g. ipaddr `IN4MASK ^
|
|
375
|
+
# @mask_addr` rejects the resulting `Integer | nil`). The set holds the `object_id`s of the transient write
|
|
376
|
+
# nodes to skip; soundness is post-domination at the top statement level, so dropping the nil never hides a real
|
|
377
|
+
# runtime-nil read.
|
|
471
378
|
dead_writes = dead_transient_nil_writes(def_node.body, class_name, method_assign_effects)
|
|
472
379
|
gather_ivar_writes(def_node.body, body_scope, class_name, accumulator,
|
|
473
380
|
EMPTY_GUARDED_IVARS, mutated_ivars, dead_writes)
|
|
474
381
|
|
|
475
|
-
# B2.3 — collect per-method evidence for the read-before-
|
|
476
|
-
#
|
|
477
|
-
#
|
|
478
|
-
#
|
|
479
|
-
# `contribute_read_before_write_nil!` after the whole
|
|
480
|
-
# class body has been walked, using `init_writes` as
|
|
481
|
-
# the soundness gate (an ivar written in `initialize`
|
|
482
|
-
# is initialised before any other method body runs).
|
|
382
|
+
# B2.3 — collect per-method evidence for the read-before- write nil contribution. The accumulator-level decision
|
|
383
|
+
# ("is this ivar truly read-before-write across the class lifetime?") is finalised at
|
|
384
|
+
# `contribute_read_before_write_nil!` after the whole class body has been walked, using `init_writes` as the
|
|
385
|
+
# soundness gate (an ivar written in `initialize` is initialised before any other method body runs).
|
|
483
386
|
collect_read_before_write_evidence(def_node, class_name, read_before_write, init_writes, default_scope)
|
|
484
387
|
end
|
|
485
388
|
|
|
486
|
-
# ADR-38 block-form: collects ivar writes from a CallNode's
|
|
487
|
-
#
|
|
488
|
-
#
|
|
489
|
-
#
|
|
490
|
-
#
|
|
491
|
-
# initializer (the caller has already verified the method name
|
|
492
|
-
# is declared as a block_method initializer), so there is no
|
|
493
|
-
# read-before-write evidence collection step here.
|
|
389
|
+
# ADR-38 block-form: collects ivar writes from a CallNode's block body (e.g. RSpec `before { @x = … }` / `let(:x)
|
|
390
|
+
# { … }`) and folds them into `init_writes`, suppressing the read-before-write nil contribution the same way a
|
|
391
|
+
# def-form initializer does. The block body is always treated as an initializer (the caller has already verified
|
|
392
|
+
# the method name is declared as a block_method initializer), so there is no read-before-write evidence collection
|
|
393
|
+
# step here.
|
|
494
394
|
def collect_block_ivar_writes(block_node, qualified_prefix, default_scope, accumulator,
|
|
495
395
|
mutated_ivars, init_writes)
|
|
496
396
|
return if block_node.body.nil? || qualified_prefix.empty?
|
|
@@ -509,10 +409,9 @@ module Rigor
|
|
|
509
409
|
seen_writes.each { |name| init_set << name }
|
|
510
410
|
end
|
|
511
411
|
|
|
512
|
-
# ADR-38 block-form gate: true when a loaded plugin declares
|
|
513
|
-
# `
|
|
514
|
-
#
|
|
515
|
-
# `covers_block_method?` instead of `covers_method?`.
|
|
412
|
+
# ADR-38 block-form gate: true when a loaded plugin declares `method_name` a block-form initializer for
|
|
413
|
+
# `class_name` (or an ancestor). Mirrors `additional_initializer?` but queries `covers_block_method?` instead of
|
|
414
|
+
# `covers_method?`.
|
|
516
415
|
def block_initializer?(class_name, method_name, default_scope)
|
|
517
416
|
return false if class_name.nil? || default_scope.nil?
|
|
518
417
|
|
|
@@ -530,14 +429,10 @@ module Rigor
|
|
|
530
429
|
false
|
|
531
430
|
end
|
|
532
431
|
|
|
533
|
-
# Walks the method body in AST (== execution) order
|
|
534
|
-
#
|
|
535
|
-
#
|
|
536
|
-
#
|
|
537
|
-
# bodies, every write target is unioned into
|
|
538
|
-
# `init_writes` instead — used by the finalisation step
|
|
539
|
-
# to suppress nil contribution for ivars the constructor
|
|
540
|
-
# guarantees are initialised.
|
|
432
|
+
# Walks the method body in AST (== execution) order tracking ivar names whose first reference is a read. The set
|
|
433
|
+
# is unioned into the class-wide `read_before_write` accumulator. For `initialize` def bodies, every write target
|
|
434
|
+
# is unioned into `init_writes` instead — used by the finalisation step to suppress nil contribution for ivars the
|
|
435
|
+
# constructor guarantees are initialised.
|
|
541
436
|
def collect_read_before_write_evidence(def_node, class_name, read_before_write, init_writes, default_scope = nil)
|
|
542
437
|
return if read_before_write.nil? || init_writes.nil?
|
|
543
438
|
|
|
@@ -545,12 +440,9 @@ module Rigor
|
|
|
545
440
|
read_first = Set.new
|
|
546
441
|
detect_read_before_write(def_node.body, seen_writes, read_first)
|
|
547
442
|
|
|
548
|
-
# ADR-38 — `initialize` is the built-in initializer gate;
|
|
549
|
-
# a
|
|
550
|
-
#
|
|
551
|
-
# setters) on a constrained class. Both fold their writes
|
|
552
|
-
# into `init_writes`, suppressing the read-before-write nil
|
|
553
|
-
# contribution for sibling readers.
|
|
443
|
+
# ADR-38 — `initialize` is the built-in initializer gate; a plugin may declare additional `def`-form initializer
|
|
444
|
+
# methods (minitest `setup`, Rails `after_initialize`, DI setters) on a constrained class. Both fold their
|
|
445
|
+
# writes into `init_writes`, suppressing the read-before-write nil contribution for sibling readers.
|
|
554
446
|
if def_node.name == :initialize ||
|
|
555
447
|
additional_initializer?(class_name, def_node.name, default_scope)
|
|
556
448
|
init_set = (init_writes[class_name] ||= Set.new)
|
|
@@ -564,16 +456,11 @@ module Rigor
|
|
|
564
456
|
read_first.each { |name| rbw_set << name }
|
|
565
457
|
end
|
|
566
458
|
|
|
567
|
-
# ADR-38 — true when a loaded plugin declares `method_name` an
|
|
568
|
-
#
|
|
569
|
-
#
|
|
570
|
-
#
|
|
571
|
-
#
|
|
572
|
-
# Tier A's `MacroBlockSelfType` uses). The whole lookup is
|
|
573
|
-
# wrapped so any resolution failure degrades to "no match" —
|
|
574
|
-
# since the gate only ever SUPPRESSES a nil contribution, a
|
|
575
|
-
# missed match is false-positive-safe (it merely leaves the
|
|
576
|
-
# existing nil widening in place).
|
|
459
|
+
# ADR-38 — true when a loaded plugin declares `method_name` an additional initializer for `class_name` (or an
|
|
460
|
+
# ancestor). Reads the plugin registry off the pre-pass scope's environment; the receiver-constraint match reuses
|
|
461
|
+
# `Environment#class_ordering` (the same mechanism ADR-16 Tier A's `MacroBlockSelfType` uses). The whole lookup is
|
|
462
|
+
# wrapped so any resolution failure degrades to "no match" — since the gate only ever SUPPRESSES a nil
|
|
463
|
+
# contribution, a missed match is false-positive-safe (it merely leaves the existing nil widening in place).
|
|
577
464
|
def additional_initializer?(class_name, method_name, default_scope)
|
|
578
465
|
return false if class_name.nil? || default_scope.nil?
|
|
579
466
|
|
|
@@ -609,15 +496,10 @@ module Rigor
|
|
|
609
496
|
].freeze
|
|
610
497
|
private_constant :IVAR_WRITE_NODES
|
|
611
498
|
|
|
612
|
-
# Walks class-body level statements (i.e. NOT inside any
|
|
613
|
-
#
|
|
614
|
-
#
|
|
615
|
-
#
|
|
616
|
-
# exempt ivars the author already knows might be nil
|
|
617
|
-
# (the `@x = nil` at class-body level is the canonical
|
|
618
|
-
# nullability acknowledgement; the instance @x is
|
|
619
|
-
# technically a separate store, but the pragmatic intent
|
|
620
|
-
# is unambiguous).
|
|
499
|
+
# Walks class-body level statements (i.e. NOT inside any nested DefNode / ClassNode / ModuleNode) and records
|
|
500
|
+
# every `@x = …` write target as a class-body init. Consumed by `contribute_read_before_write_nil!` to exempt
|
|
501
|
+
# ivars the author already knows might be nil (the `@x = nil` at class-body level is the canonical nullability
|
|
502
|
+
# acknowledgement; the instance @x is technically a separate store, but the pragmatic intent is unambiguous).
|
|
621
503
|
def collect_class_body_ivar_writes(node, class_name, init_writes)
|
|
622
504
|
return unless node.is_a?(Prism::Node)
|
|
623
505
|
return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
@@ -640,12 +522,9 @@ module Rigor
|
|
|
640
522
|
|
|
641
523
|
read_first << node.name if node.is_a?(Prism::InstanceVariableReadNode) && !seen_writes.include?(node.name)
|
|
642
524
|
|
|
643
|
-
# Descend BEFORE recording a write — `@x = @x + 1`'s
|
|
644
|
-
#
|
|
645
|
-
# the
|
|
646
|
-
# read-before-write semantically. Prism's
|
|
647
|
-
# `compact_child_nodes` returns the value child before
|
|
648
|
-
# the lvalue target, matching this order.
|
|
525
|
+
# Descend BEFORE recording a write — `@x = @x + 1`'s RHS is an `InstanceVariableReadNode` that runs before the
|
|
526
|
+
# write is committed; the read is therefore read-before-write semantically. Prism's `compact_child_nodes`
|
|
527
|
+
# returns the value child before the lvalue target, matching this order.
|
|
649
528
|
node.compact_child_nodes.each do |c|
|
|
650
529
|
detect_read_before_write(c, seen_writes, read_first)
|
|
651
530
|
end
|
|
@@ -669,24 +548,18 @@ module Rigor
|
|
|
669
548
|
guarded: guarded_ivars.include?(node.name))
|
|
670
549
|
end
|
|
671
550
|
|
|
672
|
-
# N1 — parallel / multiple assignment (`old, @cb = @cb, block`,
|
|
673
|
-
#
|
|
674
|
-
# `
|
|
675
|
-
#
|
|
676
|
-
#
|
|
677
|
-
#
|
|
678
|
-
#
|
|
679
|
-
# `@thr.alive?` undefined-for-nil. Record each ivar target with
|
|
680
|
-
# its tuple-position RHS type where the RHS is array/tuple-shaped,
|
|
681
|
-
# else the unanalyzable floor (the same `Dynamic[top]` a single
|
|
682
|
-
# write to an unknown RHS records — an unanalyzable multi-write
|
|
683
|
-
# means unknown, not nil).
|
|
551
|
+
# N1 — parallel / multiple assignment (`old, @cb = @cb, block`, `@i, @o, @e, @thr = Open3.popen3(cmd)`). A
|
|
552
|
+
# direct `InstanceVariableWriteNode` is the only write form this collector handled, so an ivar appearing as a
|
|
553
|
+
# `MultiWriteNode` target was silently dropped from the class-ivar union — leaving it to seed as pure `nil`
|
|
554
|
+
# (from a sibling `@cb = nil` ctor write, or absent entirely) and false-fire `if @cb` always-falsey /
|
|
555
|
+
# `@thr.alive?` undefined-for-nil. Record each ivar target with its tuple-position RHS type where the RHS is
|
|
556
|
+
# array/tuple-shaped, else the unanalyzable floor (the same `Dynamic[top]` a single write to an unknown RHS
|
|
557
|
+
# records — an unanalyzable multi-write means unknown, not nil).
|
|
684
558
|
record_multi_write_ivars(node, scope, class_name, accumulator)
|
|
685
559
|
|
|
686
560
|
record_ivar_mutator_call(node, class_name, mutated_ivars) if mutated_ivars && node.is_a?(Prism::CallNode)
|
|
687
561
|
|
|
688
|
-
# Don't recurse into nested defs, classes, or modules; their
|
|
689
|
-
# ivars belong to their own enclosing class.
|
|
562
|
+
# Don't recurse into nested defs, classes, or modules; their ivars belong to their own enclosing class.
|
|
690
563
|
return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
691
564
|
|
|
692
565
|
if node.is_a?(Prism::IfNode) || node.is_a?(Prism::UnlessNode)
|
|
@@ -700,13 +573,10 @@ module Rigor
|
|
|
700
573
|
end
|
|
701
574
|
end
|
|
702
575
|
|
|
703
|
-
# Records `@ivar.<method>(...)` calls whose method is in
|
|
704
|
-
#
|
|
705
|
-
#
|
|
706
|
-
#
|
|
707
|
-
# {.widen_mutated_ivar_entries!}). Always-safe to over-
|
|
708
|
-
# collect: any name that the widening primitive declines
|
|
709
|
-
# is ignored at finalization.
|
|
576
|
+
# Records `@ivar.<method>(...)` calls whose method is in `MutationWidening::ARRAY_MUTATORS` or `HASH_MUTATORS`.
|
|
577
|
+
# The class-ivar pre-pass uses the resulting set to widen the post-collected accumulator entries (see
|
|
578
|
+
# {.widen_mutated_ivar_entries!}). Always-safe to over- collect: any name that the widening primitive declines is
|
|
579
|
+
# ignored at finalization.
|
|
710
580
|
def record_ivar_mutator_call(node, class_name, mutated_ivars)
|
|
711
581
|
receiver = node.receiver
|
|
712
582
|
return unless receiver.is_a?(Prism::InstanceVariableReadNode)
|
|
@@ -718,22 +588,16 @@ module Rigor
|
|
|
718
588
|
per_ivar << node.name
|
|
719
589
|
end
|
|
720
590
|
|
|
721
|
-
# Walk an `IfNode` / `UnlessNode` so writes inside the THEN body
|
|
722
|
-
#
|
|
723
|
-
#
|
|
724
|
-
#
|
|
725
|
-
#
|
|
726
|
-
# working program. Mirrors the existing skip for `@x ||= v`
|
|
727
|
-
# (`Prism::InstanceVariableOrWriteNode`, which the pre-pass does
|
|
728
|
-
# not seed at all).
|
|
591
|
+
# Walk an `IfNode` / `UnlessNode` so writes inside the THEN body that look like defensive ivar initialisation gain
|
|
592
|
+
# a `nil` union in the seeded type. Without this, `@x = v unless @x` records `Constant[v]` for `@x`, then the
|
|
593
|
+
# predicate folds to that same constant and `flow.always-truthy-condition` fires against a working program.
|
|
594
|
+
# Mirrors the existing skip for `@x ||= v` (`Prism::InstanceVariableOrWriteNode`, which the pre-pass does not seed
|
|
595
|
+
# at all).
|
|
729
596
|
#
|
|
730
|
-
# Polarity-aware on purpose: only the THEN body picks up the
|
|
731
|
-
#
|
|
732
|
-
# would
|
|
733
|
-
#
|
|
734
|
-
# reads of `@x` would then surface a nil-receiver FP. The
|
|
735
|
-
# ELSE branch is left ungarded so those reads continue to type
|
|
736
|
-
# as they did before this fix.
|
|
597
|
+
# Polarity-aware on purpose: only the THEN body picks up the guard. The ELSE branch of `if @x; ...; else; @x =
|
|
598
|
+
# init; end` would otherwise be marked too — but that pattern (write @x in the else of `if @x`) is a separate
|
|
599
|
+
# idiom whose surrounding reads of `@x` would then surface a nil-receiver FP. The ELSE branch is left ungarded so
|
|
600
|
+
# those reads continue to type as they did before this fix.
|
|
737
601
|
def walk_conditional_ivar_writes(node, scope, class_name, accumulator, guarded_ivars,
|
|
738
602
|
mutated_ivars = nil, dead_writes = nil)
|
|
739
603
|
then_guards = then_body_guarded_ivars(node)
|
|
@@ -752,11 +616,9 @@ module Rigor
|
|
|
752
616
|
mutated_ivars, dead_writes)
|
|
753
617
|
end
|
|
754
618
|
|
|
755
|
-
# Returns the set of ivar names that, in the THEN body of this
|
|
756
|
-
#
|
|
757
|
-
#
|
|
758
|
-
# idiom. Conservative on purpose: only the shapes that
|
|
759
|
-
# idiomatically express "the ivar is missing" qualify.
|
|
619
|
+
# Returns the set of ivar names that, in the THEN body of this conditional, are statically known to be in a nil /
|
|
620
|
+
# unset state — i.e. the body really IS the defensive-init half of the idiom. Conservative on purpose: only the
|
|
621
|
+
# shapes that idiomatically express "the ivar is missing" qualify.
|
|
760
622
|
#
|
|
761
623
|
# For `unless P; body; end`, body runs when `P` is falsey:
|
|
762
624
|
# - `P = @x` (or `@x && other` / `@x || other`) → @x is falsey
|
|
@@ -817,12 +679,9 @@ module Rigor
|
|
|
817
679
|
end
|
|
818
680
|
end
|
|
819
681
|
|
|
820
|
-
# C2 — returns a Set of `object_id`s for transient `@x = nil`
|
|
821
|
-
#
|
|
822
|
-
#
|
|
823
|
-
# completing path. Such a nil can never be the ivar's value at
|
|
824
|
-
# method exit, so it must not contribute a `nil` constituent to
|
|
825
|
-
# the (flow-insensitive) class-ivar union.
|
|
682
|
+
# C2 — returns a Set of `object_id`s for transient `@x = nil` writes that a later statement in the same method
|
|
683
|
+
# body *definitely* overwrites with a non-nil value on every completing path. Such a nil can never be the ivar's
|
|
684
|
+
# value at method exit, so it must not contribute a `nil` constituent to the (flow-insensitive) class-ivar union.
|
|
826
685
|
#
|
|
827
686
|
# Scope is deliberately narrow and post-domination-sound:
|
|
828
687
|
# - only the top-level statement sequence of the body is
|
|
@@ -836,27 +695,21 @@ module Rigor
|
|
|
836
695
|
# - only `@x = nil` literal writes are ever marked dead — a
|
|
837
696
|
# non-nil transient is left untouched (it is already
|
|
838
697
|
# precision-additive in the union).
|
|
839
|
-
# WD3 — ADR-41-style hard cap on how deep the same-class-call
|
|
840
|
-
#
|
|
841
|
-
#
|
|
842
|
-
# Cycle-guarded independently; the cap bounds even acyclic
|
|
843
|
-
# chains.
|
|
698
|
+
# WD3 — ADR-41-style hard cap on how deep the same-class-call definite-assignment crediting recurses (the ctor
|
|
699
|
+
# calls `mask!`, which could itself call another same-class helper). Cycle-guarded independently; the cap bounds
|
|
700
|
+
# even acyclic chains.
|
|
844
701
|
SAME_CLASS_CALL_DEPTH_CAP = 3
|
|
845
702
|
private_constant :SAME_CLASS_CALL_DEPTH_CAP
|
|
846
703
|
|
|
847
|
-
# WD3 — builds the per-class definite-assignment summary
|
|
848
|
-
#
|
|
849
|
-
#
|
|
850
|
-
# `
|
|
851
|
-
# through an unconditionally-called same-class method (ipaddr's
|
|
852
|
-
# `initialize` reassigns `@mask_addr` via `mask!`).
|
|
704
|
+
# WD3 — builds the per-class definite-assignment summary `{class_name => {method_name => Set<ivar names assigned
|
|
705
|
+
# non-nil on every completing path>}}`. Used so a ctor's `dead_transient_nil_writes` can credit an indirect
|
|
706
|
+
# overwrite through an unconditionally-called same-class method (ipaddr's `initialize` reassigns `@mask_addr` via
|
|
707
|
+
# `mask!`).
|
|
853
708
|
#
|
|
854
|
-
# Each method's set is computed by the same suffix
|
|
855
|
-
#
|
|
856
|
-
# the
|
|
857
|
-
#
|
|
858
|
-
# transitively (depth-capped, cycle-guarded) so the resulting
|
|
859
|
-
# FLAT table is correct at depth 0 for the ctor lookup.
|
|
709
|
+
# Each method's set is computed by the same suffix definite-assignment analysis used for the ctor seed, run from
|
|
710
|
+
# the method body's first statement for every ivar the method writes anywhere. Same-class calls inside a method
|
|
711
|
+
# are credited transitively (depth-capped, cycle-guarded) so the resulting FLAT table is correct at depth 0 for
|
|
712
|
+
# the ctor lookup.
|
|
860
713
|
def build_method_assign_effects(root)
|
|
861
714
|
defs = collect_class_method_defs(root)
|
|
862
715
|
effects = {}
|
|
@@ -870,10 +723,9 @@ module Rigor
|
|
|
870
723
|
effects.freeze
|
|
871
724
|
end
|
|
872
725
|
|
|
873
|
-
# Collects `{class_name => {method_name => DefNode}}` for every
|
|
874
|
-
#
|
|
875
|
-
#
|
|
876
|
-
# instance-method calls on `self`. Last def wins on redefinition.
|
|
726
|
+
# Collects `{class_name => {method_name => DefNode}}` for every instance-method def in the program. Singleton defs
|
|
727
|
+
# (`def self.x`) are excluded — the ctor-call crediting only follows instance-method calls on `self`. Last def
|
|
728
|
+
# wins on redefinition.
|
|
877
729
|
def collect_class_method_defs(root, prefix = [], acc = {})
|
|
878
730
|
return acc unless root.is_a?(Prism::Node)
|
|
879
731
|
|
|
@@ -894,10 +746,9 @@ module Rigor
|
|
|
894
746
|
acc
|
|
895
747
|
end
|
|
896
748
|
|
|
897
|
-
# Computes the definite-assignment set for one method, memoised
|
|
898
|
-
#
|
|
899
|
-
#
|
|
900
|
-
# (sound under-approximation), so mutual recursion terminates.
|
|
749
|
+
# Computes the definite-assignment set for one method, memoised per def node. The `memo` cycle-guards: a method
|
|
750
|
+
# re-entered while its own summary is in progress contributes nothing (sound under-approximation), so mutual
|
|
751
|
+
# recursion terminates.
|
|
901
752
|
def method_definite_assigns(class_name, _method_name, def_node, defs, effects, memo, depth)
|
|
902
753
|
return Set.new if def_node.body.nil?
|
|
903
754
|
return memo[def_node] if memo.key?(def_node)
|
|
@@ -906,9 +757,8 @@ module Rigor
|
|
|
906
757
|
memo[def_node] = Set.new # in-progress sentinel (cycle guard)
|
|
907
758
|
statements = top_level_statements(def_node.body)
|
|
908
759
|
candidates = ivar_write_targets(def_node.body)
|
|
909
|
-
# A transient `@x = nil` opener whose own method reassigns it
|
|
910
|
-
#
|
|
911
|
-
# crediting is computed at the BUILD-time depth.
|
|
760
|
+
# A transient `@x = nil` opener whose own method reassigns it later must still count `@x` as assigned for
|
|
761
|
+
# callers, so the crediting is computed at the BUILD-time depth.
|
|
912
762
|
resolver = MethodEffectResolver.new(self, class_name, defs, effects, memo, depth)
|
|
913
763
|
assigns = Set.new
|
|
914
764
|
candidates.each do |ivar|
|
|
@@ -917,8 +767,8 @@ module Rigor
|
|
|
917
767
|
memo[def_node] = assigns
|
|
918
768
|
end
|
|
919
769
|
|
|
920
|
-
# Every ivar this body assigns a non-nil value to ANYWHERE (the
|
|
921
|
-
#
|
|
770
|
+
# Every ivar this body assigns a non-nil value to ANYWHERE (the candidate set for the method's definite-assignment
|
|
771
|
+
# scan).
|
|
922
772
|
def ivar_write_targets(node, acc = Set.new)
|
|
923
773
|
return acc unless node.is_a?(Prism::Node)
|
|
924
774
|
|
|
@@ -927,10 +777,9 @@ module Rigor
|
|
|
927
777
|
acc
|
|
928
778
|
end
|
|
929
779
|
|
|
930
|
-
# Build-time variant of `suffix_definitely_assigns?` that resolves
|
|
931
|
-
#
|
|
932
|
-
#
|
|
933
|
-
# rather than the finished flat table.
|
|
780
|
+
# Build-time variant of `suffix_definitely_assigns?` that resolves same-class calls through the lazy `resolver`
|
|
781
|
+
# (which recurses into `method_definite_assigns` for not-yet-computed callees) rather than the finished flat
|
|
782
|
+
# table.
|
|
934
783
|
def suffix_definitely_assigns_with_resolver?(statements, from, target, class_name, resolver, depth)
|
|
935
784
|
statements[from..].each do |stmt|
|
|
936
785
|
outcome = statement_assignment_outcome(stmt, target, class_name, resolver, depth, nil)
|
|
@@ -940,9 +789,8 @@ module Rigor
|
|
|
940
789
|
false
|
|
941
790
|
end
|
|
942
791
|
|
|
943
|
-
# Adapts `effects.dig(class, method)` for build-time crediting:
|
|
944
|
-
#
|
|
945
|
-
# it on demand (depth+1) via `method_definite_assigns`.
|
|
792
|
+
# Adapts `effects.dig(class, method)` for build-time crediting: when the callee summary is not yet in the flat
|
|
793
|
+
# table, compute it on demand (depth+1) via `method_definite_assigns`.
|
|
946
794
|
class MethodEffectResolver
|
|
947
795
|
def initialize(indexer, class_name, defs, effects, memo, depth)
|
|
948
796
|
@indexer = indexer
|
|
@@ -974,12 +822,9 @@ module Rigor
|
|
|
974
822
|
statements.each_with_index do |stmt, i|
|
|
975
823
|
next unless stmt.is_a?(Prism::InstanceVariableWriteNode) && nil_literal_value?(stmt.value)
|
|
976
824
|
|
|
977
|
-
# The opening `@x = nil` is dead when every completing path
|
|
978
|
-
#
|
|
979
|
-
#
|
|
980
|
-
# `@x` non-nil. The suffix analysis credits an
|
|
981
|
-
# unconditionally-called same-class method's own definite
|
|
982
|
-
# assignments via `method_assign_effects`.
|
|
825
|
+
# The opening `@x = nil` is dead when every completing path of the SUFFIX after it (normal end OR early
|
|
826
|
+
# `return`, never a `raise`-terminated path) definitely reassigns `@x` non-nil. The suffix analysis credits an
|
|
827
|
+
# unconditionally-called same-class method's own definite assignments via `method_assign_effects`.
|
|
983
828
|
if suffix_definitely_assigns?(statements, i + 1, stmt.name, class_name, method_assign_effects)
|
|
984
829
|
(dead ||= Set.new) << stmt.object_id
|
|
985
830
|
end
|
|
@@ -999,24 +844,19 @@ module Rigor
|
|
|
999
844
|
node.is_a?(Prism::NilNode)
|
|
1000
845
|
end
|
|
1001
846
|
|
|
1002
|
-
# True when, starting from `statements[from]`, EVERY path that
|
|
1003
|
-
#
|
|
1004
|
-
#
|
|
1005
|
-
#
|
|
1006
|
-
# ignored (they never observe the ivar at method exit). A path
|
|
1007
|
-
# that can fall through `statements` without assigning fails.
|
|
847
|
+
# True when, starting from `statements[from]`, EVERY path that completes the method (falls off the end OR hits an
|
|
848
|
+
# early `return`) definitely assigns `target` a non-nil value first. Paths terminated by `raise` are not
|
|
849
|
+
# completing paths and are ignored (they never observe the ivar at method exit). A path that can fall through
|
|
850
|
+
# `statements` without assigning fails.
|
|
1008
851
|
def suffix_definitely_assigns?(statements, from, target, class_name, effects)
|
|
1009
852
|
statements[from..].each do |stmt|
|
|
1010
853
|
outcome = statement_assignment_outcome(stmt, target, class_name, effects, 0, nil)
|
|
1011
|
-
# The statement assigned on every continuing path -> the
|
|
1012
|
-
# suffix is satisfied no matter what follows.
|
|
854
|
+
# The statement assigned on every continuing path -> the suffix is satisfied no matter what follows.
|
|
1013
855
|
return true if outcome == :assigned
|
|
1014
|
-
# The statement terminates control here (return/raise) and
|
|
1015
|
-
#
|
|
1016
|
-
# completing path reached exit without the assignment.
|
|
856
|
+
# The statement terminates control here (return/raise) and the value it carried did not assign on every path
|
|
857
|
+
# -> some completing path reached exit without the assignment.
|
|
1017
858
|
return false if outcome == :terminates_unassigned
|
|
1018
|
-
# Otherwise (:falls_through_unassigned) keep scanning the
|
|
1019
|
-
# remaining statements.
|
|
859
|
+
# Otherwise (:falls_through_unassigned) keep scanning the remaining statements.
|
|
1020
860
|
end
|
|
1021
861
|
# Fell off the end with no definite assignment.
|
|
1022
862
|
false
|
|
@@ -1052,20 +892,16 @@ module Rigor
|
|
|
1052
892
|
when Prism::ReturnNode
|
|
1053
893
|
:terminates_unassigned
|
|
1054
894
|
else
|
|
1055
|
-
# Any other statement — including a bare `raise`/`fail`,
|
|
1056
|
-
#
|
|
1057
|
-
# the
|
|
1058
|
-
# path never reaches method exit. Keep scanning the suffix.
|
|
895
|
+
# Any other statement — including a bare `raise`/`fail`, which terminates without a completing path that
|
|
896
|
+
# observes the seed nil — is neutral: control either continues or the path never reaches method exit. Keep
|
|
897
|
+
# scanning the suffix.
|
|
1059
898
|
:falls_through_unassigned
|
|
1060
899
|
end
|
|
1061
900
|
end
|
|
1062
901
|
|
|
1063
|
-
# True when a branch body (a StatementsNode / single node)
|
|
1064
|
-
#
|
|
1065
|
-
#
|
|
1066
|
-
# raise (vacuously safe — no completing path observes the seed
|
|
1067
|
-
# nil). Returns false if any path can complete/return without the
|
|
1068
|
-
# assignment.
|
|
902
|
+
# True when a branch body (a StatementsNode / single node) definitely assigns `target` non-nil on every path that
|
|
903
|
+
# completes the method through it, OR terminates every path by raise (vacuously safe — no completing path observes
|
|
904
|
+
# the seed nil). Returns false if any path can complete/return without the assignment.
|
|
1069
905
|
def branch_definitely_assigns?(branch, target, class_name, effects, depth, visiting)
|
|
1070
906
|
stmts = top_level_statements(branch)
|
|
1071
907
|
return false if stmts.empty?
|
|
@@ -1075,17 +911,14 @@ module Rigor
|
|
|
1075
911
|
return true if outcome == :assigned
|
|
1076
912
|
return false if outcome == :terminates_unassigned
|
|
1077
913
|
end
|
|
1078
|
-
# Reached the end of the branch without a definite assignment;
|
|
1079
|
-
#
|
|
1080
|
-
# completing path falls out of it).
|
|
914
|
+
# Reached the end of the branch without a definite assignment; safe only if the branch's last statement always
|
|
915
|
+
# raises (no completing path falls out of it).
|
|
1081
916
|
always_raises?(stmts.last)
|
|
1082
917
|
end
|
|
1083
918
|
|
|
1084
|
-
# `if`/`unless` is a definite assignment of `target` only when
|
|
1085
|
-
#
|
|
1086
|
-
#
|
|
1087
|
-
# assignment -> not definite. Modifier-form `if`/`unless` (no
|
|
1088
|
-
# else, single predicate'd statement) likewise.
|
|
919
|
+
# `if`/`unless` is a definite assignment of `target` only when BOTH the then and else arms definitely assign (or
|
|
920
|
+
# raise-out). A missing else arm means the fall-through path skips the assignment -> not definite. Modifier-form
|
|
921
|
+
# `if`/`unless` (no else, single predicate'd statement) likewise.
|
|
1089
922
|
def conditional_assignment_outcome(node, target, class_name, effects, depth, visiting)
|
|
1090
923
|
else_branch = node.is_a?(Prism::IfNode) ? node.subsequent : node.else_clause
|
|
1091
924
|
return :falls_through_unassigned unless else_branch.is_a?(Prism::ElseNode)
|
|
@@ -1096,10 +929,8 @@ module Rigor
|
|
|
1096
929
|
then_ok && else_ok ? :assigned : :falls_through_unassigned
|
|
1097
930
|
end
|
|
1098
931
|
|
|
1099
|
-
# `case` is a definite assignment only when there is a real
|
|
1100
|
-
#
|
|
1101
|
-
# definitely assigns (or raises-out). A missing else lets an
|
|
1102
|
-
# unmatched subject fall through unassigned.
|
|
932
|
+
# `case` is a definite assignment only when there is a real `else` clause AND every `when`/`in` body plus the else
|
|
933
|
+
# body definitely assigns (or raises-out). A missing else lets an unmatched subject fall through unassigned.
|
|
1103
934
|
def case_assignment_outcome(node, target, class_name, effects, depth, visiting)
|
|
1104
935
|
else_clause = node.else_clause
|
|
1105
936
|
return :falls_through_unassigned unless else_clause.is_a?(Prism::ElseNode)
|
|
@@ -1112,10 +943,9 @@ module Rigor
|
|
|
1112
943
|
all_ok ? :assigned : :falls_through_unassigned
|
|
1113
944
|
end
|
|
1114
945
|
|
|
1115
|
-
# True when `node` (a single statement or its last statement) is
|
|
1116
|
-
#
|
|
1117
|
-
#
|
|
1118
|
-
# non-completing (they never observe the seed nil).
|
|
946
|
+
# True when `node` (a single statement or its last statement) is an unconditional `raise`/`fail` call that always
|
|
947
|
+
# terminates the path — used to treat raise-terminated branches as non-completing (they never observe the seed
|
|
948
|
+
# nil).
|
|
1119
949
|
def always_raises?(node)
|
|
1120
950
|
node = top_level_statements(node).last if node.is_a?(Prism::StatementsNode)
|
|
1121
951
|
return false unless node.is_a?(Prism::CallNode)
|
|
@@ -1124,11 +954,9 @@ module Rigor
|
|
|
1124
954
|
%i[raise fail].include?(node.name)
|
|
1125
955
|
end
|
|
1126
956
|
|
|
1127
|
-
# True when `call` is an unconditional, statement-level,
|
|
1128
|
-
#
|
|
1129
|
-
#
|
|
1130
|
-
# block, on another receiver, or to an unresolved name contribute
|
|
1131
|
-
# nothing (the seed nil stays).
|
|
957
|
+
# True when `call` is an unconditional, statement-level, implicit-self (or `self.`) call to a SAME-CLASS method
|
|
958
|
+
# whose definite-assignment summary includes `target`. Calls through a block, on another receiver, or to an
|
|
959
|
+
# unresolved name contribute nothing (the seed nil stays).
|
|
1132
960
|
def unconditional_call_assigns?(call, target, class_name, effects, depth, _visiting)
|
|
1133
961
|
return false if effects.nil? || class_name.nil?
|
|
1134
962
|
return false if depth >= SAME_CLASS_CALL_DEPTH_CAP
|
|
@@ -1146,23 +974,14 @@ module Rigor
|
|
|
1146
974
|
def record_ivar_write(node, scope, class_name, accumulator, guarded: false)
|
|
1147
975
|
rvalue_type = scope.type_of(node.value)
|
|
1148
976
|
|
|
1149
|
-
# `@x = nil unless @x` / `@y = false unless @y` —
|
|
1150
|
-
#
|
|
1151
|
-
#
|
|
1152
|
-
#
|
|
1153
|
-
# falsey
|
|
1154
|
-
#
|
|
1155
|
-
#
|
|
1156
|
-
#
|
|
1157
|
-
# `Constant[false]` and the
|
|
1158
|
-
# `flow.always-truthy-condition` / `-always-falsey-`
|
|
1159
|
-
# rule false-fires on the no-op-but-documented-default
|
|
1160
|
-
# idiom. Skip the seed contribution for this write
|
|
1161
|
-
# (matches the existing skip for `@x ||= v`, which the
|
|
1162
|
-
# pre-pass also does not seed). Other writes to the
|
|
1163
|
-
# same ivar still contribute; the falsey-default write
|
|
1164
|
-
# carries no useful precision the predicate hasn't
|
|
1165
|
-
# already given us. See tdiary-core HEAD `ee40c2b`
|
|
977
|
+
# `@x = nil unless @x` / `@y = false unless @y` — follow-up to the polarity-aware defensive-init guard fix
|
|
978
|
+
# (ROADMAP § Future cycles — "Defensive ivar-init with nil / false rvalue"). When the rvalue is itself a falsey
|
|
979
|
+
# Constant, `union(rvalue, Constant[nil])` collapses (for `nil`) or doesn't widen the type's truthiness profile
|
|
980
|
+
# (for `false`) — the predicate `unless @x` then folds to a single `Constant[nil]` / `Constant[false]` and the
|
|
981
|
+
# `flow.always-truthy-condition` / `-always-falsey-` rule false-fires on the no-op-but-documented-default idiom.
|
|
982
|
+
# Skip the seed contribution for this write (matches the existing skip for `@x ||= v`, which the pre-pass also
|
|
983
|
+
# does not seed). Other writes to the same ivar still contribute; the falsey-default write carries no useful
|
|
984
|
+
# precision the predicate hasn't already given us. See tdiary-core HEAD `ee40c2b`
|
|
1166
985
|
# `lib/tdiary/configuration.rb:157` for the worked site.
|
|
1167
986
|
return if guarded && falsey_constant?(rvalue_type)
|
|
1168
987
|
|
|
@@ -1170,9 +989,8 @@ module Rigor
|
|
|
1170
989
|
accumulate_ivar_type(accumulator, class_name, node.name, rvalue_type)
|
|
1171
990
|
end
|
|
1172
991
|
|
|
1173
|
-
# Unions `type` into the class-ivar accumulator for `(class_name,
|
|
1174
|
-
#
|
|
1175
|
-
# (parallel-assignment) collectors.
|
|
992
|
+
# Unions `type` into the class-ivar accumulator for `(class_name, ivar_name)`. Shared by the single-write and
|
|
993
|
+
# multi-write (parallel-assignment) collectors.
|
|
1176
994
|
def accumulate_ivar_type(accumulator, class_name, ivar_name, type)
|
|
1177
995
|
accumulator[class_name] ||= {}
|
|
1178
996
|
existing = accumulator[class_name][ivar_name]
|
|
@@ -1180,17 +998,13 @@ module Rigor
|
|
|
1180
998
|
existing ? Type::Combinator.union(existing, type) : type
|
|
1181
999
|
end
|
|
1182
1000
|
|
|
1183
|
-
# N1 — records each `InstanceVariableTargetNode` of a
|
|
1184
|
-
#
|
|
1185
|
-
#
|
|
1186
|
-
#
|
|
1187
|
-
#
|
|
1188
|
-
#
|
|
1189
|
-
#
|
|
1190
|
-
# cannot decompose means the value is *unknown*, and `Dynamic[top]`
|
|
1191
|
-
# is the sound union constituent, mirroring what a single write to
|
|
1192
|
-
# an unknown RHS records). Nested targets (`(@a, @b), @c = …`)
|
|
1193
|
-
# recurse with the slot's type as the new RHS type.
|
|
1001
|
+
# N1 — records each `InstanceVariableTargetNode` of a `MultiWriteNode` (parallel / multiple assignment) into the
|
|
1002
|
+
# class-ivar union, with the best cheap per-slot type. When the RHS is array/tuple-shaped (`Type::Tuple`) the ivar
|
|
1003
|
+
# at position `i` records the type of element `i`; otherwise — an unanalyzable RHS such as `Open3.popen3(cmd)`
|
|
1004
|
+
# typing to `Dynamic[top]` — every ivar slot records that unanalyzable floor (NOT `nil`: a multi-write we cannot
|
|
1005
|
+
# decompose means the value is *unknown*, and `Dynamic[top]` is the sound union constituent, mirroring what a
|
|
1006
|
+
# single write to an unknown RHS records). Nested targets (`(@a, @b), @c = …`) recurse with the slot's type as the
|
|
1007
|
+
# new RHS type.
|
|
1194
1008
|
def record_multi_write_ivars(node, scope, class_name, accumulator)
|
|
1195
1009
|
return unless node.is_a?(Prism::MultiWriteNode)
|
|
1196
1010
|
|
|
@@ -1198,10 +1012,8 @@ module Rigor
|
|
|
1198
1012
|
record_multi_target_ivars(node, rhs_type, class_name, accumulator)
|
|
1199
1013
|
end
|
|
1200
1014
|
|
|
1201
|
-
# Walks a `MultiWriteNode` / `MultiTargetNode` target tree against
|
|
1202
|
-
# `
|
|
1203
|
-
# `MultiTargetBinder`'s tuple decomposition but for ivar (rather
|
|
1204
|
-
# than local-variable) targets.
|
|
1015
|
+
# Walks a `MultiWriteNode` / `MultiTargetNode` target tree against `rhs_type`, recording ivar targets per slot.
|
|
1016
|
+
# Mirrors `MultiTargetBinder`'s tuple decomposition but for ivar (rather than local-variable) targets.
|
|
1205
1017
|
def record_multi_target_ivars(node, rhs_type, class_name, accumulator)
|
|
1206
1018
|
lefts = node.lefts || []
|
|
1207
1019
|
rest = node.rest
|
|
@@ -1233,12 +1045,10 @@ module Rigor
|
|
|
1233
1045
|
end
|
|
1234
1046
|
end
|
|
1235
1047
|
|
|
1236
|
-
# The per-slot type for index `i` of a tuple RHS. A missing slot
|
|
1237
|
-
#
|
|
1238
|
-
#
|
|
1239
|
-
#
|
|
1240
|
-
# genuine `T | nil`, and any spurious nil is removed by the
|
|
1241
|
-
# flow-side narrowing, not by dropping it at collection time.
|
|
1048
|
+
# The per-slot type for index `i` of a tuple RHS. A missing slot (over-destructure) is `nil` at runtime; a present
|
|
1049
|
+
# slot keeps its type. Unlike the local-variable binder we do NOT soften an optional slot here — a class-ivar seed
|
|
1050
|
+
# deliberately preserves a genuine `T | nil`, and any spurious nil is removed by the flow-side narrowing, not by
|
|
1051
|
+
# dropping it at collection time.
|
|
1242
1052
|
def multi_write_slot_type(elements, index)
|
|
1243
1053
|
element = elements[index]
|
|
1244
1054
|
return Type::Combinator.constant_of(nil) if element.nil?
|
|
@@ -1261,9 +1071,8 @@ module Rigor
|
|
|
1261
1071
|
expression = splat_node.expression
|
|
1262
1072
|
return unless expression.is_a?(Prism::InstanceVariableTargetNode)
|
|
1263
1073
|
|
|
1264
|
-
# A splat collects the middle slots into an Array; the precise
|
|
1265
|
-
#
|
|
1266
|
-
# unanalyzable floor (an Array of unknown), never nil.
|
|
1074
|
+
# A splat collects the middle slots into an Array; the precise element type is not worth recovering here. Record
|
|
1075
|
+
# the unanalyzable floor (an Array of unknown), never nil.
|
|
1267
1076
|
accumulate_ivar_type(accumulator, class_name, expression.name, Type::Combinator.untyped)
|
|
1268
1077
|
end
|
|
1269
1078
|
|
|
@@ -1271,13 +1080,10 @@ module Rigor
|
|
|
1271
1080
|
type.is_a?(Type::Constant) && (type.value.nil? || type.value == false)
|
|
1272
1081
|
end
|
|
1273
1082
|
|
|
1274
|
-
# Slice 7 phase 6 — class-cvar pre-pass. Same shape as the
|
|
1275
|
-
#
|
|
1276
|
-
#
|
|
1277
|
-
#
|
|
1278
|
-
# facets. The resulting table is seeded into both instance
|
|
1279
|
-
# and singleton method bodies through
|
|
1280
|
-
# `Scope#class_cvars_for`.
|
|
1083
|
+
# Slice 7 phase 6 — class-cvar pre-pass. Same shape as the ivar pre-pass but collects
|
|
1084
|
+
# `Prism::ClassVariableWriteNode` writes inside ANY def body (instance or singleton) of the enclosing class,
|
|
1085
|
+
# because Ruby cvars are shared across both facets. The resulting table is seeded into both instance and singleton
|
|
1086
|
+
# method bodies through `Scope#class_cvars_for`.
|
|
1281
1087
|
def build_class_cvar_index(root, default_scope)
|
|
1282
1088
|
accumulator = {}
|
|
1283
1089
|
walk_class_cvars(root, [], default_scope, accumulator)
|
|
@@ -1330,12 +1136,9 @@ module Rigor
|
|
|
1330
1136
|
existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type
|
|
1331
1137
|
end
|
|
1332
1138
|
|
|
1333
|
-
# Slice 7 phase 6 — program-global pre-pass. Globals are
|
|
1334
|
-
#
|
|
1335
|
-
#
|
|
1336
|
-
# `Prism::GlobalVariableWriteNode` in the program (top-level
|
|
1337
|
-
# AND inside method bodies). The same accumulator is
|
|
1338
|
-
# seeded into every method body and the top-level scope.
|
|
1139
|
+
# Slice 7 phase 6 — program-global pre-pass. Globals are process-wide so the accumulator is a flat `Hash[Symbol,
|
|
1140
|
+
# Type::t]` populated from every `Prism::GlobalVariableWriteNode` in the program (top-level AND inside method
|
|
1141
|
+
# bodies). The same accumulator is seeded into every method body and the top-level scope.
|
|
1339
1142
|
def build_program_global_index(root, default_scope)
|
|
1340
1143
|
accumulator = {}
|
|
1341
1144
|
gather_global_writes(root, default_scope, accumulator)
|
|
@@ -1356,14 +1159,10 @@ module Rigor
|
|
|
1356
1159
|
existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type
|
|
1357
1160
|
end
|
|
1358
1161
|
|
|
1359
|
-
# Slice 7 phase 9 — in-source constant value pre-pass.
|
|
1360
|
-
#
|
|
1361
|
-
#
|
|
1362
|
-
# `
|
|
1363
|
-
# accumulates by qualified name. Constants defined inside
|
|
1364
|
-
# a class body are qualified with the surrounding class
|
|
1365
|
-
# path; constants written via a path (`Foo::BAR = ...`)
|
|
1366
|
-
# use the rendered path as-is.
|
|
1162
|
+
# Slice 7 phase 9 — in-source constant value pre-pass. Walks the entire program (top-level AND inside class /
|
|
1163
|
+
# module / def bodies) for `Prism::ConstantWriteNode` and `Prism::ConstantPathWriteNode`, types each rvalue, and
|
|
1164
|
+
# accumulates by qualified name. Constants defined inside a class body are qualified with the surrounding class
|
|
1165
|
+
# path; constants written via a path (`Foo::BAR = ...`) use the rendered path as-is.
|
|
1367
1166
|
def build_in_source_constants(root, default_scope)
|
|
1368
1167
|
accumulator = {}
|
|
1369
1168
|
walk_constant_writes(root, [], default_scope, accumulator)
|
|
@@ -1406,14 +1205,10 @@ module Rigor
|
|
|
1406
1205
|
accumulator[full] = existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type
|
|
1407
1206
|
end
|
|
1408
1207
|
|
|
1409
|
-
# Survey item (e): when the rvalue is a recognised
|
|
1410
|
-
# `
|
|
1411
|
-
# `
|
|
1412
|
-
#
|
|
1413
|
-
# `Singleton[<full>]` so the discovered-method table
|
|
1414
|
-
# registered under `full` becomes reachable through
|
|
1415
|
-
# singleton-side dispatch (`Const.[]=` etc.). Returns nil
|
|
1416
|
-
# for non-meta-new rvalues so the caller falls back to the
|
|
1208
|
+
# Survey item (e): when the rvalue is a recognised `Module.new do ... end` / `Class.new do ... end` /
|
|
1209
|
+
# `Struct.new(*sym) do ... end` / `Data.define(*sym) do ... end` form, type the named constant as
|
|
1210
|
+
# `Singleton[<full>]` so the discovered-method table registered under `full` becomes reachable through
|
|
1211
|
+
# singleton-side dispatch (`Const.[]=` etc.). Returns nil for non-meta-new rvalues so the caller falls back to the
|
|
1417
1212
|
# default `body_scope.type_of(node.value)` shape.
|
|
1418
1213
|
def meta_new_constant_type(node, full)
|
|
1419
1214
|
return nil unless meta_new_block_body(node)
|
|
@@ -1421,10 +1216,8 @@ module Rigor
|
|
|
1421
1216
|
Type::Combinator.singleton_of(full)
|
|
1422
1217
|
end
|
|
1423
1218
|
|
|
1424
|
-
# Slice 7 phase 12 — in-source method discovery pre-pass, fused with
|
|
1425
|
-
# the
|
|
1426
|
-
# produces BOTH tables the per-file `index` and the cross-file
|
|
1427
|
-
# pre-pass each need together:
|
|
1219
|
+
# Slice 7 phase 12 — in-source method discovery pre-pass, fused with the instance-method def-node pre-pass (v0.0.2
|
|
1220
|
+
# #5). One descent produces BOTH tables the per-file `index` and the cross-file pre-pass each need together:
|
|
1428
1221
|
#
|
|
1429
1222
|
# - `methods` : `{class_name => {method => :instance | :singleton}}`
|
|
1430
1223
|
# for every `def` / `define_method(:name)` / `attr_*` / `alias` /
|
|
@@ -1434,10 +1227,9 @@ module Rigor
|
|
|
1434
1227
|
# table; singleton defs and `define_method` are intentionally
|
|
1435
1228
|
# skipped — `record_def_node` filters them).
|
|
1436
1229
|
#
|
|
1437
|
-
# `walk_methods` and `walk_def_nodes` had byte-identical class /
|
|
1438
|
-
#
|
|
1439
|
-
#
|
|
1440
|
-
# of traversing every file twice.
|
|
1230
|
+
# `walk_methods` and `walk_def_nodes` had byte-identical class / module / singleton / meta-block descents (both
|
|
1231
|
+
# stop at `DefNode`), so a single combined walk records both accumulators at once instead of traversing every file
|
|
1232
|
+
# twice.
|
|
1441
1233
|
def build_methods_and_def_nodes(root)
|
|
1442
1234
|
methods = {}
|
|
1443
1235
|
def_nodes = {}
|
|
@@ -1446,9 +1238,8 @@ module Rigor
|
|
|
1446
1238
|
[methods.transform_values(&:freeze).freeze, def_nodes.transform_values(&:freeze).freeze]
|
|
1447
1239
|
end
|
|
1448
1240
|
|
|
1449
|
-
# Merges two `class_name => { method => kind }` tables, unioning
|
|
1450
|
-
#
|
|
1451
|
-
# current file's table combine instead of clobbering).
|
|
1241
|
+
# Merges two `class_name => { method => kind }` tables, unioning the per-class method maps (so a seeded cross-file
|
|
1242
|
+
# table and the current file's table combine instead of clobbering).
|
|
1452
1243
|
def deep_merge_class_methods(base, overlay)
|
|
1453
1244
|
return overlay if base.nil? || base.empty?
|
|
1454
1245
|
return base if overlay.empty?
|
|
@@ -1459,13 +1250,11 @@ module Rigor
|
|
|
1459
1250
|
end
|
|
1460
1251
|
|
|
1461
1252
|
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
1462
|
-
# Combined `walk_methods` + `walk_def_nodes` descent. The two walks
|
|
1463
|
-
#
|
|
1464
|
-
#
|
|
1465
|
-
#
|
|
1466
|
-
#
|
|
1467
|
-
# children carry no def / class node, so not descending them is
|
|
1468
|
-
# byte-identical for `def_nodes` too). See {#build_methods_and_def_nodes}.
|
|
1253
|
+
# Combined `walk_methods` + `walk_def_nodes` descent. The two walks had identical class / module / singleton-class
|
|
1254
|
+
# / meta-block traversals and both stopped at `DefNode`; the only divergences are leaf actions (recorded into the
|
|
1255
|
+
# right accumulator) and the original `walk_methods` returning at `AliasMethodNode` (its symbol-only children
|
|
1256
|
+
# carry no def / class node, so not descending them is byte-identical for `def_nodes` too). See
|
|
1257
|
+
# {#build_methods_and_def_nodes}.
|
|
1469
1258
|
def walk_methods_and_def_nodes(node, qualified_prefix, in_singleton_class, methods_acc, def_nodes_acc)
|
|
1470
1259
|
return unless node.is_a?(Prism::Node)
|
|
1471
1260
|
|
|
@@ -1536,16 +1325,11 @@ module Rigor
|
|
|
1536
1325
|
end
|
|
1537
1326
|
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
1538
1327
|
|
|
1539
|
-
# v0.1.2 — when a `Const = Data.define(*sym) do ... end`
|
|
1540
|
-
#
|
|
1541
|
-
#
|
|
1542
|
-
#
|
|
1543
|
-
# the
|
|
1544
|
-
# `Const = Class.new(?super) do ... end` — the
|
|
1545
|
-
# ADR-16 Tier A "block-as-method" idiom at constant-write
|
|
1546
|
-
# position. Returns the block body node (a
|
|
1547
|
-
# `Prism::StatementsNode`) when the rvalue matches; nil
|
|
1548
|
-
# otherwise. Used by `walk_methods` / `walk_def_nodes` to
|
|
1328
|
+
# v0.1.2 — when a `Const = Data.define(*sym) do ... end` / `Const = Struct.new(*sym) do ... end` constant write
|
|
1329
|
+
# carries a block, the block body holds method overrides whose canonical class is `Const`. Survey item (e)
|
|
1330
|
+
# extended the recognition to `Const = Module.new do ... end` and `Const = Class.new(?super) do ... end` — the
|
|
1331
|
+
# ADR-16 Tier A "block-as-method" idiom at constant-write position. Returns the block body node (a
|
|
1332
|
+
# `Prism::StatementsNode`) when the rvalue matches; nil otherwise. Used by `walk_methods` / `walk_def_nodes` to
|
|
1549
1333
|
# push `Const` onto the qualified prefix before recursing.
|
|
1550
1334
|
def meta_new_block_body(node)
|
|
1551
1335
|
return nil unless node.is_a?(Prism::ConstantWriteNode)
|
|
@@ -1559,17 +1343,12 @@ module Rigor
|
|
|
1559
1343
|
rvalue.block&.body
|
|
1560
1344
|
end
|
|
1561
1345
|
|
|
1562
|
-
# `class Foo < Data.define(:a, :b)` / `class Bar < Struct.new(:x)`
|
|
1563
|
-
#
|
|
1564
|
-
#
|
|
1565
|
-
#
|
|
1566
|
-
#
|
|
1567
|
-
#
|
|
1568
|
-
# recorder, which must treat a synthesized member as an existing
|
|
1569
|
-
# method, not an unresolved call. The block-form
|
|
1570
|
-
# (`Const = Data.define(:a) do ... end`) is handled by the
|
|
1571
|
-
# `ConstantWriteNode` branch's block recursion; its members type
|
|
1572
|
-
# `self` as `Object`, out of scope here.
|
|
1346
|
+
# `class Foo < Data.define(:a, :b)` / `class Bar < Struct.new(:x)` synthesizes reader methods (`a`, `b`, `x`) on
|
|
1347
|
+
# the subclass that no `def` / `attr_*` declares. Register them in the discovered-methods existence table so an
|
|
1348
|
+
# implicit-self read of a member inside the class body is known to exist — both for the existing undefined-method
|
|
1349
|
+
# suppression and for the ADR-24 slice-4 self-call recorder, which must treat a synthesized member as an existing
|
|
1350
|
+
# method, not an unresolved call. The block-form (`Const = Data.define(:a) do ... end`) is handled by the
|
|
1351
|
+
# `ConstantWriteNode` branch's block recursion; its members type `self` as `Object`, out of scope here.
|
|
1573
1352
|
def record_meta_superclass_members(class_node, qualified_prefix, accumulator)
|
|
1574
1353
|
superclass = class_node.superclass
|
|
1575
1354
|
return unless data_define_call?(superclass) || struct_new_call?(superclass)
|
|
@@ -1582,11 +1361,9 @@ module Rigor
|
|
|
1582
1361
|
members.each { |member| table[member] ||= :instance }
|
|
1583
1362
|
end
|
|
1584
1363
|
|
|
1585
|
-
# The Symbol member names of a `Data.define(*Symbol)` /
|
|
1586
|
-
# `Struct.new
|
|
1587
|
-
#
|
|
1588
|
-
# stripped by {#struct_new_positionals}; `Data.define` args are all
|
|
1589
|
-
# Symbols already.
|
|
1364
|
+
# The Symbol member names of a `Data.define(*Symbol)` / `Struct.new(*Symbol [, keyword_init:])` call. For
|
|
1365
|
+
# `Struct.new` the optional leading String name and trailing `keyword_init:` hash are stripped by
|
|
1366
|
+
# {#struct_new_positionals}; `Data.define` args are all Symbols already.
|
|
1590
1367
|
def meta_member_names(call_node)
|
|
1591
1368
|
raw = call_node.arguments&.arguments || []
|
|
1592
1369
|
symbols = struct_new_call?(call_node) ? (struct_new_positionals(raw) || []) : raw
|
|
@@ -1603,16 +1380,12 @@ module Rigor
|
|
|
1603
1380
|
accumulator[class_name][def_node.name] = kind
|
|
1604
1381
|
end
|
|
1605
1382
|
|
|
1606
|
-
# `def Foo.bar` inside `module Foo` (or `def Meta.init` inside
|
|
1607
|
-
#
|
|
1608
|
-
#
|
|
1609
|
-
# the module itself (== `self`). Recognise the form so the
|
|
1610
|
-
# method registers as singleton on the enclosing class.
|
|
1383
|
+
# `def Foo.bar` inside `module Foo` (or `def Meta.init` inside `module Meta`) is semantically equivalent to `def
|
|
1384
|
+
# self.bar`: at the def-site, the runtime value of the constant `Foo` is the module itself (== `self`). Recognise
|
|
1385
|
+
# the form so the method registers as singleton on the enclosing class.
|
|
1611
1386
|
#
|
|
1612
|
-
# The cross-class form `def Bar.baz` inside `module Foo` —
|
|
1613
|
-
#
|
|
1614
|
-
# enclosing class — is not supported at this slice; falls
|
|
1615
|
-
# through to `:instance` (current behaviour) rather than
|
|
1387
|
+
# The cross-class form `def Bar.baz` inside `module Foo` — where the receiver names a constant other than the
|
|
1388
|
+
# enclosing class — is not supported at this slice; falls through to `:instance` (current behaviour) rather than
|
|
1616
1389
|
# silently re-routing the registration.
|
|
1617
1390
|
def def_singleton?(def_node, qualified_prefix, in_singleton_class)
|
|
1618
1391
|
return true if def_node.receiver.is_a?(Prism::SelfNode) || in_singleton_class
|
|
@@ -1620,10 +1393,9 @@ module Rigor
|
|
|
1620
1393
|
def_receiver_targets_lexical_self?(def_node.receiver, qualified_prefix)
|
|
1621
1394
|
end
|
|
1622
1395
|
|
|
1623
|
-
# Only `Prism::ConstantReadNode` is observed in real Ruby —
|
|
1624
|
-
#
|
|
1625
|
-
#
|
|
1626
|
-
# branch stays defensive in case Prism's grammar widens.
|
|
1396
|
+
# Only `Prism::ConstantReadNode` is observed in real Ruby — Prism mis-parses `def C::P.method` as `def C.P` (Ruby
|
|
1397
|
+
# itself rejects the form as a SyntaxError). The ConstantPathNode branch stays defensive in case Prism's grammar
|
|
1398
|
+
# widens.
|
|
1627
1399
|
def def_receiver_targets_lexical_self?(receiver, qualified_prefix)
|
|
1628
1400
|
return false if qualified_prefix.empty?
|
|
1629
1401
|
|
|
@@ -1641,13 +1413,10 @@ module Rigor
|
|
|
1641
1413
|
end
|
|
1642
1414
|
end
|
|
1643
1415
|
|
|
1644
|
-
# v0.0.3 A — sentinel key under which `record_def_node`
|
|
1645
|
-
#
|
|
1646
|
-
#
|
|
1647
|
-
#
|
|
1648
|
-
# Looked up by `Scope#top_level_def_for` to give
|
|
1649
|
-
# implicit-self calls priority over RBS dispatch when
|
|
1650
|
-
# the file defines a same-named local method.
|
|
1416
|
+
# v0.0.3 A — sentinel key under which `record_def_node` files DefNodes that live outside any class / module body
|
|
1417
|
+
# (top-level helpers, `def`s nested inside DSL blocks like `RSpec.describe ... do; def helper; end`). Looked up by
|
|
1418
|
+
# `Scope#top_level_def_for` to give implicit-self calls priority over RBS dispatch when the file defines a
|
|
1419
|
+
# same-named local method.
|
|
1651
1420
|
TOP_LEVEL_DEF_KEY = "<toplevel>"
|
|
1652
1421
|
|
|
1653
1422
|
def record_def_node(def_node, qualified_prefix, in_singleton_class, accumulator)
|
|
@@ -1658,18 +1427,13 @@ module Rigor
|
|
|
1658
1427
|
accumulator[class_name][def_node.name] = def_node
|
|
1659
1428
|
end
|
|
1660
1429
|
|
|
1661
|
-
# Module-singleton call resolution (ADR-57 follow-up) — the
|
|
1662
|
-
#
|
|
1663
|
-
# `
|
|
1664
|
-
#
|
|
1665
|
-
#
|
|
1666
|
-
# `
|
|
1667
|
-
#
|
|
1668
|
-
# singleton-free on purpose (its ancestor walk binds `self` as
|
|
1669
|
-
# `Nominal`), so the two never overlap except for `module_function`
|
|
1670
|
-
# defs, which are genuinely callable on both sides and so appear in
|
|
1671
|
-
# both tables. Top-level singleton defs (`def self.x` outside any
|
|
1672
|
-
# class — `self` is `main`) are not recorded; they have no constant
|
|
1430
|
+
# Module-singleton call resolution (ADR-57 follow-up) — the SINGLETON-side mirror of `build_discovered_def_nodes`.
|
|
1431
|
+
# Records the `Prism::DefNode` for every singleton-side method (`def self.x`, `def Foo.x`, a `class << self` body,
|
|
1432
|
+
# and a `module_function` method) keyed by qualified class/module name → method → node, so `ExpressionTyper` can
|
|
1433
|
+
# re-type the body when a `Singleton[Foo]` receiver dispatches `Foo.x`. The instance-side table is kept
|
|
1434
|
+
# singleton-free on purpose (its ancestor walk binds `self` as `Nominal`), so the two never overlap except for
|
|
1435
|
+
# `module_function` defs, which are genuinely callable on both sides and so appear in both tables. Top-level
|
|
1436
|
+
# singleton defs (`def self.x` outside any class — `self` is `main`) are not recorded; they have no constant
|
|
1673
1437
|
# receiver to dispatch through.
|
|
1674
1438
|
def build_discovered_singleton_def_nodes(root)
|
|
1675
1439
|
accumulator = {}
|
|
@@ -1677,12 +1441,10 @@ module Rigor
|
|
|
1677
1441
|
accumulator.transform_values(&:freeze).freeze
|
|
1678
1442
|
end
|
|
1679
1443
|
|
|
1680
|
-
# Walks every node, entering class/module/singleton-class bodies via
|
|
1681
|
-
#
|
|
1682
|
-
#
|
|
1683
|
-
#
|
|
1684
|
-
# node there is no `module_function` state to carry, so descent is a
|
|
1685
|
-
# plain per-child walk.
|
|
1444
|
+
# Walks every node, entering class/module/singleton-class bodies via {#walk_singleton_body} so a bare
|
|
1445
|
+
# `module_function` toggle threads correctly across the body's *sibling* statements (a child-by-child recursion
|
|
1446
|
+
# would reset it). At the top level / inside an arbitrary node there is no `module_function` state to carry, so
|
|
1447
|
+
# descent is a plain per-child walk.
|
|
1686
1448
|
def walk_singleton_def_nodes(node, qualified_prefix, in_singleton_class, accumulator)
|
|
1687
1449
|
return unless node.is_a?(Prism::Node)
|
|
1688
1450
|
|
|
@@ -1717,12 +1479,10 @@ module Rigor
|
|
|
1717
1479
|
end
|
|
1718
1480
|
end
|
|
1719
1481
|
|
|
1720
|
-
# Walks a class/module/singleton-class body's direct statements in
|
|
1721
|
-
#
|
|
1722
|
-
#
|
|
1723
|
-
#
|
|
1724
|
-
# `module_function :a, :b` named forms recurse / record through the
|
|
1725
|
-
# general walker so the toggle stays scoped to its own body.
|
|
1482
|
+
# Walks a class/module/singleton-class body's direct statements in source order, threading the
|
|
1483
|
+
# bare-`module_function` toggle: once a bare `module_function` is seen, every subsequent `def` in the body
|
|
1484
|
+
# registers as a singleton method. Nested classes/modules/defs and `module_function :a, :b` named forms recurse /
|
|
1485
|
+
# record through the general walker so the toggle stays scoped to its own body.
|
|
1726
1486
|
def walk_singleton_body(body, qualified_prefix, in_singleton_class, accumulator)
|
|
1727
1487
|
module_function_on = false
|
|
1728
1488
|
statements_of(body).each do |stmt|
|
|
@@ -1742,9 +1502,8 @@ module Rigor
|
|
|
1742
1502
|
end
|
|
1743
1503
|
end
|
|
1744
1504
|
|
|
1745
|
-
# Direct statement children of a class/module body node (a
|
|
1746
|
-
#
|
|
1747
|
-
# lone statement). Returns an empty list for an empty body.
|
|
1505
|
+
# Direct statement children of a class/module body node (a `Prism::StatementsNode`, a `Prism::BeginNode` wrapping
|
|
1506
|
+
# one, or a lone statement). Returns an empty list for an empty body.
|
|
1748
1507
|
def statements_of(body)
|
|
1749
1508
|
case body
|
|
1750
1509
|
when Prism::StatementsNode then body.body
|
|
@@ -1763,8 +1522,8 @@ module Rigor
|
|
|
1763
1522
|
(accumulator[class_name] ||= {})[def_node.name] = def_node
|
|
1764
1523
|
end
|
|
1765
1524
|
|
|
1766
|
-
# A bare `module_function` (no arguments) flips every following `def`
|
|
1767
|
-
#
|
|
1525
|
+
# A bare `module_function` (no arguments) flips every following `def` in the module body to module-function
|
|
1526
|
+
# (instance + singleton) mode.
|
|
1768
1527
|
def module_function_toggle?(node)
|
|
1769
1528
|
node.name == :module_function && node.receiver.nil?
|
|
1770
1529
|
end
|
|
@@ -1773,12 +1532,10 @@ module Rigor
|
|
|
1773
1532
|
node.arguments.nil? || node.arguments.arguments.empty?
|
|
1774
1533
|
end
|
|
1775
1534
|
|
|
1776
|
-
# `module_function :a, :b` retro-marks named siblings (defined
|
|
1777
|
-
#
|
|
1778
|
-
#
|
|
1779
|
-
#
|
|
1780
|
-
# Non-symbol arguments and names with no matching `def` are skipped
|
|
1781
|
-
# (a miss degrades to today's `Dynamic`, never a false resolution).
|
|
1535
|
+
# `module_function :a, :b` retro-marks named siblings (defined earlier OR later in the same body) as
|
|
1536
|
+
# module-functions. Resolves each symbol-literal argument against the body's own `def`s and registers the matching
|
|
1537
|
+
# `DefNode` on the module's singleton side. Non-symbol arguments and names with no matching `def` are skipped (a
|
|
1538
|
+
# miss degrades to today's `Dynamic`, never a false resolution).
|
|
1782
1539
|
def record_module_function_names(node, qualified_prefix, body, accumulator)
|
|
1783
1540
|
return if qualified_prefix.empty?
|
|
1784
1541
|
|
|
@@ -1798,14 +1555,10 @@ module Rigor
|
|
|
1798
1555
|
arg.unescaped.to_sym if arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::StringNode)
|
|
1799
1556
|
end
|
|
1800
1557
|
|
|
1801
|
-
# ADR-24 slice 2 — per-class table mapping a fully
|
|
1802
|
-
#
|
|
1803
|
-
#
|
|
1804
|
-
#
|
|
1805
|
-
# and other non-constant superclasses produce no entry).
|
|
1806
|
-
# The as-written name is resolved to a qualified class at
|
|
1807
|
-
# the call site against the subclass's lexical nesting —
|
|
1808
|
-
# see `ExpressionTyper#resolve_ancestor_class_name`.
|
|
1558
|
+
# ADR-24 slice 2 — per-class table mapping a fully qualified user class to its superclass name AS WRITTEN at the
|
|
1559
|
+
# `class Foo < Bar` declaration. Only constant superclasses are recorded (`class Foo < Struct.new(...)` and other
|
|
1560
|
+
# non-constant superclasses produce no entry). The as-written name is resolved to a qualified class at the call
|
|
1561
|
+
# site against the subclass's lexical nesting — see `ExpressionTyper#resolve_ancestor_class_name`.
|
|
1809
1562
|
def build_discovered_superclasses(root)
|
|
1810
1563
|
accumulator = {}
|
|
1811
1564
|
walk_class_superclasses(root, [], accumulator)
|
|
@@ -1838,15 +1591,11 @@ module Rigor
|
|
|
1838
1591
|
end
|
|
1839
1592
|
end
|
|
1840
1593
|
|
|
1841
|
-
# ADR-48 — per qualified class name -> ordered `Data.define`
|
|
1842
|
-
#
|
|
1843
|
-
#
|
|
1844
|
-
#
|
|
1845
|
-
#
|
|
1846
|
-
# folding would be unsound (the Struct follow-up is deferred — see
|
|
1847
|
-
# ADR-48 § "Struct follow-up"). Consumed by
|
|
1848
|
-
# {Inference::MethodDispatcher::DataFolding} via
|
|
1849
|
-
# {Scope#data_member_layout}.
|
|
1594
|
+
# ADR-48 — per qualified class name -> ordered `Data.define` member-name list, for both the named-subclass form
|
|
1595
|
+
# (`class Point < Data.define(:x, :y)`) and the constant-assigned form (`Point = Data.define(:x, :y)`). Only
|
|
1596
|
+
# `Data.define` is recorded: `Struct.new` instances are mutable, so member-value folding would be unsound (the
|
|
1597
|
+
# Struct follow-up is deferred — see ADR-48 § "Struct follow-up"). Consumed by
|
|
1598
|
+
# {Inference::MethodDispatcher::DataFolding} via {Scope#data_member_layout}.
|
|
1850
1599
|
def build_data_member_layouts(root)
|
|
1851
1600
|
accumulator = {}
|
|
1852
1601
|
walk_data_member_layouts(root, [], accumulator)
|
|
@@ -1879,8 +1628,8 @@ module Rigor
|
|
|
1879
1628
|
end
|
|
1880
1629
|
end
|
|
1881
1630
|
|
|
1882
|
-
# Records `qualified -> [members]` when `expr` is a
|
|
1883
|
-
#
|
|
1631
|
+
# Records `qualified -> [members]` when `expr` is a `Data.define(*Symbol)` call with at least one literal-Symbol
|
|
1632
|
+
# member.
|
|
1884
1633
|
def record_data_member_layout(accumulator, qualified_parts, expr)
|
|
1885
1634
|
return unless data_define_call?(expr)
|
|
1886
1635
|
|
|
@@ -1890,12 +1639,10 @@ module Rigor
|
|
|
1890
1639
|
accumulator[qualified_parts.join("::")] = members.freeze
|
|
1891
1640
|
end
|
|
1892
1641
|
|
|
1893
|
-
# ADR-48 Struct follow-up — the `Struct.new(...)` sibling of
|
|
1894
|
-
#
|
|
1895
|
-
#
|
|
1896
|
-
#
|
|
1897
|
-
# because the dispatcher needs the flag to fold the matching `.new`
|
|
1898
|
-
# call form (positional vs keyword) without manufacturing a wrong map.
|
|
1642
|
+
# ADR-48 Struct follow-up — the `Struct.new(...)` sibling of {#build_data_member_layouts}. A separate, additive
|
|
1643
|
+
# table so the existing `Data.define` value-shape contract (a bare `[Symbol]`) is untouched: a Struct entry
|
|
1644
|
+
# carries `{ members:, keyword_init: }` because the dispatcher needs the flag to fold the matching `.new` call
|
|
1645
|
+
# form (positional vs keyword) without manufacturing a wrong map.
|
|
1899
1646
|
def build_struct_member_layouts(root)
|
|
1900
1647
|
accumulator = {}
|
|
1901
1648
|
walk_struct_member_layouts(root, [], accumulator)
|
|
@@ -1928,9 +1675,8 @@ module Rigor
|
|
|
1928
1675
|
end
|
|
1929
1676
|
end
|
|
1930
1677
|
|
|
1931
|
-
# Records `qualified -> { members:, keyword_init: }` when `expr` is a
|
|
1932
|
-
#
|
|
1933
|
-
# literal-Symbol member.
|
|
1678
|
+
# Records `qualified -> { members:, keyword_init: }` when `expr` is a `Struct.new(*Symbol [, keyword_init:
|
|
1679
|
+
# <bool>])` call with at least one literal-Symbol member.
|
|
1934
1680
|
def record_struct_member_layout(accumulator, qualified_parts, expr)
|
|
1935
1681
|
return unless struct_new_call?(expr)
|
|
1936
1682
|
|
|
@@ -1943,9 +1689,8 @@ module Rigor
|
|
|
1943
1689
|
}.freeze
|
|
1944
1690
|
end
|
|
1945
1691
|
|
|
1946
|
-
# True when a `Struct.new` call carries `keyword_init: true` as a
|
|
1947
|
-
# literal
|
|
1948
|
-
# absence) reads as `false` — the conservative positional default.
|
|
1692
|
+
# True when a `Struct.new` call carries `keyword_init: true` as a literal in its trailing keyword hash. A
|
|
1693
|
+
# non-literal value (or its absence) reads as `false` — the conservative positional default.
|
|
1949
1694
|
def struct_new_keyword_init?(call_node)
|
|
1950
1695
|
args = call_node.arguments&.arguments || []
|
|
1951
1696
|
last = args.last
|
|
@@ -1960,16 +1705,11 @@ module Rigor
|
|
|
1960
1705
|
|
|
1961
1706
|
MIXIN_CALL_NAMES = %i[include prepend].freeze
|
|
1962
1707
|
|
|
1963
|
-
# ADR-24 slice 2 — per-class/module table mapping a fully
|
|
1964
|
-
#
|
|
1965
|
-
#
|
|
1966
|
-
#
|
|
1967
|
-
#
|
|
1968
|
-
# (`include some_method`) produce no entry. `prepend` is
|
|
1969
|
-
# bucketed with `include` — both contribute instance
|
|
1970
|
-
# methods to the ancestor chain. `extend` is NOT tracked
|
|
1971
|
-
# (it adds singleton methods; ADR-24 slice 2 resolves the
|
|
1972
|
-
# instance-side chain).
|
|
1708
|
+
# ADR-24 slice 2 — per-class/module table mapping a fully qualified user class or module to the list of module
|
|
1709
|
+
# names it `include`s / `prepend`s, AS WRITTEN at the mixin call (`include Foo` / `include Foo::Bar`). Only
|
|
1710
|
+
# constant arguments are recorded; dynamic mixins (`include some_method`) produce no entry. `prepend` is bucketed
|
|
1711
|
+
# with `include` — both contribute instance methods to the ancestor chain. `extend` is NOT tracked (it adds
|
|
1712
|
+
# singleton methods; ADR-24 slice 2 resolves the instance-side chain).
|
|
1973
1713
|
def build_discovered_includes(root)
|
|
1974
1714
|
accumulator = {}
|
|
1975
1715
|
walk_class_includes(root, [], nil, accumulator)
|
|
@@ -2008,8 +1748,7 @@ module Rigor
|
|
|
2008
1748
|
|
|
2009
1749
|
VISIBILITY_MODIFIERS = %i[public private protected].freeze
|
|
2010
1750
|
|
|
2011
|
-
# v0.1.2 — per-class method-visibility table for the
|
|
2012
|
-
# `def.method-visibility-mismatch` CheckRule.
|
|
1751
|
+
# v0.1.2 — per-class method-visibility table for the `def.method-visibility-mismatch` CheckRule.
|
|
2013
1752
|
#
|
|
2014
1753
|
# Tracks two visibility-changing forms:
|
|
2015
1754
|
#
|
|
@@ -2025,10 +1764,8 @@ module Rigor
|
|
|
2025
1764
|
# tracking the def-call's return-value visibility,
|
|
2026
1765
|
# which is a separate slice.
|
|
2027
1766
|
#
|
|
2028
|
-
# Top-level (no surrounding class) defs do not contribute
|
|
2029
|
-
#
|
|
2030
|
-
# top-level marks the method on `Object`) are out of
|
|
2031
|
-
# scope for v0.1.2.
|
|
1767
|
+
# Top-level (no surrounding class) defs do not contribute — Ruby's top-level visibility nuances (private at
|
|
1768
|
+
# top-level marks the method on `Object`) are out of scope for v0.1.2.
|
|
2032
1769
|
def build_discovered_method_visibilities(root)
|
|
2033
1770
|
accumulator = {}
|
|
2034
1771
|
walk_method_visibilities(root, [], false, :public, accumulator)
|
|
@@ -2069,9 +1806,8 @@ module Rigor
|
|
|
2069
1806
|
return updated unless updated.equal?(current_visibility)
|
|
2070
1807
|
end
|
|
2071
1808
|
|
|
2072
|
-
# Statement-position StatementsNode preserves
|
|
2073
|
-
#
|
|
2074
|
-
# recurses with the entry visibility unchanged.
|
|
1809
|
+
# Statement-position StatementsNode preserves left-to-right visibility flow; everything else recurses with the
|
|
1810
|
+
# entry visibility unchanged.
|
|
2075
1811
|
if node.is_a?(Prism::StatementsNode)
|
|
2076
1812
|
local_visibility = current_visibility
|
|
2077
1813
|
node.compact_child_nodes.each do |child|
|
|
@@ -2096,8 +1832,7 @@ module Rigor
|
|
|
2096
1832
|
accumulator[class_name][def_node.name] = current_visibility
|
|
2097
1833
|
end
|
|
2098
1834
|
|
|
2099
|
-
# Recognises modifier calls on the implicit-self receiver
|
|
2100
|
-
# inside a class body. Returns the (possibly updated)
|
|
1835
|
+
# Recognises modifier calls on the implicit-self receiver inside a class body. Returns the (possibly updated)
|
|
2101
1836
|
# current visibility:
|
|
2102
1837
|
#
|
|
2103
1838
|
# - `private` / `public` / `protected` (no args) —
|
|
@@ -2137,11 +1872,9 @@ module Rigor
|
|
|
2137
1872
|
nil
|
|
2138
1873
|
end
|
|
2139
1874
|
|
|
2140
|
-
# Registers the alias name in the `discovered_methods` table so
|
|
2141
|
-
#
|
|
2142
|
-
#
|
|
2143
|
-
# (instance inside a regular class body, singleton inside
|
|
2144
|
-
# `class << self`).
|
|
1875
|
+
# Registers the alias name in the `discovered_methods` table so `undefined-method` diagnostics are not emitted for
|
|
1876
|
+
# calls to the aliased name. The kind mirrors the surrounding class context (instance inside a regular class body,
|
|
1877
|
+
# singleton inside `class << self`).
|
|
2145
1878
|
def record_alias_method(alias_node, qualified_prefix, in_singleton_class, accumulator)
|
|
2146
1879
|
return if qualified_prefix.empty?
|
|
2147
1880
|
return unless alias_node.new_name.is_a?(Prism::SymbolNode)
|
|
@@ -2152,11 +1885,9 @@ module Rigor
|
|
|
2152
1885
|
(accumulator[class_name] ||= {})[new_name] = kind
|
|
2153
1886
|
end
|
|
2154
1887
|
|
|
2155
|
-
# Post-pass over the `def_nodes` accumulator: for every `alias`
|
|
2156
|
-
#
|
|
2157
|
-
#
|
|
2158
|
-
# the same node so inter-procedural return-type inference works
|
|
2159
|
-
# for the aliased name.
|
|
1888
|
+
# Post-pass over the `def_nodes` accumulator: for every `alias` declaration inside a class body, if the original
|
|
1889
|
+
# method name maps to a `Prism::DefNode`, register the new name pointing to the same node so inter-procedural
|
|
1890
|
+
# return-type inference works for the aliased name.
|
|
2160
1891
|
def apply_alias_def_nodes(root, accumulator)
|
|
2161
1892
|
alias_map = collect_class_alias_map(root, [], {})
|
|
2162
1893
|
alias_map.each do |class_name, aliases|
|
|
@@ -2172,8 +1903,8 @@ module Rigor
|
|
|
2172
1903
|
end
|
|
2173
1904
|
end
|
|
2174
1905
|
|
|
2175
|
-
# Builds a map `{class_name => {new_name_sym => old_name_sym}}` by
|
|
2176
|
-
#
|
|
1906
|
+
# Builds a map `{class_name => {new_name_sym => old_name_sym}}` by walking the tree for `AliasMethodNode` nodes
|
|
1907
|
+
# inside class bodies.
|
|
2177
1908
|
def collect_class_alias_map(node, qualified_prefix, accumulator)
|
|
2178
1909
|
return accumulator unless node.is_a?(Prism::Node)
|
|
2179
1910
|
|
|
@@ -2218,14 +1949,11 @@ module Rigor
|
|
|
2218
1949
|
accumulator[class_name][method_name] = in_singleton_class ? :singleton : :instance
|
|
2219
1950
|
end
|
|
2220
1951
|
|
|
2221
|
-
# The `attr_*` accessor macros that introduce methods Rigor must
|
|
2222
|
-
#
|
|
2223
|
-
#
|
|
2224
|
-
#
|
|
2225
|
-
#
|
|
2226
|
-
# undefined-method rule only suppressed `def` / `define_method` /
|
|
2227
|
-
# `alias_method`-discovered methods. `attr_reader` defines
|
|
2228
|
-
# readers, `attr_writer` writers (`x=`), `attr_accessor` both.
|
|
1952
|
+
# The `attr_*` accessor macros that introduce methods Rigor must treat as source-declared. Without this, a class
|
|
1953
|
+
# that defines an accessor with `attr_reader :x` AND carries RBS that omits `x` (a common gap — the project ships
|
|
1954
|
+
# an incomplete `sig/`) fires a false `call.undefined-method` on `obj.x`, because the undefined-method rule only
|
|
1955
|
+
# suppressed `def` / `define_method` / `alias_method`-discovered methods. `attr_reader` defines readers,
|
|
1956
|
+
# `attr_writer` writers (`x=`), `attr_accessor` both.
|
|
2229
1957
|
ATTR_MACROS = %i[attr_reader attr_writer attr_accessor].freeze
|
|
2230
1958
|
|
|
2231
1959
|
def record_attr_methods(call_node, qualified_prefix, in_singleton_class, accumulator)
|
|
@@ -2253,36 +1981,23 @@ module Rigor
|
|
|
2253
1981
|
node.unescaped&.to_sym
|
|
2254
1982
|
end
|
|
2255
1983
|
|
|
2256
|
-
# Walks every file in `paths` (each path is parsed once with
|
|
2257
|
-
# `
|
|
2258
|
-
# `discovered_classes`
|
|
2259
|
-
#
|
|
2260
|
-
#
|
|
2261
|
-
#
|
|
2262
|
-
#
|
|
2263
|
-
# last-write-wins (matches the existing in-file merge
|
|
2264
|
-
# semantics). Parse failures fail-soft to an empty
|
|
2265
|
-
# contribution. The `buffer` argument, when present,
|
|
2266
|
-
# redirects reads for the bound logical path to the
|
|
2267
|
-
# buffer's physical path so editor-mode pre-passes see
|
|
2268
|
-
# the in-flight bytes.
|
|
1984
|
+
# Walks every file in `paths` (each path is parsed once with `Prism.parse_file`) and returns the unioned
|
|
1985
|
+
# project-wide `discovered_classes` Hash: `{qualified_name => Singleton[…]}`. Used by `Analysis::Runner` to seed
|
|
1986
|
+
# each file's `default_scope.discovered_classes` so that lexical constant lookup in one file resolves a `class
|
|
1987
|
+
# Foo` declared in a sibling file. Per-file collisions are last-write-wins (matches the existing in-file merge
|
|
1988
|
+
# semantics). Parse failures fail-soft to an empty contribution. The `buffer` argument, when present, redirects
|
|
1989
|
+
# reads for the bound logical path to the buffer's physical path so editor-mode pre-passes see the in-flight
|
|
1990
|
+
# bytes.
|
|
2269
1991
|
#
|
|
2270
|
-
# **Modules are intentionally excluded** from the
|
|
2271
|
-
#
|
|
2272
|
-
#
|
|
2273
|
-
#
|
|
2274
|
-
#
|
|
2275
|
-
# `
|
|
2276
|
-
#
|
|
2277
|
-
# Until cross-file `discovered_methods` follows the same
|
|
2278
|
-
# project-wide seed, registering modules here would
|
|
2279
|
-
# introduce regressions in modules-with-module_function
|
|
2280
|
-
# idioms that previously resolved to `Dynamic[Top]`.
|
|
2281
|
-
# Class declarations are safe because per-file
|
|
2282
|
-
# `discovered_methods` already tracks `def self.x` /
|
|
2283
|
-
# `def x` instance and singleton methods consistently.
|
|
1992
|
+
# **Modules are intentionally excluded** from the project-wide seed: a `module M; module_function; def x; end;
|
|
1993
|
+
# end` body, when surfaced as `singleton(M)` to the dispatcher, falls through to `Kernel#x` (or any Module
|
|
1994
|
+
# ancestor method) when the project's per-file `discovered_methods` doesn't know `M.x` — leading to surprising
|
|
1995
|
+
# types like `Kernel.select → Array[String]`. Until cross-file `discovered_methods` follows the same project-wide
|
|
1996
|
+
# seed, registering modules here would introduce regressions in modules-with-module_function idioms that
|
|
1997
|
+
# previously resolved to `Dynamic[Top]`. Class declarations are safe because per-file `discovered_methods` already
|
|
1998
|
+
# tracks `def self.x` / `def x` instance and singleton methods consistently.
|
|
2284
1999
|
#
|
|
2285
|
-
# @param paths
|
|
2000
|
+
# @param paths [Array<String>] project file paths.
|
|
2286
2001
|
# @param buffer [Rigor::Analysis::BufferBinding, nil]
|
|
2287
2002
|
# @return [Hash{String => Rigor::Type::Singleton}]
|
|
2288
2003
|
def discovered_classes_for_paths(paths, buffer: nil)
|
|
@@ -2293,35 +2008,26 @@ module Rigor
|
|
|
2293
2008
|
root = Prism.parse(source, filepath: path).value
|
|
2294
2009
|
collect_class_decls(root, [], accumulator)
|
|
2295
2010
|
rescue StandardError
|
|
2296
|
-
# Skip files that fail to parse or read; the per-file
|
|
2297
|
-
# analyzer surfaces the parse error separately.
|
|
2011
|
+
# Skip files that fail to parse or read; the per-file analyzer surfaces the parse error separately.
|
|
2298
2012
|
next
|
|
2299
2013
|
end
|
|
2300
2014
|
accumulator.freeze
|
|
2301
2015
|
end
|
|
2302
2016
|
|
|
2303
|
-
# ADR-24 slice 2 — cross-file companion to
|
|
2304
|
-
# `
|
|
2305
|
-
#
|
|
2306
|
-
# `
|
|
2307
|
-
#
|
|
2308
|
-
# class -> superclass-name map. The engine consults these
|
|
2309
|
-
# so an implicit-self call inside a subclass resolves
|
|
2310
|
-
# against a superclass `def` declared in a sibling file
|
|
2311
|
-
# (`Mastodon::CLI::Accounts` calling a helper defined in
|
|
2312
|
-
# `Mastodon::CLI::Base`).
|
|
2017
|
+
# ADR-24 slice 2 — cross-file companion to `discovered_classes_for_paths`. Walks every project file once and
|
|
2018
|
+
# returns both the merged `discovered_def_nodes` table (a class reopened across files has its method tables
|
|
2019
|
+
# merged) and the merged class -> superclass-name map. The engine consults these so an implicit-self call inside a
|
|
2020
|
+
# subclass resolves against a superclass `def` declared in a sibling file (`Mastodon::CLI::Accounts` calling a
|
|
2021
|
+
# helper defined in `Mastodon::CLI::Base`).
|
|
2313
2022
|
#
|
|
2314
|
-
# The returned `def_sources` map mirrors `def_nodes` but stores
|
|
2315
|
-
#
|
|
2316
|
-
#
|
|
2317
|
-
#
|
|
2318
|
-
#
|
|
2319
|
-
#
|
|
2320
|
-
# copy to name the defining file when a project monkey-patch on
|
|
2321
|
-
# a core/stdlib/gem class is called cross-file (ADR-17). First
|
|
2322
|
-
# write wins, matching `def_nodes`' own merge order.
|
|
2023
|
+
# The returned `def_sources` map mirrors `def_nodes` but stores a `"path:line"` String per `(class_name,
|
|
2024
|
+
# method_name)` instead of the `Prism::DefNode`. A `Prism::Location` does not expose its source file through
|
|
2025
|
+
# public API, so the source site is captured here, in the pre-pass loop that still holds `path`.
|
|
2026
|
+
# `CheckRules#undefined_method_diagnostic` consults the seeded copy to name the defining file when a project
|
|
2027
|
+
# monkey-patch on a core/stdlib/gem class is called cross-file (ADR-17). First write wins, matching `def_nodes`'
|
|
2028
|
+
# own merge order.
|
|
2323
2029
|
#
|
|
2324
|
-
# @param paths
|
|
2030
|
+
# @param paths [Array<String>] project file paths.
|
|
2325
2031
|
# @param buffer [Rigor::Analysis::BufferBinding, nil]
|
|
2326
2032
|
# @return [Hash{Symbol => Hash}]
|
|
2327
2033
|
# `{ def_nodes:, def_sources:, superclasses:, includes:, class_sources: }`
|
|
@@ -2334,18 +2040,14 @@ module Rigor
|
|
|
2334
2040
|
root = Prism.parse(File.read(physical), filepath: path).value
|
|
2335
2041
|
accumulate_project_index(acc, path, root)
|
|
2336
2042
|
rescue StandardError
|
|
2337
|
-
# Skip files that fail to parse or read; the per-file
|
|
2338
|
-
# analyzer surfaces the parse error separately.
|
|
2043
|
+
# Skip files that fail to parse or read; the per-file analyzer surfaces the parse error separately.
|
|
2339
2044
|
next
|
|
2340
2045
|
end
|
|
2341
|
-
# Cross-file method suppression is for the project's OWN
|
|
2342
|
-
#
|
|
2343
|
-
#
|
|
2344
|
-
#
|
|
2345
|
-
#
|
|
2346
|
-
# so dropping the `def`-declared names keeps that contract
|
|
2347
|
-
# intact while still letting `attr_reader :x` in one file
|
|
2348
|
-
# suppress a false undefined-method for `obj.x` in another.
|
|
2046
|
+
# Cross-file method suppression is for the project's OWN accessors (attr_* / define_method / alias) — NOT for
|
|
2047
|
+
# plain `def`s. A cross-file `def` on a class is exactly the ADR-17 monkey-patch case the undefined-method rule
|
|
2048
|
+
# deliberately surfaces (fire + def-site annotation, nudging `pre_eval:`), so dropping the `def`-declared names
|
|
2049
|
+
# keeps that contract intact while still letting `attr_reader :x` in one file suppress a false undefined-method
|
|
2050
|
+
# for `obj.x` in another.
|
|
2349
2051
|
acc[:methods] = subtract_def_methods(acc[:methods], acc[:def_nodes])
|
|
2350
2052
|
%i[def_nodes singleton_def_nodes def_sources includes method_visibilities methods class_sources].each do |key|
|
|
2351
2053
|
acc[key].each_value(&:freeze)
|
|
@@ -2353,9 +2055,8 @@ module Rigor
|
|
|
2353
2055
|
acc.transform_values(&:freeze)
|
|
2354
2056
|
end
|
|
2355
2057
|
|
|
2356
|
-
# Removes, per class, the method names that have a project `def`
|
|
2357
|
-
#
|
|
2358
|
-
# methods in the cross-file suppression table.
|
|
2058
|
+
# Removes, per class, the method names that have a project `def` node, leaving only
|
|
2059
|
+
# accessor/alias/define_method-introduced methods in the cross-file suppression table.
|
|
2359
2060
|
def subtract_def_methods(methods, def_nodes)
|
|
2360
2061
|
methods.each_with_object({}) do |(class_name, table), out|
|
|
2361
2062
|
defs = def_nodes[class_name] || {}
|
|
@@ -2364,16 +2065,13 @@ module Rigor
|
|
|
2364
2065
|
end
|
|
2365
2066
|
end
|
|
2366
2067
|
|
|
2367
|
-
# Folds one file's class-keyed indexes into the cross-file
|
|
2368
|
-
#
|
|
2369
|
-
#
|
|
2370
|
-
# visibility declared in a sibling file.
|
|
2068
|
+
# Folds one file's class-keyed indexes into the cross-file accumulator. `method_visibilities` (ADR-35) is
|
|
2069
|
+
# collected here so the override-visibility-reduced rule can read an ancestor's visibility declared in a sibling
|
|
2070
|
+
# file.
|
|
2371
2071
|
def accumulate_project_index(acc, path, root)
|
|
2372
|
-
# One combined descent yields both the methods existence table and
|
|
2373
|
-
#
|
|
2374
|
-
#
|
|
2375
|
-
# instead of three times (methods + def-nodes ×2). See
|
|
2376
|
-
# {#build_methods_and_def_nodes}.
|
|
2072
|
+
# One combined descent yields both the methods existence table and the def-node table; the latter is also
|
|
2073
|
+
# consumed by `record_class_sources`, so a def-dense file is walked once here instead of three times (methods +
|
|
2074
|
+
# def-nodes ×2). See {#build_methods_and_def_nodes}.
|
|
2377
2075
|
file_methods, file_def_nodes = build_methods_and_def_nodes(root)
|
|
2378
2076
|
merge_discovered_defs(acc[:def_nodes], acc[:def_sources], path, file_def_nodes)
|
|
2379
2077
|
build_discovered_singleton_def_nodes(root).each do |class_name, methods|
|
|
@@ -2390,18 +2088,16 @@ module Rigor
|
|
|
2390
2088
|
merge_member_layout_tables(acc, root)
|
|
2391
2089
|
end
|
|
2392
2090
|
|
|
2393
|
-
# Folds one file's Data + Struct member-layout tables into the
|
|
2394
|
-
#
|
|
2395
|
-
# hold its ABC budget).
|
|
2091
|
+
# Folds one file's Data + Struct member-layout tables into the cross-file accumulator (kept out of
|
|
2092
|
+
# {#accumulate_project_index} to hold its ABC budget).
|
|
2396
2093
|
def merge_member_layout_tables(acc, root)
|
|
2397
2094
|
acc[:data_member_layouts].merge!(build_data_member_layouts(root))
|
|
2398
2095
|
acc[:struct_member_layouts].merge!(build_struct_member_layouts(root))
|
|
2399
2096
|
end
|
|
2400
2097
|
|
|
2401
|
-
# Folds the per-class method-visibility and method-existence tables of
|
|
2402
|
-
#
|
|
2403
|
-
#
|
|
2404
|
-
# is the existence table from the combined methods/def-nodes descent.
|
|
2098
|
+
# Folds the per-class method-visibility and method-existence tables of one file into the cross-file accumulator
|
|
2099
|
+
# (kept out of {#accumulate_project_index} to hold its ABC budget). `file_methods` is the existence table from the
|
|
2100
|
+
# combined methods/def-nodes descent.
|
|
2405
2101
|
def merge_class_keyed_index_tables(acc, root, file_methods)
|
|
2406
2102
|
build_discovered_method_visibilities(root).each do |class_name, table|
|
|
2407
2103
|
(acc[:method_visibilities][class_name] ||= {}).merge!(table)
|
|
@@ -2411,16 +2107,12 @@ module Rigor
|
|
|
2411
2107
|
end
|
|
2412
2108
|
end
|
|
2413
2109
|
|
|
2414
|
-
# ADR-46 slice 1 — accumulates, per qualified user class/module
|
|
2415
|
-
#
|
|
2416
|
-
#
|
|
2417
|
-
#
|
|
2418
|
-
#
|
|
2419
|
-
#
|
|
2420
|
-
# {Scope#includes_of} record this set when resolving the edge during
|
|
2421
|
-
# dependency recording (ADR-46). The class-declaration walk
|
|
2422
|
-
# (`collect_class_decls`) catches bodyless / def-less reopenings the
|
|
2423
|
-
# other three builders miss.
|
|
2110
|
+
# ADR-46 slice 1 — accumulates, per qualified user class/module name, the set of files that declare it. A class's
|
|
2111
|
+
# declaration shape (its body `def`s, its `class Foo < Bar` superclass, its `include`s) lives wherever the class
|
|
2112
|
+
# is opened, so every file that contributes a def / superclass / include for a name is a source of that name's
|
|
2113
|
+
# ancestry edges. {Scope#superclass_of} / {Scope#includes_of} record this set when resolving the edge during
|
|
2114
|
+
# dependency recording (ADR-46). The class-declaration walk (`collect_class_decls`) catches bodyless / def-less
|
|
2115
|
+
# reopenings the other three builders miss.
|
|
2424
2116
|
def record_class_sources(class_sources, path, root, superclasses, includes, file_def_nodes)
|
|
2425
2117
|
names = Set.new
|
|
2426
2118
|
collect_class_decls(root, [], decls = {})
|
|
@@ -2431,11 +2123,9 @@ module Rigor
|
|
|
2431
2123
|
names.each { |name| (class_sources[name] ||= Set.new) << path }
|
|
2432
2124
|
end
|
|
2433
2125
|
|
|
2434
|
-
# Merges one file's `class → method → DefNode` map into the
|
|
2435
|
-
#
|
|
2436
|
-
#
|
|
2437
|
-
# the un-registered-project-patch signal `call.undefined-method`
|
|
2438
|
-
# and `rigor triage` key on).
|
|
2126
|
+
# Merges one file's `class → method → DefNode` map into the cross-file `def_nodes` index and records each method's
|
|
2127
|
+
# first- seen `"path:line"` definition site in `def_sources` (ADR-17 — the un-registered-project-patch signal
|
|
2128
|
+
# `call.undefined-method` and `rigor triage` key on).
|
|
2439
2129
|
def merge_discovered_defs(def_nodes, def_sources, path, file_def_nodes)
|
|
2440
2130
|
file_def_nodes.each do |class_name, methods|
|
|
2441
2131
|
(def_nodes[class_name] ||= {}).merge!(methods)
|
|
@@ -2446,10 +2136,8 @@ module Rigor
|
|
|
2446
2136
|
end
|
|
2447
2137
|
end
|
|
2448
2138
|
|
|
2449
|
-
# Class-only variant of `record_declarations` — descends
|
|
2450
|
-
#
|
|
2451
|
-
# registers `Foo::Bar`) but never registers the module
|
|
2452
|
-
# itself in `accumulator`.
|
|
2139
|
+
# Class-only variant of `record_declarations` — descends into nested module bodies (so `module Foo; class Bar`
|
|
2140
|
+
# registers `Foo::Bar`) but never registers the module itself in `accumulator`.
|
|
2453
2141
|
def collect_class_decls(node, qualified_prefix, accumulator)
|
|
2454
2142
|
return unless node.is_a?(Prism::Node)
|
|
2455
2143
|
|
|
@@ -2471,22 +2159,16 @@ module Rigor
|
|
|
2471
2159
|
node.compact_child_nodes.each { |child| collect_class_decls(child, qualified_prefix, accumulator) }
|
|
2472
2160
|
end
|
|
2473
2161
|
|
|
2474
|
-
# T1 (template-corpora survey) — record a `Const = Class.new(Super)`
|
|
2475
|
-
#
|
|
2476
|
-
#
|
|
2477
|
-
#
|
|
2478
|
-
#
|
|
2479
|
-
#
|
|
2480
|
-
#
|
|
2481
|
-
#
|
|
2482
|
-
#
|
|
2483
|
-
#
|
|
2484
|
-
# The superclass name is resolved lexically against the enclosing
|
|
2485
|
-
# prefix; a bare `Class.new` with no superclass (or `Module.new`)
|
|
2486
|
-
# types as `Singleton[Const]` itself. The block form is left to the
|
|
2487
|
-
# existing `meta_new_block_body` machinery — only the plain
|
|
2488
|
-
# `Class.new(Super)` constant (the namespaced-sibling-error idiom)
|
|
2489
|
-
# is added here.
|
|
2162
|
+
# T1 (template-corpora survey) — record a `Const = Class.new(Super)` (and the bare `Class.new` / `Module.new`)
|
|
2163
|
+
# class-creating constant in the cross-file discovery table so a reference to `Const` from ANOTHER file under the
|
|
2164
|
+
# same namespace resolves to the project class instead of falling through to a core same-named class
|
|
2165
|
+
# (`Liquid::SyntaxError = Class.new(Error)` referenced in a sibling file's `rescue SyntaxError => e`, which
|
|
2166
|
+
# otherwise resolved to core `::SyntaxError`). Mirrors the single-file `in_source_constants` answer, which types
|
|
2167
|
+
# `Class.new(Super)` as `Singleton[Super]` (the constructed class answers method lookups through Super's chain).
|
|
2168
|
+
# The superclass name is resolved lexically against the enclosing prefix; a bare `Class.new` with no superclass
|
|
2169
|
+
# (or `Module.new`) types as `Singleton[Const]` itself. The block form is left to the existing
|
|
2170
|
+
# `meta_new_block_body` machinery — only the plain `Class.new(Super)` constant (the namespaced-sibling-error
|
|
2171
|
+
# idiom) is added here.
|
|
2490
2172
|
def record_class_new_constant_decl(node, qualified_prefix, accumulator)
|
|
2491
2173
|
rvalue = node.value
|
|
2492
2174
|
return unless class_new_call?(rvalue) || module_new_call?(rvalue)
|
|
@@ -2497,13 +2179,11 @@ module Rigor
|
|
|
2497
2179
|
accumulator[full] = Type::Combinator.singleton_of(super_name || full)
|
|
2498
2180
|
end
|
|
2499
2181
|
|
|
2500
|
-
# Lexically-qualified name of a `Class.new(Super)` superclass
|
|
2501
|
-
#
|
|
2502
|
-
#
|
|
2503
|
-
#
|
|
2504
|
-
#
|
|
2505
|
-
# resolves to `M::Error`); otherwise the literal name is returned
|
|
2506
|
-
# (covering a core / RBS-known superclass spelled bare).
|
|
2182
|
+
# Lexically-qualified name of a `Class.new(Super)` superclass argument, or nil when there is no positional
|
|
2183
|
+
# superclass (a bare `Class.new` / `Module.new`). When the unqualified super name is a class already discovered
|
|
2184
|
+
# under an enclosing-prefix segment, the qualified form is returned (so `Class.new(Error)` inside `module M`
|
|
2185
|
+
# resolves to `M::Error`); otherwise the literal name is returned (covering a core / RBS-known superclass spelled
|
|
2186
|
+
# bare).
|
|
2507
2187
|
def class_new_superclass_name(call_node, qualified_prefix, accumulator)
|
|
2508
2188
|
arg = call_node.arguments&.arguments&.first
|
|
2509
2189
|
return nil if arg.nil?
|
|
@@ -2521,15 +2201,11 @@ module Rigor
|
|
|
2521
2201
|
raw
|
|
2522
2202
|
end
|
|
2523
2203
|
|
|
2524
|
-
# Walks the program once for `Prism::ModuleNode` and
|
|
2525
|
-
#
|
|
2526
|
-
#
|
|
2527
|
-
#
|
|
2528
|
-
#
|
|
2529
|
-
# ordinary lexical walk), so we annotate ONLY the topmost
|
|
2530
|
-
# path node. Nested declarations contribute their fully
|
|
2531
|
-
# qualified path: `class A::B; class C; ...` produces
|
|
2532
|
-
# `A::B` for the outer and `A::B::C` for the inner.
|
|
2204
|
+
# Walks the program once for `Prism::ModuleNode` and `Prism::ClassNode`, recording the `Singleton[<qualified>]`
|
|
2205
|
+
# type for the outermost `constant_path` node of each declaration. Inner segments of a `class Foo::Bar::Baz` path
|
|
2206
|
+
# remain real references (resolved through the ordinary lexical walk), so we annotate ONLY the topmost path node.
|
|
2207
|
+
# Nested declarations contribute their fully qualified path: `class A::B; class C; ...` produces `A::B` for the
|
|
2208
|
+
# outer and `A::B::C` for the inner.
|
|
2533
2209
|
def build_declaration_artifacts(root)
|
|
2534
2210
|
identity_table = {}.compare_by_identity
|
|
2535
2211
|
discovered = {}
|
|
@@ -2565,21 +2241,17 @@ module Rigor
|
|
|
2565
2241
|
true
|
|
2566
2242
|
end
|
|
2567
2243
|
|
|
2568
|
-
# Recognises class-creating meta calls at constant-write rvalue
|
|
2569
|
-
#
|
|
2570
|
-
#
|
|
2571
|
-
# then resolves to a fresh `Nominal[Const]` via `meta_new`,
|
|
2572
|
-
# instead of the un-narrowed `Dynamic[top]` returned by the
|
|
2573
|
-
# default `Class#new` envelope.
|
|
2244
|
+
# Recognises class-creating meta calls at constant-write rvalue position and registers `Const` (qualified by the
|
|
2245
|
+
# surrounding class/module path) as a discovered class. `Const.new(...)` then resolves to a fresh `Nominal[Const]`
|
|
2246
|
+
# via `meta_new`, instead of the un-narrowed `Dynamic[top]` returned by the default `Class#new` envelope.
|
|
2574
2247
|
#
|
|
2575
2248
|
# Two recognised meta forms:
|
|
2576
2249
|
#
|
|
2577
2250
|
# - `Const = Data.define(*Symbol) [do ... end]`
|
|
2578
2251
|
# - `Const = Struct.new(*Symbol [, keyword_init: ...]) [do ... end]`
|
|
2579
2252
|
#
|
|
2580
|
-
# The block body, if present, is recursed into so any nested
|
|
2581
|
-
#
|
|
2582
|
-
# still feed the discovered table.
|
|
2253
|
+
# The block body, if present, is recursed into so any nested class/module declarations in the override block (rare
|
|
2254
|
+
# but legal) still feed the discovered table.
|
|
2583
2255
|
def record_meta_new_constant?(node, qualified_prefix, identity_table, discovered)
|
|
2584
2256
|
return false unless data_define_call?(node.value) || struct_new_call?(node.value)
|
|
2585
2257
|
|
|
@@ -2589,11 +2261,9 @@ module Rigor
|
|
|
2589
2261
|
true
|
|
2590
2262
|
end
|
|
2591
2263
|
|
|
2592
|
-
# Recognises `Data.define(*Symbol)` and `Data.define(*Symbol) do ... end`
|
|
2593
|
-
#
|
|
2594
|
-
#
|
|
2595
|
-
# method call return) are rejected because their identity is not
|
|
2596
|
-
# statically known.
|
|
2264
|
+
# Recognises `Data.define(*Symbol)` and `Data.define(*Symbol) do ... end` at constant-write rvalue position. The
|
|
2265
|
+
# receiver MUST be the bare `Data` constant (or `::Data`); other receivers (a local variable, a method call
|
|
2266
|
+
# return) are rejected because their identity is not statically known.
|
|
2597
2267
|
def data_define_call?(node)
|
|
2598
2268
|
return false unless node.is_a?(Prism::CallNode)
|
|
2599
2269
|
return false unless node.name == :define
|
|
@@ -2603,12 +2273,9 @@ module Rigor
|
|
|
2603
2273
|
args.all?(Prism::SymbolNode)
|
|
2604
2274
|
end
|
|
2605
2275
|
|
|
2606
|
-
# Recognises `Struct.new(*Symbol)` and
|
|
2607
|
-
# `
|
|
2608
|
-
#
|
|
2609
|
-
# `keyword_init: ...` form) is accepted but does not contribute
|
|
2610
|
-
# to member discovery; every other argument MUST be a
|
|
2611
|
-
# `Prism::SymbolNode`. At least one Symbol member is required —
|
|
2276
|
+
# Recognises `Struct.new(*Symbol)` and `Struct.new(*Symbol, keyword_init: <expr>)` at constant-write rvalue
|
|
2277
|
+
# position. A trailing `KeywordHashNode` (the `keyword_init: ...` form) is accepted but does not contribute to
|
|
2278
|
+
# member discovery; every other argument MUST be a `Prism::SymbolNode`. At least one Symbol member is required —
|
|
2612
2279
|
# `Struct.new()` is a degenerate form callers don't typically use.
|
|
2613
2280
|
def struct_new_call?(node)
|
|
2614
2281
|
return false unless meta_call_with_name?(node, :Struct, :new)
|
|
@@ -2620,27 +2287,19 @@ module Rigor
|
|
|
2620
2287
|
positional.all?(Prism::SymbolNode)
|
|
2621
2288
|
end
|
|
2622
2289
|
|
|
2623
|
-
# Recognises `Module.new` and `Module.new(&block)` /
|
|
2624
|
-
#
|
|
2625
|
-
#
|
|
2626
|
-
#
|
|
2627
|
-
# named constant (`Const = Module.new do ...; def foo; ...; end; end`).
|
|
2628
|
-
# Arguments are NOT inspected because `Module.new` accepts
|
|
2629
|
-
# no positionals — Ruby raises ArgumentError if any are
|
|
2630
|
-
# passed — so a malformed call falls through the walker
|
|
2290
|
+
# Recognises `Module.new` and `Module.new(&block)` / `Module.new do ... end` at constant-write rvalue position.
|
|
2291
|
+
# The block body is the anonymous module's `module_eval` body; defs inside it bind methods on the named constant
|
|
2292
|
+
# (`Const = Module.new do ...; def foo; ...; end; end`). Arguments are NOT inspected because `Module.new` accepts
|
|
2293
|
+
# no positionals — Ruby raises ArgumentError if any are passed — so a malformed call falls through the walker
|
|
2631
2294
|
# without affecting analysis.
|
|
2632
2295
|
def module_new_call?(node)
|
|
2633
2296
|
meta_call_with_name?(node, :Module, :new)
|
|
2634
2297
|
end
|
|
2635
2298
|
|
|
2636
|
-
# Recognises `Class.new`, `Class.new(super_class)`, and the
|
|
2637
|
-
# block
|
|
2638
|
-
#
|
|
2639
|
-
#
|
|
2640
|
-
# NOT route through `ancestor` discovery in this slice — the
|
|
2641
|
-
# synthesised class still answers method lookups via its
|
|
2642
|
-
# own body's defs, mirroring how `Struct.new` / `Data.define`
|
|
2643
|
-
# are handled.
|
|
2299
|
+
# Recognises `Class.new`, `Class.new(super_class)`, and the block form `Class.new { ... }`. Like
|
|
2300
|
+
# `module_new_call?`, the block body is walked as the anonymous class's body. The optional `super_class`
|
|
2301
|
+
# positional is accepted but does NOT route through `ancestor` discovery in this slice — the synthesised class
|
|
2302
|
+
# still answers method lookups via its own body's defs, mirroring how `Struct.new` / `Data.define` are handled.
|
|
2644
2303
|
def class_new_call?(node)
|
|
2645
2304
|
meta_call_with_name?(node, :Class, :new)
|
|
2646
2305
|
end
|
|
@@ -2665,17 +2324,14 @@ module Rigor
|
|
|
2665
2324
|
end
|
|
2666
2325
|
end
|
|
2667
2326
|
|
|
2668
|
-
# Walks `node`'s subtree DFS and fills in scope entries for every
|
|
2669
|
-
#
|
|
2670
|
-
#
|
|
2671
|
-
# nodes inherit their nearest recorded ancestor's scope.
|
|
2327
|
+
# Walks `node`'s subtree DFS and fills in scope entries for every Prism node the StatementEvaluator did not visit
|
|
2328
|
+
# (i.e. expression- interior nodes like the receiver/args of a CallNode). Those nodes inherit their nearest
|
|
2329
|
+
# recorded ancestor's scope.
|
|
2672
2330
|
#
|
|
2673
|
-
# `IfNode` / `UnlessNode` are special-cased: the truthy and falsey
|
|
2674
|
-
#
|
|
2675
|
-
#
|
|
2676
|
-
#
|
|
2677
|
-
# nested as call arguments) which are typed by ExpressionTyper
|
|
2678
|
-
# without going through `eval_if`'s narrowing path.
|
|
2331
|
+
# `IfNode` / `UnlessNode` are special-cased: the truthy and falsey branches each get their predicate's narrowed
|
|
2332
|
+
# scope before recursing. This handles expression-position conditionals (e.g. `cache[k] = if cond; t; else; e;
|
|
2333
|
+
# end` and conditionals nested as call arguments) which are typed by ExpressionTyper without going through
|
|
2334
|
+
# `eval_if`'s narrowing path.
|
|
2679
2335
|
def propagate(node, table, parent_scope)
|
|
2680
2336
|
return unless node.is_a?(Prism::Node)
|
|
2681
2337
|
|