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
|
@@ -2,27 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
module Rigor
|
|
4
4
|
module Plugin
|
|
5
|
-
# ADR-32 WD6 — per-run accumulator for failures encountered by
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# `#entries` after analysis and emits one
|
|
11
|
-
# `source-rbs-synthesis-failed` `:info` diagnostic per
|
|
12
|
-
# entry so the user sees which files contributed nothing
|
|
13
|
-
# and why.
|
|
5
|
+
# ADR-32 WD6 — per-run accumulator for failures encountered by a plugin's
|
|
6
|
+
# `Manifest#source_rbs_synthesizer` callable. The synthesizer returns `[:error, message]` on parse failure
|
|
7
|
+
# (per its contract); `Environment.for_project` routes the tuple through `#record` here.
|
|
8
|
+
# `Analysis::Runner` queries `#entries` after analysis and emits one `source-rbs-synthesis-failed` `:info`
|
|
9
|
+
# diagnostic per entry so the user sees which files contributed nothing and why.
|
|
14
10
|
#
|
|
15
|
-
# Empty by default. The Runner only emits diagnostics when
|
|
16
|
-
#
|
|
17
|
-
# synthesizer-emitting plugins pay zero cost.
|
|
11
|
+
# Empty by default. The Runner only emits diagnostics when at least one entry is recorded — projects
|
|
12
|
+
# without synthesizer-emitting plugins pay zero cost.
|
|
18
13
|
#
|
|
19
|
-
# Thread-/Ractor-safety: this reporter is per-`WorkerSession`
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
# across the run; entries are appended one at a time during
|
|
24
|
-
# env build (before any per-file analysis runs), so no
|
|
25
|
-
# locking is needed.
|
|
14
|
+
# Thread-/Ractor-safety: this reporter is per-`WorkerSession` in pool mode, so concurrent writes from one
|
|
15
|
+
# Ractor's `collect_virtual_rbs` loop are serialised by the worker body itself. The sequential path shares a
|
|
16
|
+
# single reporter across the run; entries are appended one at a time during env build (before any per-file
|
|
17
|
+
# analysis runs), so no locking is needed.
|
|
26
18
|
class SourceRbsSynthesisReporter
|
|
27
19
|
Entry = Data.define(:plugin_id, :path, :message)
|
|
28
20
|
|
|
@@ -2,42 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
module Rigor
|
|
4
4
|
module Plugin
|
|
5
|
-
# Declarative trust / I/O policy for the active plugin set.
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# programmatic surface that documents that trust and lets the
|
|
10
|
-
# analyzer enforce read scope + network disablement at the
|
|
5
|
+
# Declarative trust / I/O policy for the active plugin set. Pinned by
|
|
6
|
+
# [ADR-2 § "Plugin Trust and I/O Policy"](../../../docs/adr/2-extension-api.md): plugins are *trusted Ruby
|
|
7
|
+
# gems selected by the user, their Gemfile, or project configuration*; this class is the programmatic
|
|
8
|
+
# surface that documents that trust and lets the analyzer enforce read scope + network disablement at the
|
|
11
9
|
# documented edges.
|
|
12
10
|
#
|
|
13
|
-
# The policy is **not a sandbox.** A plugin that uses raw
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# boundary checks against this policy and feeds compliant reads
|
|
19
|
-
# into the cache descriptor for invalidation. Slices 3-6 wire
|
|
20
|
-
# plugin contributions through the boundary so the policy is
|
|
21
|
-
# the actual mechanism, not just paperwork.
|
|
11
|
+
# The policy is **not a sandbox.** A plugin that uses raw `File.read` or `Net::HTTP` bypasses the policy —
|
|
12
|
+
# ADR-2 explicitly chooses documentation over forced isolation. The contract is: when plugins go through
|
|
13
|
+
# {Rigor::Plugin::IoBoundary} (the analyzer-side helper service slice 2 introduces), the boundary checks
|
|
14
|
+
# against this policy and feeds compliant reads into the cache descriptor for invalidation. Slices 3-6 wire
|
|
15
|
+
# plugin contributions through the boundary so the policy is the actual mechanism, not just paperwork.
|
|
22
16
|
#
|
|
23
17
|
# ## Fields
|
|
24
18
|
#
|
|
25
|
-
# - `trusted_gems`: gem names the user has authorised. Derived
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
# `
|
|
33
|
-
# `
|
|
34
|
-
#
|
|
35
|
-
# - `network_policy`: one of {VALID_NETWORK_POLICIES}.
|
|
36
|
-
# `:disabled` (default) makes {IoBoundary#open_url} always
|
|
37
|
-
# raise. `:allowlist` (v0.1.2) consults `allowed_url_hosts`
|
|
38
|
-
# on every fetch — the hostname must be on the list and
|
|
39
|
-
# the URL scheme MUST be `https`. The list of allowed hosts
|
|
40
|
-
# is exact-match (no wildcards in v0.1.2).
|
|
19
|
+
# - `trusted_gems`: gem names the user has authorised. Derived from the `plugins:` section of `.rigor.yml`
|
|
20
|
+
# plus any gems they reach transitively. Used today for documentation and future trust diagnostics.
|
|
21
|
+
# - `allowed_read_roots`: absolute paths plugin code may read from through the {IoBoundary}. The default
|
|
22
|
+
# set covers the project root, the project's `signature_paths`, the active `Gemfile.lock`, and each
|
|
23
|
+
# trusted gem's `Gem::Specification#full_gem_path`. The user extends this with `.rigor.yml`'s
|
|
24
|
+
# `plugins_io.allowed_paths:`.
|
|
25
|
+
# - `network_policy`: one of {VALID_NETWORK_POLICIES}. `:disabled` (default) makes
|
|
26
|
+
# {IoBoundary#open_url} always raise. `:allowlist` (v0.1.2) consults `allowed_url_hosts` on every fetch —
|
|
27
|
+
# the hostname must be on the list and the URL scheme MUST be `https`. The list of allowed hosts is
|
|
28
|
+
# exact-match (no wildcards in v0.1.2).
|
|
41
29
|
class TrustPolicy
|
|
42
30
|
VALID_NETWORK_POLICIES = %i[disabled allowlist].freeze
|
|
43
31
|
|
|
@@ -58,10 +46,9 @@ module Rigor
|
|
|
58
46
|
end
|
|
59
47
|
|
|
60
48
|
# @param path [String]
|
|
61
|
-
# @return [Boolean] true when the absolute path falls inside
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
# adversarial intent are out of scope per ADR-2.
|
|
49
|
+
# @return [Boolean] true when the absolute path falls inside any allowed read root. Symlinks are
|
|
50
|
+
# resolved through `File.expand_path` only (no `realpath`); plugins with adversarial intent are out of
|
|
51
|
+
# scope per ADR-2.
|
|
65
52
|
def allow_read?(path)
|
|
66
53
|
absolute = File.expand_path(path.to_s)
|
|
67
54
|
@allowed_read_roots.any? { |root| inside?(absolute, root) }
|
|
@@ -72,9 +59,8 @@ module Rigor
|
|
|
72
59
|
end
|
|
73
60
|
|
|
74
61
|
# @param url [String, URI]
|
|
75
|
-
# @return [Boolean] true when the URL scheme is `https` and
|
|
76
|
-
#
|
|
77
|
-
# `false` while `network_policy` is `:disabled`.
|
|
62
|
+
# @return [Boolean] true when the URL scheme is `https` and the parsed hostname is in
|
|
63
|
+
# `allowed_url_hosts`. Always `false` while `network_policy` is `:disabled`.
|
|
78
64
|
def allow_url?(url)
|
|
79
65
|
return false if @network_policy == :disabled
|
|
80
66
|
return false if @allowed_url_hosts.empty?
|
|
@@ -2,18 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module Rigor
|
|
4
4
|
module Plugin
|
|
5
|
-
# Plugin-supplied resolver for custom named / generic type
|
|
6
|
-
#
|
|
5
|
+
# Plugin-supplied resolver for custom named / generic type vocabulary in RBS::Extended payloads. ADR-13 §
|
|
6
|
+
# "Decision".
|
|
7
7
|
#
|
|
8
|
-
# Subclasses override {#resolve} to return a
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# resolver in the chain (and finally to the built-in / RBS
|
|
12
|
-
# fallback). The base implementation returns `nil` so an
|
|
13
|
-
# unimplemented subclass is a safe no-op.
|
|
8
|
+
# Subclasses override {#resolve} to return a {Rigor::Type::Base} when the node matches the vocabulary the
|
|
9
|
+
# resolver covers, or `nil` to fall through to the next resolver in the chain (and finally to the built-in /
|
|
10
|
+
# RBS fallback). The base implementation returns `nil` so an unimplemented subclass is a safe no-op.
|
|
14
11
|
#
|
|
15
|
-
# Resolvers are registered through their plugin's manifest
|
|
16
|
-
# under the `type_node_resolvers:` slot:
|
|
12
|
+
# Resolvers are registered through their plugin's manifest under the `type_node_resolvers:` slot:
|
|
17
13
|
#
|
|
18
14
|
# class RigorTypescriptUtilityTypes < Rigor::Plugin::Base
|
|
19
15
|
# manifest(
|
|
@@ -24,24 +20,19 @@ module Rigor
|
|
|
24
20
|
# )
|
|
25
21
|
# end
|
|
26
22
|
#
|
|
27
|
-
# ADR-13 — base class, manifest hook, and registry aggregation
|
|
28
|
-
# for
|
|
29
|
-
#
|
|
30
|
-
# `%a{rigor:v1:...}` payload through `TypeNode::ResolverChain`
|
|
31
|
-
# (built by `Environment#build_name_scope` from
|
|
23
|
+
# ADR-13 — base class, manifest hook, and registry aggregation for plugin-contributed type-node resolvers.
|
|
24
|
+
# Resolvers declared via `manifest(type_node_resolvers:)` run for every real `%a{rigor:v1:...}` payload
|
|
25
|
+
# through `TypeNode::ResolverChain` (built by `Environment#build_name_scope` from
|
|
32
26
|
# `Plugin::Registry#type_node_resolvers`).
|
|
33
27
|
#
|
|
34
|
-
# Resolvers SHOULD be stateless and re-entrant; the registry
|
|
35
|
-
#
|
|
36
|
-
# consult any resolver multiple times for the same node.
|
|
28
|
+
# Resolvers SHOULD be stateless and re-entrant; the registry builds the chain once per
|
|
29
|
+
# `Analysis::Runner.run` and may consult any resolver multiple times for the same node.
|
|
37
30
|
class TypeNodeResolver
|
|
38
|
-
# @param node [Rigor::TypeNode::Identifier, Rigor::TypeNode::Generic]
|
|
39
|
-
#
|
|
40
|
-
# @param scope [Rigor::TypeNode::NameScope] companion
|
|
41
|
-
# value object (slice 3); slice 2 invocations MAY pass
|
|
31
|
+
# @param node [Rigor::TypeNode::Identifier, Rigor::TypeNode::Generic] the parser-emitted node the chain is
|
|
32
|
+
# asking about.
|
|
33
|
+
# @param scope [Rigor::TypeNode::NameScope] companion value object (slice 3); slice 2 invocations MAY pass
|
|
42
34
|
# `nil` because the chain doesn't exist yet.
|
|
43
|
-
# @return [Rigor::Type::Base, nil] resolved type, or `nil`
|
|
44
|
-
# to fall through.
|
|
35
|
+
# @return [Rigor::Type::Base, nil] resolved type, or `nil` to fall through.
|
|
45
36
|
def resolve(node, scope) # rubocop:disable Lint/UnusedMethodArgument
|
|
46
37
|
nil
|
|
47
38
|
end
|
|
@@ -4,18 +4,16 @@ require_relative "../analysis/runner"
|
|
|
4
4
|
|
|
5
5
|
module Rigor
|
|
6
6
|
module Protection
|
|
7
|
-
# ADR-69 Seam 1 — the **kill oracle** Rigor's analyzer-teeth measurement uses:
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# scanner baking in either assumption.
|
|
7
|
+
# ADR-69 Seam 1 — the **kill oracle** Rigor's analyzer-teeth measurement uses: a mutant is killed iff
|
|
8
|
+
# re-analysing it introduces a diagnostic absent from the clean baseline. This is exactly the behaviour
|
|
9
|
+
# ADR-62/63 shipped, lifted out of {MutationScanner} so a {TestSuiteOracle} (ADR-70) — which kills by
|
|
10
|
+
# *running tests* rather than by re-analysis — can sit beside it without the scanner baking in either
|
|
11
|
+
# assumption.
|
|
13
12
|
#
|
|
14
|
-
# The expensive builds (RBS environment + the whole-project pre-pass scan) are
|
|
15
|
-
#
|
|
16
|
-
# `
|
|
17
|
-
#
|
|
18
|
-
# *disk* file), so a mutant is never served a stale clean hit.
|
|
13
|
+
# The expensive builds (RBS environment + the whole-project pre-pass scan) are paid once by the caller and
|
|
14
|
+
# threaded in; each mutant reuses them through `Runner.new(prebuilt:)#run_source` (in-memory overlay, no
|
|
15
|
+
# disk write). Passing `prebuilt:` disables the run-result cache (whose key digests the *disk* file), so a
|
|
16
|
+
# mutant is never served a stale clean hit.
|
|
19
17
|
class DiagnosticOracle
|
|
20
18
|
def initialize(configuration:, environment:, project_scan:)
|
|
21
19
|
@configuration = configuration
|
|
@@ -23,8 +21,8 @@ module Rigor
|
|
|
23
21
|
@project_scan = project_scan
|
|
24
22
|
end
|
|
25
23
|
|
|
26
|
-
# The clean per-file baseline: the diagnostic signatures a mutant must add
|
|
27
|
-
#
|
|
24
|
+
# The clean per-file baseline: the diagnostic signatures a mutant must add to count as killed. Computed
|
|
25
|
+
# once per file by the caller.
|
|
28
26
|
def baseline(source:, path:)
|
|
29
27
|
analyse(source, path).to_set { |d| sig(d) }
|
|
30
28
|
end
|
|
@@ -7,24 +7,20 @@ require_relative "diagnostic_oracle"
|
|
|
7
7
|
|
|
8
8
|
module Rigor
|
|
9
9
|
module Protection
|
|
10
|
-
# ADR-63 Tier 2 — the mutation *effectiveness* tier (the truth tier behind
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# introduced at a dispatch site, does Rigor actually catch it?
|
|
10
|
+
# ADR-63 Tier 2 — the mutation *effectiveness* tier (the truth tier behind Tier 1's static
|
|
11
|
+
# {Inference::ProtectionScanner} proxy). For one file it answers the question Tier 1 only bounds: when a
|
|
12
|
+
# type-visible bug is introduced at a dispatch site, does Rigor actually catch it?
|
|
14
13
|
#
|
|
15
|
-
# Mechanism (the ADR-62 warm loop, narrowed to per-file measurement):
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
# a kill); {#scan_file_fused} additionally consults a {TestSuiteOracle} on the
|
|
22
|
-
# type-survivors (ADR-70 — the dynamic protection axis).
|
|
14
|
+
# Mechanism (the ADR-62 warm loop, narrowed to per-file measurement): generate the type-visible mutations
|
|
15
|
+
# ({Mutator}), keep only those whose receiver Rigor holds a concrete type for (the type-aware filter — the
|
|
16
|
+
# FP-safe meaning-maker; an unresolved receiver is kept), then for each ask the **kill oracle** whether the
|
|
17
|
+
# mutant is caught. The oracle is the ADR-69 seam: {#scan_file} uses the {DiagnosticOracle} (a *new Rigor
|
|
18
|
+
# diagnostic* = a kill); {#scan_file_fused} additionally consults a {TestSuiteOracle} on the type-survivors
|
|
19
|
+
# (ADR-70 — the dynamic protection axis).
|
|
23
20
|
#
|
|
24
|
-
# The expensive builds (RBS environment + the whole-project pre-pass scan)
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
# overlay, no disk write).
|
|
21
|
+
# The expensive builds (RBS environment + the whole-project pre-pass scan) are paid ONCE by the caller and
|
|
22
|
+
# threaded into the {DiagnosticOracle}; each mutant reuses them through `Runner.new(prebuilt:)#run_source`
|
|
23
|
+
# (in-memory overlay, no disk write).
|
|
28
24
|
class MutationScanner
|
|
29
25
|
# A surviving mutation site — a breakage Rigor did not catch.
|
|
30
26
|
SurvivingSite = Data.define(:line, :receiver, :method_name, :operator)
|
|
@@ -33,21 +29,19 @@ module Rigor
|
|
|
33
29
|
# Mutations actually analysed (parse-invalid mutants are not counted).
|
|
34
30
|
def total = killed + survived
|
|
35
31
|
|
|
36
|
-
# Effectiveness ratio; a file with no type-relevant mutation is
|
|
37
|
-
#
|
|
32
|
+
# Effectiveness ratio; a file with no type-relevant mutation is vacuously fully effective (no breakage
|
|
33
|
+
# was available to miss).
|
|
38
34
|
def ratio = total.zero? ? 1.0 : killed.to_f / total
|
|
39
35
|
end
|
|
40
36
|
|
|
41
|
-
# ADR-70 — one type-survivor classified by the dynamic (test) axis.
|
|
42
|
-
#
|
|
43
|
-
# "add a type OR a test here" sites).
|
|
37
|
+
# ADR-70 — one type-survivor classified by the dynamic (test) axis. `protection` is `:test` (a test caught
|
|
38
|
+
# it) or `:none` (unprotected — the "add a type OR a test here" sites).
|
|
44
39
|
FusedSite = Data.define(:line, :receiver, :method_name, :operator, :protection)
|
|
45
40
|
|
|
46
|
-
# ADR-70 — the per-file fused classification. The gradual short-circuit
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
# *would also* catch it is wasted work. So the observed buckets are three.
|
|
41
|
+
# ADR-70 — the per-file fused classification. The gradual short-circuit collapses the conceptual
|
|
42
|
+
# "doubly-protected" bucket into `type_killed`: a mutant the type checker already kills never reaches the
|
|
43
|
+
# suite, because the static net already suffices and re-running the suite to learn a test *would also*
|
|
44
|
+
# catch it is wasted work. So the observed buckets are three.
|
|
51
45
|
FusedFileResult = Data.define(:path, :type_killed, :test_killed, :sites) do
|
|
52
46
|
# The unprotected sites (neither a type nor a test caught the breakage).
|
|
53
47
|
def unprotected = sites.size
|
|
@@ -101,11 +95,10 @@ module Rigor
|
|
|
101
95
|
FileResult.new(path: path, killed: killed, survived: sites.size, sites: sites)
|
|
102
96
|
end
|
|
103
97
|
|
|
104
|
-
# ADR-70 — the fused static∪dynamic measurement. Runs the type pass
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
# short-circuit), so the cost is proportional to the protection hole.
|
|
98
|
+
# ADR-70 — the fused static∪dynamic measurement. Runs the type pass (the {DiagnosticOracle}); for every
|
|
99
|
+
# mutant the type checker did **not** kill, asks `test_oracle` whether the project's test suite catches it.
|
|
100
|
+
# The expensive suite run is paid only for type-survivors (the gradual short-circuit), so the cost is
|
|
101
|
+
# proportional to the protection hole.
|
|
109
102
|
# @param test_oracle [TestSuiteOracle]
|
|
110
103
|
# @return [FusedFileResult]
|
|
111
104
|
def scan_file_fused(path, test_oracle:, source: nil)
|
|
@@ -134,10 +127,9 @@ module Rigor
|
|
|
134
127
|
|
|
135
128
|
private
|
|
136
129
|
|
|
137
|
-
# The mutations to measure: the biteable filter (concrete-type sites only;
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
# receivers. Optionally sampled.
|
|
130
|
+
# The mutations to measure: the biteable filter (concrete-type sites only; the FP-safe default — an
|
|
131
|
+
# unresolved receiver is kept) or, under the `:all` selector (ADR-69 Seam 2), every dispatch site including
|
|
132
|
+
# Dynamic receivers. Optionally sampled.
|
|
141
133
|
def kept_mutations(source, path)
|
|
142
134
|
mutator = Mutator.new(source)
|
|
143
135
|
muts = mutator.mutations
|
|
@@ -6,19 +6,15 @@ require_relative "../scope"
|
|
|
6
6
|
require_relative "../inference/scope_indexer"
|
|
7
7
|
|
|
8
8
|
module Rigor
|
|
9
|
-
# ADR-63 Tier 2 — the productized subset of the dev-only mutation-testing
|
|
10
|
-
#
|
|
11
|
-
# measurement
|
|
12
|
-
#
|
|
13
|
-
# clustering stay off the frozen surface in `tool/mutation/mutate.rb`
|
|
14
|
-
# (which now reuses this {Mutator} so there is one source of truth).
|
|
9
|
+
# ADR-63 Tier 2 — the productized subset of the dev-only mutation-testing harness (`tool/mutation/`, ADR-62).
|
|
10
|
+
# Only the *per-file effectiveness measurement* lives here — the type-visible {Mutator} and the warm-loop
|
|
11
|
+
# {MutationScanner} kill-rate measurement. The dev sweep / fuzz / survivor clustering stay off the frozen
|
|
12
|
+
# surface in `tool/mutation/mutate.rb` (which now reuses this {Mutator} so there is one source of truth).
|
|
15
13
|
module Protection
|
|
16
|
-
# One concrete edit: replace source bytes [start, stop) with `replacement`.
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# `anchor_type` (the rendered receiver type) and `method_name` are filled in
|
|
21
|
-
# for reporting a surviving site; both may stay nil.
|
|
14
|
+
# One concrete edit: replace source bytes [start, stop) with `replacement`. `anchor` is the Prism node whose
|
|
15
|
+
# inferred type decides type-relevance — the call receiver whose contract the mutation could violate, or nil
|
|
16
|
+
# when there is no concrete receiver (implicit-self call, literal outside a call). `anchor_type` (the
|
|
17
|
+
# rendered receiver type) and `method_name` are filled in for reporting a surviving site; both may stay nil.
|
|
22
18
|
Mutation = Struct.new(
|
|
23
19
|
:operator, :expected_rule, :start, :stop, :replacement, :line, :label, :anchor,
|
|
24
20
|
:anchor_type, :method_name,
|
|
@@ -31,35 +27,29 @@ module Rigor
|
|
|
31
27
|
end
|
|
32
28
|
end
|
|
33
29
|
|
|
34
|
-
# Generates type-visible mutations of a Ruby source string by walking the
|
|
35
|
-
#
|
|
36
|
-
# hands us exact offsets, and the analyzer re-parses the spliced source).
|
|
30
|
+
# Generates type-visible mutations of a Ruby source string by walking the Prism AST and recording byte-range
|
|
31
|
+
# splices (no unparser needed — Prism hands us exact offsets, and the analyzer re-parses the spliced source).
|
|
37
32
|
#
|
|
38
|
-
# A mutation is "type-visible" when it should trip a diagnostic rule *if*
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
# bodies are mutated, never `def` signatures, so a reused project scan stays
|
|
43
|
-
# valid.
|
|
33
|
+
# A mutation is "type-visible" when it should trip a diagnostic rule *if* Rigor holds a type at the site: a
|
|
34
|
+
# call-argument literal dropped to `nil` or type-swapped (→ `call.argument-type-mismatch`), or a call site
|
|
35
|
+
# renamed to a missing method (→ `call.undefined-method`). Only call sites and bodies are mutated, never
|
|
36
|
+
# `def` signatures, so a reused project scan stays valid.
|
|
44
37
|
class Mutator
|
|
45
38
|
IDENT = /\A[a-z_][A-Za-z0-9_]*\z/
|
|
46
39
|
QUOTES = ['"', "'"].freeze
|
|
47
|
-
# Mutating an argument to a universal-equality method is always an
|
|
48
|
-
#
|
|
49
|
-
# type mismatch rather than raising, so the engine exempts them
|
|
40
|
+
# Mutating an argument to a universal-equality method is always an equivalent mutant: Ruby's `==` / `<=>`
|
|
41
|
+
# family returns false / nil on a type mismatch rather than raising, so the engine exempts them
|
|
50
42
|
# (`UNIVERSAL_EQUALITY_METHODS`). Skip them to keep survivors meaningful.
|
|
51
43
|
UNIVERSAL_EQUALITY = %w[== != eql? equal? <=>].freeze
|
|
52
44
|
|
|
53
|
-
# Every operator the mutator knows. Each maps to the diagnostic rule
|
|
54
|
-
#
|
|
55
|
-
# context where Rigor has type knowledge.
|
|
45
|
+
# Every operator the mutator knows. Each maps to the diagnostic rule family it is *engineered* to trip when
|
|
46
|
+
# the mutated value/call sits in a context where Rigor has type knowledge.
|
|
56
47
|
ALL_OPERATORS = %i[nil_inject type_swap undefined_method arity_extra].freeze
|
|
57
48
|
|
|
58
|
-
# The default set. `arity_extra` is excluded: most Ruby methods accept an
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
# guard would make it default-worthy — a follow-up.)
|
|
49
|
+
# The default set. `arity_extra` is excluded: most Ruby methods accept an extra argument (splat / optional),
|
|
50
|
+
# so appending one is usually an equivalent mutant — it contributes almost only noise. Re-enable it
|
|
51
|
+
# explicitly via `operators:` to measure arity teeth. (A signature-arity guard would make it
|
|
52
|
+
# default-worthy — a follow-up.)
|
|
63
53
|
OPERATORS = %i[nil_inject type_swap undefined_method].freeze
|
|
64
54
|
|
|
65
55
|
def initialize(source, operators: OPERATORS)
|
|
@@ -78,14 +68,12 @@ module Rigor
|
|
|
78
68
|
out
|
|
79
69
|
end
|
|
80
70
|
|
|
81
|
-
# Phase 1.5 — keep only mutations whose anchor types to a concrete,
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
# provably-Dynamic sites. Returns [kept, dropped_count]. Builds the scope
|
|
88
|
-
# index from THIS mutator's parse so anchor node identity matches the keys.
|
|
71
|
+
# Phase 1.5 — keep only mutations whose anchor types to a concrete, non-Dynamic type, i.e. a site where
|
|
72
|
+
# Rigor actually holds a contract the mutation could violate. Drops implicit-self calls and literals
|
|
73
|
+
# outside a typed call (no contract → guaranteed survival → noise). FP-safe direction: an
|
|
74
|
+
# unresolved/probe-failed type KEEPS the mutation, so the filter never hides a kill it is unsure about — it
|
|
75
|
+
# only removes provably-Dynamic sites. Returns [kept, dropped_count]. Builds the scope index from THIS
|
|
76
|
+
# mutator's parse so anchor node identity matches the keys.
|
|
89
77
|
def filter_by_type(mutations, environment:, path:)
|
|
90
78
|
base = Rigor::Scope.empty(environment: environment, source_path: path)
|
|
91
79
|
index = Rigor::Inference::ScopeIndexer.index(@parse.value, default_scope: base)
|
|
@@ -98,13 +86,11 @@ module Rigor
|
|
|
98
86
|
[kept, mutations.size - kept.size]
|
|
99
87
|
end
|
|
100
88
|
|
|
101
|
-
# ADR-69 Seam 2 (AllSites) — keep every *dispatch-site* mutation (a method
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
# consumer most wants to probe: where Rigor cannot bite, a test is the only
|
|
107
|
-
# protection. Use only with a {TestSuiteOracle} — at a Dynamic site the
|
|
89
|
+
# ADR-69 Seam 2 (AllSites) — keep every *dispatch-site* mutation (a method call or a call-argument
|
|
90
|
+
# literal), Dynamic receiver included, annotating the anchor type where Rigor holds one. Drops only
|
|
91
|
+
# non-dispatch literals (a literal outside any call — no receiver contract to violate). The biteable
|
|
92
|
+
# {#filter_by_type} hides exactly the Dynamic sites a test-suite consumer most wants to probe: where Rigor
|
|
93
|
+
# cannot bite, a test is the only protection. Use only with a {TestSuiteOracle} — at a Dynamic site the
|
|
108
94
|
# type pass can never kill, so without the test axis these are all noise.
|
|
109
95
|
def dispatch_site_mutations(mutations, environment:, path:)
|
|
110
96
|
base = Rigor::Scope.empty(environment: environment, source_path: path)
|
|
@@ -139,10 +125,9 @@ module Rigor
|
|
|
139
125
|
end
|
|
140
126
|
end
|
|
141
127
|
|
|
142
|
-
# Record, for each literal that is a direct call argument, the receiver of
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
# the type filter).
|
|
128
|
+
# Record, for each literal that is a direct call argument, the receiver of the enclosing call — the anchor
|
|
129
|
+
# whose param contract a literal mutation could violate. Literals elsewhere get a nil anchor (filtered out
|
|
130
|
+
# under the type filter).
|
|
146
131
|
def index_literal_anchors(node)
|
|
147
132
|
return if node.nil?
|
|
148
133
|
|
|
@@ -158,9 +143,8 @@ module Rigor
|
|
|
158
143
|
node.is_a?(Prism::IntegerNode) || node.is_a?(Prism::FloatNode) || node.is_a?(Prism::StringNode)
|
|
159
144
|
end
|
|
160
145
|
|
|
161
|
-
# Returns [keep?, rendered_type]. Keep when `anchor` is a site where Rigor
|
|
162
|
-
#
|
|
163
|
-
# probe-failed type keeps the mutation (with a nil rendered type).
|
|
146
|
+
# Returns [keep?, rendered_type]. Keep when `anchor` is a site where Rigor holds a concrete (non-Dynamic/Top)
|
|
147
|
+
# type. FP-safe: an unresolved or probe-failed type keeps the mutation (with a nil rendered type).
|
|
164
148
|
def anchor_decision(anchor, index, cache)
|
|
165
149
|
return [false, nil] if anchor.nil?
|
|
166
150
|
return cache[anchor] if cache.key?(anchor)
|
|
@@ -181,11 +165,10 @@ module Rigor
|
|
|
181
165
|
[true, nil] # never let a probe failure hide a candidate
|
|
182
166
|
end
|
|
183
167
|
|
|
184
|
-
# A receiver type Rigor cannot bite on, so a mutation anchored to it would
|
|
185
|
-
#
|
|
186
|
-
#
|
|
187
|
-
#
|
|
188
|
-
# undefined-method teeth.
|
|
168
|
+
# A receiver type Rigor cannot bite on, so a mutation anchored to it would survive as noise: `Dynamic` /
|
|
169
|
+
# `Top` / `bot`, or a union with any such arm (gradually valid — `Array | Dynamic[top]`.whatever never
|
|
170
|
+
# fires). A union of fully-concrete arms (`String | Symbol`) stays concrete — it now has undefined-method
|
|
171
|
+
# teeth.
|
|
189
172
|
def non_concrete_type?(type)
|
|
190
173
|
return true if type.is_a?(Rigor::Type::Dynamic) || type.is_a?(Rigor::Type::Top) ||
|
|
191
174
|
type.is_a?(Rigor::Type::Bot)
|
|
@@ -200,9 +183,8 @@ module Rigor
|
|
|
200
183
|
type.class.name
|
|
201
184
|
end
|
|
202
185
|
|
|
203
|
-
# Mutate a literal: drop it to nil (possible-nil channel) and swap its
|
|
204
|
-
#
|
|
205
|
-
# node is a real quoted string, so we never corrupt `%w[...]` words.
|
|
186
|
+
# Mutate a literal: drop it to nil (possible-nil channel) and swap its type (type-mismatch channel). String
|
|
187
|
+
# literals are only touched when the node is a real quoted string, so we never corrupt `%w[...]` words.
|
|
206
188
|
def literal_mutations(node, out, numeric:)
|
|
207
189
|
return if !numeric && !QUOTES.include?(node.opening_loc&.slice)
|
|
208
190
|
|
|
@@ -222,12 +204,10 @@ module Rigor
|
|
|
222
204
|
extend_arity(node, out)
|
|
223
205
|
end
|
|
224
206
|
|
|
225
|
-
# Rename the *call site* (not the def) to a method that cannot exist, so a
|
|
226
|
-
#
|
|
227
|
-
#
|
|
228
|
-
#
|
|
229
|
-
# Anchor is the explicit receiver (nil ⇒ implicit self ⇒ filtered out, as
|
|
230
|
-
# call.self-undefined-method ships `:off`).
|
|
207
|
+
# Rename the *call site* (not the def) to a method that cannot exist, so a typed receiver trips
|
|
208
|
+
# call.undefined-method. We leave `def` signatures untouched on purpose: the prebuilt ProjectScan still
|
|
209
|
+
# carries the file's original declarations, so mutating only bodies/call-sites keeps it valid. Anchor is
|
|
210
|
+
# the explicit receiver (nil ⇒ implicit self ⇒ filtered out, as call.self-undefined-method ships `:off`).
|
|
231
211
|
def rename_call(node, out)
|
|
232
212
|
name = node.name.to_s
|
|
233
213
|
mloc = node.message_loc
|
|
@@ -237,8 +217,8 @@ module Rigor
|
|
|
237
217
|
"#{name}__rigor_absent", mloc.start_line, "call ##{name} → missing method", node.receiver, name)
|
|
238
218
|
end
|
|
239
219
|
|
|
240
|
-
# Append a trailing argument inside explicit `(...)` parens to trip an
|
|
241
|
-
#
|
|
220
|
+
# Append a trailing argument inside explicit `(...)` parens to trip an arity diagnostic against a known
|
|
221
|
+
# fixed-arity signature.
|
|
242
222
|
def extend_arity(node, out)
|
|
243
223
|
open = node.opening_loc
|
|
244
224
|
close = node.closing_loc
|
|
@@ -2,23 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
module Rigor
|
|
4
4
|
module Protection
|
|
5
|
-
# ADR-70 — the **test-suite** kill oracle, the dynamic sibling of
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# site Rigor cannot bite (a type survivor) may still be fully guarded by a
|
|
10
|
-
# test.
|
|
5
|
+
# ADR-70 — the **test-suite** kill oracle, the dynamic sibling of {DiagnosticOracle} on the ADR-69 seam. A
|
|
6
|
+
# mutant is killed iff applying its bytes to the file under test turns the project's test suite **red**. This
|
|
7
|
+
# is the dynamic half of the fused static∪dynamic protection map: a `Dynamic` site Rigor cannot bite (a type
|
|
8
|
+
# survivor) may still be fully guarded by a test.
|
|
11
9
|
#
|
|
12
|
-
# The suite command is the **runner hook** (`--test-command`, e.g.
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# process exit status (0 = green / passed).
|
|
10
|
+
# The suite command is the **runner hook** (`--test-command`, e.g. `bundle exec rake`). The runner is
|
|
11
|
+
# injectable so the decision logic is unit-testable without shelling out; the default shells out and reads
|
|
12
|
+
# the process exit status (0 = green / passed).
|
|
16
13
|
#
|
|
17
|
-
# I/O policy: {#killed?} writes the mutant to disk, runs the suite, and
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
# CI accept that, as `mutant` / Stryker do.)
|
|
14
|
+
# I/O policy: {#killed?} writes the mutant to disk, runs the suite, and **always restores** the original
|
|
15
|
+
# bytes in an `ensure` — a normal exception never leaves a mutant on disk. (A hard interrupt mid-suite is the
|
|
16
|
+
# standard mutation-testing hazard the `ensure` cannot cover; callers running this in CI accept that, as
|
|
17
|
+
# `mutant` / Stryker do.)
|
|
22
18
|
class TestSuiteOracle
|
|
23
19
|
# @param command [Array<String>] the test command (the runner hook)
|
|
24
20
|
# @param runner [#call, nil] `runner.call(command) -> true iff the suite
|
|
@@ -28,9 +24,8 @@ module Rigor
|
|
|
28
24
|
@runner = runner || method(:shell_run)
|
|
29
25
|
end
|
|
30
26
|
|
|
31
|
-
# The baseline: the suite must pass on clean code, else "a mutant survived"
|
|
32
|
-
#
|
|
33
|
-
# before measuring.
|
|
27
|
+
# The baseline: the suite must pass on clean code, else "a mutant survived" is meaningless (every mutant
|
|
28
|
+
# would look killed, or none would). Run once before measuring.
|
|
34
29
|
def green?
|
|
35
30
|
@runner.call(@command)
|
|
36
31
|
end
|
|
@@ -48,15 +43,13 @@ module Rigor
|
|
|
48
43
|
|
|
49
44
|
private
|
|
50
45
|
|
|
51
|
-
# Run the suite with Bundler's environment stripped, so a `bundle exec`
|
|
52
|
-
#
|
|
53
|
-
# bundle
|
|
54
|
-
#
|
|
55
|
-
# `
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
# BUNDLE_GEMFILE` is not enough — the `BUNDLER_ORIG_*` preservers defeat
|
|
59
|
-
# it). Found validating ADR-70 on real projects (2026-06-17).
|
|
46
|
+
# Run the suite with Bundler's environment stripped, so a `bundle exec` test command resolves the
|
|
47
|
+
# **target** project's Gemfile — not whatever bundle Rigor itself was launched under. Running Rigor via
|
|
48
|
+
# `bundle exec` leaks `RUBYOPT=-rbundler/setup` + `GEM_HOME` / `BUNDLE_*` into a plain `system` subprocess,
|
|
49
|
+
# which then resolves the target's Gemfile against Rigor's gems and fails — so a green suite looks red and
|
|
50
|
+
# the run aborts. `with_unbundled_env` restores the pre-bundler env (a bare `env -u BUNDLE_GEMFILE` is not
|
|
51
|
+
# enough — the `BUNDLER_ORIG_*` preservers defeat it). Found validating ADR-70 on real projects
|
|
52
|
+
# (2026-06-17).
|
|
60
53
|
def shell_run(command)
|
|
61
54
|
run = -> { system(*command, out: File::NULL, err: File::NULL) }
|
|
62
55
|
return run.call unless defined?(Bundler) && Bundler.respond_to?(:with_unbundled_env)
|