rigortype 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/data/core_overlay/csv.rbs +28 -0
- data/data/core_overlay/psych.rbs +22 -0
- data/docs/handbook/01-getting-started.md +9 -1
- data/docs/handbook/02-everyday-types.md +4 -1
- data/docs/handbook/08-understanding-errors.md +3 -3
- data/docs/manual/01-installation.md +1 -0
- data/docs/manual/02-cli-reference.md +16 -7
- data/docs/manual/04-diagnostics.md +7 -4
- data/docs/manual/06-baseline.md +5 -2
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/14-rails-quickstart.md +4 -2
- data/docs/manual/15-type-protection-coverage.md +27 -4
- data/docs/manual/plugins/rigor-actionpack.md +1 -1
- data/docs/manual/plugins/rigor-activerecord.md +12 -5
- data/docs/manual/plugins/rigor-rails-routes.md +11 -0
- 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 +248 -186
- 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 +62 -48
- 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 +46 -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_fork_scan.rb +55 -0
- data/lib/rigor/cli/protection_renderer.rb +5 -7
- data/lib/rigor/cli/protection_report.rb +34 -18
- 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/missing_gem_constant_index.rb +128 -0
- 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 +225 -304
- 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 +794 -1154
- 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/fork_map.rb +87 -0
- 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 +627 -727
- data/lib/rigor/inference/origin_lookup.rb +38 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +154 -109
- 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 -821
- data/lib/rigor/inference/statement_evaluator.rb +754 -1114
- 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 +141 -184
- 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 +92 -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 +40 -48
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +140 -129
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +56 -59
- 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/structure_sql_parser.rb +172 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +168 -241
- 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-activesupport-core-ext/sig/active_support/core_ext.rbs +32 -0
- 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/grape_api_discoverer.rb +189 -0
- 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 +61 -75
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +319 -505
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +91 -96
- 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/environment.rbs +1 -0
- data/sig/rigor/scope.rbs +6 -0
- metadata +9 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
# ADR-82 — the effective {DynamicOrigin} cause of a receiver *expression*, unifying the two provenance
|
|
8
|
+
# lookups so a chain and a bare variable resolve the same way:
|
|
9
|
+
#
|
|
10
|
+
# - the cause recorded on the expression's own node (`Scope#dynamic_origins`), covering a call node whose
|
|
11
|
+
# dispatch recorded a cause (WD2's user-method tiers, or WD6's chain inheritance), and
|
|
12
|
+
# - for a bare local / instance-variable read, the cause propagated from its binding (WD1's
|
|
13
|
+
# `Scope#local_origins` / `#ivar_origins`) — the read node itself carries none, the cause was recorded
|
|
14
|
+
# on the assignment's rhs.
|
|
15
|
+
#
|
|
16
|
+
# Consulted by `ProtectionScanner` (to label a hole) and `ExpressionTyper` (WD6 — to inherit a Dynamic
|
|
17
|
+
# receiver's cause onto the call it produces, so provenance survives a method chain).
|
|
18
|
+
module OriginLookup
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# @param scope [Rigor::Scope]
|
|
22
|
+
# @param node [Prism::Node, nil] the receiver expression's node (nil for an implicit-self receiver)
|
|
23
|
+
# @return [Symbol, nil] the effective dynamic-origin cause, or nil when none is known
|
|
24
|
+
def origin_for(scope, node)
|
|
25
|
+
return nil if node.nil?
|
|
26
|
+
|
|
27
|
+
scope.dynamic_origins[node] || propagated(scope, node)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def propagated(scope, node)
|
|
31
|
+
case node
|
|
32
|
+
when Prism::LocalVariableReadNode then scope.local_origin(node.name)
|
|
33
|
+
when Prism::InstanceVariableReadNode then scope.ivar_origin(node.name)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "scope_indexer"
|
|
6
|
+
require_relative "fork_map"
|
|
6
7
|
require_relative "../source/node_walker"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module Inference
|
|
10
|
-
# Pure argument-type classification for {ParameterInferenceCollector} — no
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
11
|
+
# Pure argument-type classification for {ParameterInferenceCollector} — no collector state,
|
|
12
|
+
# so it lives outside the orchestration class. Decides which argument types may seed a
|
|
13
|
+
# parameter (concrete enough for the protection metric to bite) and widens a literal
|
|
14
|
+
# argument to its nominal.
|
|
14
15
|
module ParameterArgTypes
|
|
15
16
|
module_function
|
|
16
17
|
|
|
@@ -20,10 +21,9 @@ module Rigor
|
|
|
20
21
|
FalseClass => "FalseClass", NilClass => "NilClass"
|
|
21
22
|
}.freeze
|
|
22
23
|
|
|
23
|
-
# A parameter holds a *value of* a type across its lifetime, not a pinned
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
# collapses to `String`.
|
|
24
|
+
# A parameter holds a *value of* a type across its lifetime, not a pinned literal — so a
|
|
25
|
+
# `Constant<"text">` argument widens to its nominal (`String`); recurses through unions so
|
|
26
|
+
# `Constant<"a"> | Constant<"b">` collapses to `String`.
|
|
27
27
|
def widen_for_param(type)
|
|
28
28
|
case type
|
|
29
29
|
when Type::Constant
|
|
@@ -41,10 +41,10 @@ module Rigor
|
|
|
41
41
|
nil
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
# The dispatch class for a receiver type, for the subset the collector
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
44
|
+
# The dispatch class for a receiver type, for the subset the collector resolves to a user
|
|
45
|
+
# `def` (mirrors `CheckRules#concrete_class_name` for the carriers a user-method receiver
|
|
46
|
+
# is typed as). `class_name_of` is the Nominal/Singleton-only variant for an implicit-self
|
|
47
|
+
# `self_type`.
|
|
48
48
|
def concrete_class_name(type)
|
|
49
49
|
case type
|
|
50
50
|
when Type::Nominal, Type::Singleton then type.class_name
|
|
@@ -57,9 +57,8 @@ module Rigor
|
|
|
57
57
|
type.class_name if type.is_a?(Type::Nominal) || type.is_a?(Type::Singleton)
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
-
# Whether `type` is too gradual to seed (not a concrete dispatch target) —
|
|
61
|
-
#
|
|
62
|
-
# concrete only when every arm is).
|
|
60
|
+
# Whether `type` is too gradual to seed (not a concrete dispatch target) — the negation of
|
|
61
|
+
# `ProtectionScanner#concrete_receiver?` (a union is concrete only when every arm is).
|
|
63
62
|
def non_concrete?(type)
|
|
64
63
|
case type
|
|
65
64
|
when Type::Dynamic, Type::Top, Type::Bot then true
|
|
@@ -71,55 +70,50 @@ module Rigor
|
|
|
71
70
|
|
|
72
71
|
# ADR-67 WD3 + WD5 — call-site parameter type inference (capped fixpoint).
|
|
73
72
|
#
|
|
74
|
-
# A `def` parameter with no RBS signature types `untyped` (the gradual entry
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
# parameter is typed one hop further per round (round 1 alone is the
|
|
84
|
-
# single-level pass).
|
|
73
|
+
# A `def` parameter with no RBS signature types `untyped` (the gradual entry point), so a
|
|
74
|
+
# param flowing into an ivar or a receiver drains everything downstream to `Dynamic`. This
|
|
75
|
+
# pass closes that hole: it walks every project file, types each call's positional
|
|
76
|
+
# arguments in their lexical scope, resolves which user-defined `def` each call targets,
|
|
77
|
+
# and records the **union of resolved call-site argument types** per parameter — TypeProf's
|
|
78
|
+
# "a parameter's type is the union of every actual argument across all call sites". WD5: it
|
|
79
|
+
# iterates (cap {DEFAULT_ROUNDS}), re-seeding each round with the previous round's inferred
|
|
80
|
+
# parameters, so a parameter passed *another* parameter is typed one hop further per round
|
|
81
|
+
# (round 1 alone is the single-level pass).
|
|
85
82
|
#
|
|
86
83
|
# The result is keyed by `[class_name, method_name, kind]` — the same triple
|
|
87
|
-
# `StatementEvaluator#build_method_entry_scope` reconstructs from the lexical
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
# WD1), so being wrong cannot manufacture a false positive at a caller.
|
|
84
|
+
# `StatementEvaluator#build_method_entry_scope` reconstructs from the lexical class path —
|
|
85
|
+
# so the consumer can seed an undeclared parameter with its inferred type. The table is
|
|
86
|
+
# precision-additive only: it never feeds a parameter-boundary diagnostic (an inferred type
|
|
87
|
+
# lives solely as a body local, and the boundary rules consult RBS, not body locals — see
|
|
88
|
+
# ADR-67 WD1), so being wrong cannot manufacture a false positive at a caller.
|
|
93
89
|
#
|
|
94
|
-
# Soundness (WD4): the inferred type is a sound over-approximation only when
|
|
95
|
-
#
|
|
96
|
-
# `
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
90
|
+
# Soundness (WD4): the inferred type is a sound over-approximation only when every
|
|
91
|
+
# contributed call site resolves to a concrete argument type. Any `Dynamic` / `Top` /
|
|
92
|
+
# `Bot` argument (a `send`/dynamic-dispatch caller, or a parameter not yet typed in an
|
|
93
|
+
# earlier round) **poisons** the parameter, which then contributes nothing this round (it
|
|
94
|
+
# may type in a later round once its own argument resolves). Unresolved call sites do not
|
|
95
|
+
# contribute.
|
|
100
96
|
#
|
|
101
|
-
# What it does NOT do yet (deferred — the check-wiring slice): feeding the
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
# consumer is the protection metric, which runs no diagnostics).
|
|
97
|
+
# What it does NOT do yet (deferred — the check-wiring slice): feeding the table into the
|
|
98
|
+
# `check` walk (only `coverage --protection` consumes it today), keyword / optional / rest
|
|
99
|
+
# / block parameters, inherited-method receivers, top-level helpers, and a rigorous
|
|
100
|
+
# closed-call-site-set proof (the union is optimistic over resolved sites — acceptable
|
|
101
|
+
# because the only consumer is the protection metric, which runs no diagnostics).
|
|
107
102
|
class ParameterInferenceCollector
|
|
108
103
|
EMPTY = {}.freeze
|
|
109
104
|
|
|
110
|
-
# A defensive widening cap (ADR-41): a parameter unioned from more than
|
|
111
|
-
#
|
|
112
|
-
#
|
|
105
|
+
# A defensive widening cap (ADR-41): a parameter unioned from more than this many distinct
|
|
106
|
+
# concrete call-site types is widened to `untyped` (poisoned) rather than carrying an
|
|
107
|
+
# unbounded union.
|
|
113
108
|
MAX_CALL_SITE_TYPES = 16
|
|
114
109
|
|
|
115
|
-
# WD5 — the call-site union is a worklist fixpoint: each round re-types the
|
|
116
|
-
#
|
|
117
|
-
# parameter
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
# is the single-level pass.
|
|
110
|
+
# WD5 — the call-site union is a worklist fixpoint: each round re-types the project with
|
|
111
|
+
# the previous round's inferred parameters seeded, so a parameter passed *another*
|
|
112
|
+
# parameter is typed one hop further per round. Capped (no true-convergence requirement —
|
|
113
|
+
# the metric tolerates a bounded approximation, and the table can oscillate at the margin
|
|
114
|
+
# since a newly resolved receiver can surface a fresh untyped-argument call site). The cap
|
|
115
|
+
# matches the `Inference::BodyFixpoint` cap-3 convention. Round 1 alone is the
|
|
116
|
+
# single-level pass.
|
|
123
117
|
DEFAULT_ROUNDS = 3
|
|
124
118
|
|
|
125
119
|
# @param files [Array<String>] project `.rb` paths to scan for call sites.
|
|
@@ -127,25 +121,34 @@ module Rigor
|
|
|
127
121
|
# @param target_ruby [String, nil] Prism parse target.
|
|
128
122
|
# @param max_rounds [Integer] the WD5 fixpoint cap (1 = single-level).
|
|
129
123
|
# @return [Hash{[String,Symbol,Symbol] => Hash{Symbol => Rigor::Type}}] frozen.
|
|
130
|
-
def self.collect(files:, environment:, target_ruby: nil, max_rounds: DEFAULT_ROUNDS)
|
|
131
|
-
new(files: files, environment: environment, target_ruby: target_ruby,
|
|
124
|
+
def self.collect(files:, environment:, target_ruby: nil, max_rounds: DEFAULT_ROUNDS, workers: 0)
|
|
125
|
+
new(files: files, environment: environment, target_ruby: target_ruby,
|
|
126
|
+
max_rounds: max_rounds, workers: workers).collect
|
|
132
127
|
end
|
|
133
128
|
|
|
134
|
-
def initialize(files:, environment:, target_ruby: nil, max_rounds: DEFAULT_ROUNDS)
|
|
129
|
+
def initialize(files:, environment:, target_ruby: nil, max_rounds: DEFAULT_ROUNDS, workers: 0)
|
|
135
130
|
@files = files
|
|
136
131
|
@environment = environment
|
|
137
132
|
@target_ruby = target_ruby
|
|
138
133
|
@max_rounds = max_rounds
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
|
|
134
|
+
# P3-10 — fork-parallelism for the per-round re-typing (the dominant coverage-protection cost). A
|
|
135
|
+
# round's per-file typing is independent; contributions merge associatively (see {#merge_round}), so
|
|
136
|
+
# forking over file slices is byte-identical to the sequential pass. 0/1 → sequential.
|
|
137
|
+
@workers = workers
|
|
138
|
+
# Reset per round (see {#run_round}). `[[class, method, kind], param_sym]` => [Type] of
|
|
139
|
+
# observed concrete arguments (a default-block Hash, not a `{}` literal, so the
|
|
140
|
+
# analyzer types its reads generically — {#finalize}), plus the ids widened to
|
|
141
|
+
# `untyped` (an untyped / over-cap argument).
|
|
143
142
|
@type_observations = Hash.new { |hash, id| hash[id] = [] }
|
|
144
143
|
@poisoned_params = Set.new
|
|
145
144
|
end
|
|
146
145
|
|
|
147
146
|
def collect
|
|
148
147
|
parsed = parse_all
|
|
148
|
+
# Force the full RBS load on the parent so forked round-workers copy-on-write inherit a warm
|
|
149
|
+
# environment instead of each rebuilding it (mirrors the check / scan fork pools). A no-op on the
|
|
150
|
+
# sequential path.
|
|
151
|
+
@environment.rbs_loader&.prewarm if ForkMap.parallel?([@workers, parsed.size].min)
|
|
149
152
|
discovery = discovery_seed_tables
|
|
150
153
|
table = EMPTY
|
|
151
154
|
@max_rounds.times do
|
|
@@ -159,8 +162,8 @@ module Rigor
|
|
|
159
162
|
|
|
160
163
|
private
|
|
161
164
|
|
|
162
|
-
# Parse every project file once; rounds re-index the cached ASTs against an
|
|
163
|
-
#
|
|
165
|
+
# Parse every project file once; rounds re-index the cached ASTs against an evolving seed
|
|
166
|
+
# rather than re-parsing.
|
|
164
167
|
def parse_all
|
|
165
168
|
@files.filter_map do |path|
|
|
166
169
|
result = Prism.parse(File.read(path), filepath: path, version: @target_ruby)
|
|
@@ -170,25 +173,58 @@ module Rigor
|
|
|
170
173
|
end
|
|
171
174
|
end
|
|
172
175
|
|
|
173
|
-
# One fixpoint round: re-type every file with `seed_table` (the previous
|
|
174
|
-
#
|
|
176
|
+
# One fixpoint round: re-type every file with `seed_table` (the previous round's inferred
|
|
177
|
+
# parameters) seeded, collecting the next round's table. The per-file typing is fork-mapped over
|
|
178
|
+
# `@workers` (byte-identical to sequential — see {#merge_round}); each slice returns a marshalable
|
|
179
|
+
# `[observations, poisoned]` contribution the parent merges in slice order.
|
|
175
180
|
def run_round(parsed, discovery_tables, seed_table)
|
|
181
|
+
seed_scope = build_seed_scope(discovery_tables, seed_table)
|
|
182
|
+
contributions = ForkMap.call(items: parsed, workers: @workers) do |slice|
|
|
183
|
+
accumulate_slice(slice, seed_scope)
|
|
184
|
+
end
|
|
185
|
+
merge_round(contributions)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Types every call in one contiguous file slice, returning a marshalable contribution: the per-`id`
|
|
189
|
+
# observed argument types (default proc stripped for `Marshal`) and the poisoned-`id` list. Uses the
|
|
190
|
+
# per-process observation ivars, so a forked worker and the sequential single-slice call share this
|
|
191
|
+
# exact code.
|
|
192
|
+
def accumulate_slice(parsed_slice, seed_scope)
|
|
176
193
|
@type_observations = Hash.new { |hash, id| hash[id] = [] }
|
|
177
194
|
@poisoned_params = Set.new
|
|
178
|
-
|
|
179
|
-
parsed.each do |path, ast|
|
|
195
|
+
parsed_slice.each do |path, ast|
|
|
180
196
|
index = ScopeIndexer.index(ast, default_scope: seed_scope.with_source_path(path))
|
|
181
197
|
Source::NodeWalker.each(ast) do |node|
|
|
182
198
|
record_call(node, index) if node.is_a?(Prism::CallNode)
|
|
183
199
|
end
|
|
184
200
|
end
|
|
185
|
-
|
|
201
|
+
# Copy into a plain Hash, dropping the default proc — a Marshal-unfriendly proc the fork worker
|
|
202
|
+
# would otherwise fail to dump. (`.to_h` returns self here, keeping the proc, so copy explicitly.)
|
|
203
|
+
observations = @type_observations.each_with_object({}) { |(id, types), plain| plain[id] = types }
|
|
204
|
+
[observations, @poisoned_params.to_a]
|
|
186
205
|
end
|
|
187
206
|
|
|
188
|
-
#
|
|
189
|
-
#
|
|
190
|
-
#
|
|
191
|
-
#
|
|
207
|
+
# Merges the per-slice contributions into the round's table. Associative and order-preserving, so the
|
|
208
|
+
# result is identical to a sequential single-slice run: a parameter is poisoned if ANY slice poisoned
|
|
209
|
+
# it, its observations are the file-order concatenation across slices, and the {MAX_CALL_SITE_TYPES}
|
|
210
|
+
# cap re-applies over the merged total (a per-slice sub-cap can undercount, so the parent enforces the
|
|
211
|
+
# real cap — the same "poisoned" outcome the sequential mid-stream cap reaches).
|
|
212
|
+
def merge_round(contributions)
|
|
213
|
+
poisoned = contributions.each_with_object(Set.new) { |(_obs, pois), set| set.merge(pois) }
|
|
214
|
+
observations = {}
|
|
215
|
+
contributions.each do |(obs, _pois)|
|
|
216
|
+
obs.each do |id, types|
|
|
217
|
+
(observations[id] ||= []).concat(types) unless poisoned.include?(id)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
observations.each { |id, types| poisoned << id if types.length > MAX_CALL_SITE_TYPES }
|
|
221
|
+
finalize(observations, poisoned)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# A scope carrying the cross-file discovery index (so `Foo.new` receivers and
|
|
225
|
+
# implicit-self calls resolve to a user `def`) plus the prior round's inferred parameters
|
|
226
|
+
# (so an argument that reads a parameter types to its current inferred type — the WD5
|
|
227
|
+
# propagation).
|
|
192
228
|
def build_seed_scope(discovery_tables, seed_table)
|
|
193
229
|
base = Scope.empty(environment: @environment)
|
|
194
230
|
tables = discovery_tables
|
|
@@ -209,9 +245,8 @@ module Rigor
|
|
|
209
245
|
end
|
|
210
246
|
tables
|
|
211
247
|
rescue StandardError
|
|
212
|
-
# Discovery is best-effort; a malformed corner of the project must not
|
|
213
|
-
#
|
|
214
|
-
# resolves fewer call sites.
|
|
248
|
+
# Discovery is best-effort; a malformed corner of the project must not crash the
|
|
249
|
+
# protection scan. Without discovery the collector simply resolves fewer call sites.
|
|
215
250
|
{}
|
|
216
251
|
end
|
|
217
252
|
|
|
@@ -240,35 +275,41 @@ module Rigor
|
|
|
240
275
|
return if callee.nil?
|
|
241
276
|
|
|
242
277
|
class_name, method, kind, def_node = callee
|
|
243
|
-
requireds =
|
|
244
|
-
|
|
278
|
+
requireds = leading_requireds(def_node)
|
|
279
|
+
# Enough positional arguments to fill every leading required (extra args map to trailing
|
|
280
|
+
# optional / rest parameters, which this pass does not infer). Fewer would be an arity error
|
|
281
|
+
# or a mis-resolved callee; skip rather than mis-attribute.
|
|
282
|
+
return if requireds.nil? || args.size < requireds.size
|
|
245
283
|
|
|
246
284
|
key = [class_name, method, kind]
|
|
247
|
-
|
|
248
|
-
arg_scope = index[
|
|
249
|
-
accumulate(key,
|
|
285
|
+
requireds.each_with_index do |param, i|
|
|
286
|
+
arg_scope = index[args[i]]
|
|
287
|
+
accumulate(key, param.name, arg_scope&.type_of(args[i]))
|
|
250
288
|
end
|
|
251
289
|
end
|
|
252
290
|
|
|
253
|
-
# The plain positional arguments
|
|
254
|
-
#
|
|
255
|
-
#
|
|
256
|
-
#
|
|
291
|
+
# The plain leading positional arguments. A trailing keyword hash (`f(x, k: 1)`) or block-pass
|
|
292
|
+
# (`f(x, &blk)`) is dropped — it occupies no positional slot, so the leading args still map to
|
|
293
|
+
# the leading required parameters. Returns nil when the call carries a splat (`f(*xs, x)`) or
|
|
294
|
+
# forwarding (`f(...)`) argument: those make an argument's position depend on runtime length,
|
|
295
|
+
# breaking the positional-index ↔ parameter mapping, so the call site is skipped.
|
|
257
296
|
def positional_args(call_node)
|
|
258
297
|
arguments = call_node.arguments
|
|
259
298
|
return [] if arguments.nil?
|
|
260
299
|
|
|
261
300
|
list = arguments.arguments
|
|
262
|
-
return nil if list.any? { |arg|
|
|
301
|
+
return nil if list.any? { |arg| position_breaking_argument?(arg) }
|
|
302
|
+
|
|
303
|
+
list.reject { |arg| non_positional_argument?(arg) }
|
|
304
|
+
end
|
|
263
305
|
|
|
264
|
-
|
|
306
|
+
def position_breaking_argument?(arg)
|
|
307
|
+
arg.is_a?(Prism::SplatNode) || arg.is_a?(Prism::ForwardingArgumentsNode)
|
|
265
308
|
end
|
|
266
309
|
|
|
267
|
-
def
|
|
268
|
-
arg.is_a?(Prism::
|
|
269
|
-
arg.is_a?(Prism::KeywordHashNode) ||
|
|
310
|
+
def non_positional_argument?(arg)
|
|
311
|
+
arg.is_a?(Prism::KeywordHashNode) ||
|
|
270
312
|
arg.is_a?(Prism::BlockArgumentNode) ||
|
|
271
|
-
arg.is_a?(Prism::ForwardingArgumentsNode) ||
|
|
272
313
|
arg.is_a?(Prism::AssocNode) ||
|
|
273
314
|
arg.is_a?(Prism::AssocSplatNode)
|
|
274
315
|
end
|
|
@@ -307,16 +348,19 @@ module Rigor
|
|
|
307
348
|
per_class && per_class[method]
|
|
308
349
|
end
|
|
309
350
|
|
|
310
|
-
# The required-positional parameters
|
|
311
|
-
#
|
|
312
|
-
#
|
|
313
|
-
#
|
|
314
|
-
def
|
|
351
|
+
# The leading required-positional parameters — the ones a call's leading positional arguments
|
|
352
|
+
# map to. Ruby orders parameters requireds-first, so optional / rest / keyword / block
|
|
353
|
+
# parameters that FOLLOW the requireds do not disturb the leading positional-index ↔
|
|
354
|
+
# required-parameter mapping; this covers the common `def f(x, opts = {}, **kw)` /
|
|
355
|
+
# `def f(x, y, &block)` shapes the earlier all-required contract skipped entirely. Returns nil
|
|
356
|
+
# only when a required slot is a destructured `(a, b)` (no bindable name) or a post-rest
|
|
357
|
+
# required (`def f(a, *b, c)` — `c` maps to a trailing arg, breaking the leading-prefix
|
|
358
|
+
# assumption for the whole call), which would mis-attribute arguments.
|
|
359
|
+
def leading_requireds(def_node)
|
|
315
360
|
params = def_node.parameters
|
|
316
361
|
return [] if params.nil?
|
|
317
362
|
return nil unless params.is_a?(Prism::ParametersNode)
|
|
318
|
-
return nil unless params.
|
|
319
|
-
params.keywords.empty? && params.keyword_rest.nil? && params.block.nil?
|
|
363
|
+
return nil unless params.posts.empty?
|
|
320
364
|
return nil unless params.requireds.all?(Prism::RequiredParameterNode)
|
|
321
365
|
|
|
322
366
|
params.requireds
|
|
@@ -335,26 +379,27 @@ module Rigor
|
|
|
335
379
|
end
|
|
336
380
|
end
|
|
337
381
|
|
|
338
|
-
# A poisoned parameter is dropped from the observation store and recorded
|
|
339
|
-
#
|
|
340
|
-
# param]` pair.
|
|
382
|
+
# A poisoned parameter is dropped from the observation store and recorded so later call
|
|
383
|
+
# sites short-circuit. `id` is the `[[class, method, kind], param]` pair.
|
|
341
384
|
def poison(id)
|
|
342
385
|
@poisoned_params << id
|
|
343
386
|
@type_observations.delete(id)
|
|
344
387
|
end
|
|
345
388
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
#
|
|
389
|
+
# Builds the round's frozen `[class, method, kind] => {param => Type}` table from the merged
|
|
390
|
+
# observations and poisoned set.
|
|
391
|
+
def finalize(merged_observations, poisoned)
|
|
392
|
+
# `result` is a default-block Hash (not a `{}` literal) so the analyzer types its reads
|
|
393
|
+
# generically rather than folding the empty shape — the nesting writes stay plain
|
|
394
|
+
# assignments, no literal-fold conditions.
|
|
350
395
|
result = Hash.new { |hash, key| hash[key] = {} }
|
|
351
|
-
|
|
352
|
-
next if
|
|
396
|
+
merged_observations.each do |id, observations|
|
|
397
|
+
next if poisoned.include?(id)
|
|
353
398
|
next if observations.empty?
|
|
354
399
|
|
|
355
400
|
union = Type::Combinator.union(*observations)
|
|
356
|
-
# A union that collapsed to a non-concrete shape (e.g. a gradual arm
|
|
357
|
-
#
|
|
401
|
+
# A union that collapsed to a non-concrete shape (e.g. a gradual arm leaked in) is no
|
|
402
|
+
# better than `untyped`; drop it.
|
|
358
403
|
next if ParameterArgTypes.non_concrete?(union)
|
|
359
404
|
|
|
360
405
|
key, param = id
|
|
@@ -6,15 +6,13 @@ require_relative "scope_indexer"
|
|
|
6
6
|
|
|
7
7
|
module Rigor
|
|
8
8
|
module Inference
|
|
9
|
-
# Measures the *type quality* of inferred expressions — not whether the
|
|
10
|
-
#
|
|
11
|
-
#
|
|
9
|
+
# Measures the *type quality* of inferred expressions — not whether the engine recognises an AST node
|
|
10
|
+
# class (that is `CoverageScanner`'s job), but whether the type it produces carries useful static
|
|
11
|
+
# information.
|
|
12
12
|
#
|
|
13
|
-
# Each visited *expression* node is classified into one of eight
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# wrappers, clause headers — are skipped; see
|
|
17
|
-
# {NON_EXPRESSION_NODE_TYPES}):
|
|
13
|
+
# Each visited *expression* node is classified into one of eight precision tiers (non-expression syntax
|
|
14
|
+
# nodes — argument / parameter lists, parameter declarations, hash pairs, statement wrappers, clause
|
|
15
|
+
# headers — are skipped; see {NON_EXPRESSION_NODE_TYPES}):
|
|
18
16
|
#
|
|
19
17
|
# :constant — Constant[T]: literal value known exactly
|
|
20
18
|
# :nominal — Nominal/Singleton: class identity known
|
|
@@ -25,42 +23,35 @@ module Rigor
|
|
|
25
23
|
# :dynamic_top — Dynamic[Top]: completely opaque (the "untyped" hole)
|
|
26
24
|
# :top — Top: universal supertype (no information)
|
|
27
25
|
#
|
|
28
|
-
# The summary exposes `precision_ratio` (constant+nominal+shaped+refined+bot
|
|
29
|
-
#
|
|
26
|
+
# The summary exposes `precision_ratio` (constant+nominal+shaped+refined+bot over total) and
|
|
27
|
+
# `opaque_ratio` (dynamic_top+top over total).
|
|
30
28
|
#
|
|
31
|
-
# For Union types the *worst* member tier is used, since the union is only
|
|
32
|
-
#
|
|
33
|
-
#
|
|
29
|
+
# For Union types the *worst* member tier is used, since the union is only as precise as its
|
|
30
|
+
# least-precise constituent. Intersection uses the *best* member (the most specific side wins).
|
|
31
|
+
# Difference follows its base type.
|
|
34
32
|
class PrecisionScanner
|
|
35
33
|
TIERS = %i[
|
|
36
34
|
constant nominal shaped refined bot
|
|
37
35
|
dynamic_specific dynamic_top top
|
|
38
36
|
].freeze
|
|
39
37
|
|
|
40
|
-
# Prism node classes that do not denote a value-producing
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
# points below the true expression-level precision. We exclude
|
|
47
|
-
# them from BOTH numerator and denominator so the ratio measures
|
|
48
|
-
# what it claims to — the type quality of actual expressions.
|
|
38
|
+
# Prism node classes that do not denote a value-producing expression, so typing them is meaningless —
|
|
39
|
+
# they have no runtime value to carry a type. Counting them (they always fall to the `dynamic_top`
|
|
40
|
+
# fallback) silently diluted the precision ratio: on a real survey target (shugo/textbringer) they
|
|
41
|
+
# were ~49% of every "opaque" node, dragging the headline number ~13 points below the true
|
|
42
|
+
# expression-level precision. We exclude them from BOTH numerator and denominator so the ratio
|
|
43
|
+
# measures what it claims to — the type quality of actual expressions.
|
|
49
44
|
#
|
|
50
|
-
# The set is deliberately CONSERVATIVE: only nodes that are
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
# and
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
# clause-header nodes whose body, not the header, carries the
|
|
58
|
-
# value. Anything that *could* be a value expression (`BlockNode`,
|
|
59
|
-
# `BeginNode`, `ImplicitNode`, `ParenthesesNode`, splats, …) is
|
|
60
|
-
# left in so a genuine inference gap stays visible.
|
|
45
|
+
# The set is deliberately CONSERVATIVE: only nodes that are unambiguously non-expressions in Ruby's
|
|
46
|
+
# grammar are listed — argument / parameter list containers and the parameter declarations inside
|
|
47
|
+
# them; the `key => value` pair node (its key and value are themselves walked and counted); the
|
|
48
|
+
# program / statements sequence wrappers (their value is the last child, already counted — listing
|
|
49
|
+
# them avoids double-counting); and the clause-header nodes whose body, not the header, carries the
|
|
50
|
+
# value. Anything that *could* be a value expression (`BlockNode`, `BeginNode`, `ImplicitNode`,
|
|
51
|
+
# `ParenthesesNode`, splats, …) is left in so a genuine inference gap stays visible.
|
|
61
52
|
#
|
|
62
|
-
# Compared by class NAME so a Prism version that lacks one of the
|
|
63
|
-
#
|
|
53
|
+
# Compared by class NAME so a Prism version that lacks one of the newer node classes does not break
|
|
54
|
+
# loading.
|
|
64
55
|
NON_EXPRESSION_NODE_TYPES = %w[
|
|
65
56
|
Prism::ProgramNode
|
|
66
57
|
Prism::StatementsNode
|
|
@@ -2,33 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
module Rigor
|
|
4
4
|
module Inference
|
|
5
|
-
# ADR-17 § "Inference contract" — project-wide patched-method
|
|
6
|
-
#
|
|
7
|
-
# the user's `.rigor.yml` `pre_eval:` list.
|
|
5
|
+
# ADR-17 § "Inference contract" — project-wide patched-method registry populated by the pre-eval
|
|
6
|
+
# pre-pass (slice 2) from the user's `.rigor.yml` `pre_eval:` list.
|
|
8
7
|
#
|
|
9
|
-
# Each entry records one `def` declaration the pre-pass
|
|
10
|
-
#
|
|
11
|
-
# `
|
|
12
|
-
#
|
|
13
|
-
# project-side `lib/core_ext/string_extensions.rb` patches
|
|
14
|
-
# are visible to cross-file dispatch.
|
|
8
|
+
# Each entry records one `def` declaration the pre-pass observed inside a class / module body. The
|
|
9
|
+
# dispatcher's `try_project_patched_method` tier consults this registry between the plugin tier and
|
|
10
|
+
# the dependency-source tier so project-side `lib/core_ext/string_extensions.rb` patches are visible
|
|
11
|
+
# to cross-file dispatch.
|
|
15
12
|
#
|
|
16
|
-
# The dispatcher answers `Dynamic[T]` (with a heuristic static
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# per-field contract.
|
|
13
|
+
# The dispatcher answers `Dynamic[T]` (with a heuristic static facet) when `Entry#return_type` is
|
|
14
|
+
# non-nil, or `Dynamic[Top]` when the heuristic declined (`nil`). See {Entry} for the per-field
|
|
15
|
+
# contract.
|
|
20
16
|
class ProjectPatchedMethods
|
|
21
|
-
# Frozen value-object recording one `def` observed by the
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# `
|
|
26
|
-
#
|
|
27
|
-
# {Analysis::DependencySourceInference::ReturnTypeHeuristic}-
|
|
28
|
-
# extracted static facet (a `Rigor::Type::*`) or `nil`
|
|
29
|
-
# when the heuristic declined. The dispatcher wraps a
|
|
30
|
-
# non-nil `return_type` in `Dynamic[T]`; a `nil`
|
|
31
|
-
# `return_type` falls back to `Dynamic[top]`.
|
|
17
|
+
# Frozen value-object recording one `def` observed by the pre-pass. `class_name` is the qualified
|
|
18
|
+
# prefix (`"String"`, `"Foo::Bar"`); `method_name` is the declared name; `kind` is `:instance` or
|
|
19
|
+
# `:singleton`; `source_path` / `source_line` carry attribution for diagnostics; `return_type` is
|
|
20
|
+
# the {Analysis::DependencySourceInference::ReturnTypeHeuristic}-extracted static facet (a
|
|
21
|
+
# `Rigor::Type::*`) or `nil` when the heuristic declined. The dispatcher wraps a non-nil
|
|
22
|
+
# `return_type` in `Dynamic[T]`; a `nil` `return_type` falls back to `Dynamic[top]`.
|
|
32
23
|
Entry = Data.define(:class_name, :method_name, :kind, :source_path, :source_line, :return_type) do
|
|
33
24
|
def initialize(class_name:, method_name:, kind:, source_path:, source_line:, return_type: nil)
|
|
34
25
|
super
|
|
@@ -37,11 +28,9 @@ module Rigor
|
|
|
37
28
|
|
|
38
29
|
attr_reader :by_key
|
|
39
30
|
|
|
40
|
-
# @param entries [Array<Entry>] flat list of declarations
|
|
41
|
-
#
|
|
42
|
-
# `
|
|
43
|
-
# `pre-eval.duplicate-declaration` diagnostic emission
|
|
44
|
-
# stays decoupled from registry behaviour.
|
|
31
|
+
# @param entries [Array<Entry>] flat list of declarations observed during the pre-pass.
|
|
32
|
+
# First-write-wins on `(class_name, method_name, kind)` duplicates so the
|
|
33
|
+
# `pre-eval.duplicate-declaration` diagnostic emission stays decoupled from registry behaviour.
|
|
45
34
|
def initialize(entries: [])
|
|
46
35
|
@by_key = entries.each_with_object({}) do |entry, acc|
|
|
47
36
|
key = [entry.class_name, entry.method_name, entry.kind]
|
|
@@ -50,9 +39,8 @@ module Rigor
|
|
|
50
39
|
freeze
|
|
51
40
|
end
|
|
52
41
|
|
|
53
|
-
# @return [Entry, nil] the recorded entry for the given
|
|
54
|
-
# `
|
|
55
|
-
# when no pre-eval file declared it.
|
|
42
|
+
# @return [Entry, nil] the recorded entry for the given `(class_name, method_name, kind)` triple,
|
|
43
|
+
# or `nil` when no pre-eval file declared it.
|
|
56
44
|
def lookup(class_name:, method_name:, kind:)
|
|
57
45
|
@by_key[[class_name, method_name, kind]]
|
|
58
46
|
end
|