rigortype 0.4.5 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
- data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
- data/docs/handbook/01-getting-started.md +5 -2
- data/docs/handbook/11-sig-gen.md +61 -5
- data/docs/handbook/appendix-phpstan.md +4 -4
- data/docs/manual/02-cli-reference.md +192 -27
- data/docs/manual/03-configuration.md +38 -4
- data/docs/manual/04-diagnostics.md +1 -1
- data/docs/manual/06-baseline.md +6 -4
- data/docs/manual/09-editor-integration.md +13 -2
- data/docs/manual/10-mcp-server.md +47 -3
- data/docs/manual/12-caching.md +9 -1
- data/docs/manual/plugins/rigor-actionmailer.md +30 -0
- data/docs/manual/plugins/rigor-actionpack.md +49 -11
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
- data/docs/manual/plugins/rigor-devise.md +15 -0
- data/docs/manual/plugins/rigor-rspec.md +2 -1
- data/lib/rigor/analysis/baseline.rb +24 -27
- data/lib/rigor/analysis/buffer_binding.rb +14 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +105 -12
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +143 -15
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -1
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
- data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
- data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
- data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +53 -2
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules.rb +217 -52
- data/lib/rigor/analysis/crash_signature.rb +2 -2
- data/lib/rigor/analysis/dependency_recorder.rb +15 -4
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
- data/lib/rigor/analysis/edit_replay.rb +154 -0
- data/lib/rigor/analysis/fact_store.rb +3 -3
- data/lib/rigor/analysis/incremental.rb +37 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
- data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
- data/lib/rigor/analysis/incremental_session.rb +202 -43
- data/lib/rigor/analysis/path_expansion.rb +41 -0
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +7 -3
- data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
- data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +8 -4
- data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +204 -96
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +157 -320
- data/lib/rigor/analysis/runner/project_pre_passes.rb +48 -6
- data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
- data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
- data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
- data/lib/rigor/analysis/runner.rb +233 -120
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
- data/lib/rigor/analysis/template_unit_positions.rb +2 -3
- data/lib/rigor/analysis/template_units.rb +9 -1
- data/lib/rigor/analysis/worker_session.rb +38 -33
- data/lib/rigor/bleeding_edge.rb +3 -6
- data/lib/rigor/builtins/imported_refinements.rb +6 -8
- data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
- data/lib/rigor/builtins/regex_refinement.rb +4 -6
- data/lib/rigor/cache/descriptor.rb +6 -5
- data/lib/rigor/cache/incremental_snapshot.rb +60 -18
- data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
- data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
- data/lib/rigor/cache/rbs_constant_table.rb +1 -1
- data/lib/rigor/cache/rbs_descriptor.rb +54 -3
- data/lib/rigor/cache/rbs_environment.rb +4 -1
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
- data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
- data/lib/rigor/cache/store.rb +11 -10
- data/lib/rigor/ci_detector.rb +1 -1
- data/lib/rigor/cli/annotate_command.rb +24 -19
- data/lib/rigor/cli/baseline_command.rb +145 -55
- data/lib/rigor/cli/catalogue.rb +256 -0
- data/lib/rigor/cli/check_command.rb +188 -93
- data/lib/rigor/cli/check_runner_factory.rb +2 -2
- data/lib/rigor/cli/command.rb +15 -1
- data/lib/rigor/cli/coverage_command.rb +21 -8
- data/lib/rigor/cli/coverage_mutation.rb +5 -7
- data/lib/rigor/cli/coverage_renderer.rb +2 -1
- data/lib/rigor/cli/coverage_scan.rb +3 -5
- data/lib/rigor/cli/diff_command.rb +27 -21
- data/lib/rigor/cli/docs_command.rb +1 -2
- data/lib/rigor/cli/doctor_command.rb +12 -12
- data/lib/rigor/cli/effects_command.rb +50 -40
- data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
- data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
- data/lib/rigor/cli/effects_renderer.rb +2 -1
- data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
- data/lib/rigor/cli/explain_command.rb +18 -8
- data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
- data/lib/rigor/cli/json_document.rb +41 -0
- data/lib/rigor/cli/lens_command.rb +17 -13
- data/lib/rigor/cli/lsp_command.rb +250 -23
- data/lib/rigor/cli/mcp_command.rb +14 -8
- data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
- data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
- data/lib/rigor/cli/options.rb +60 -4
- data/lib/rigor/cli/plugin_command.rb +93 -46
- data/lib/rigor/cli/plugins_command.rb +22 -18
- data/lib/rigor/cli/plugins_renderer.rb +34 -8
- data/lib/rigor/cli/protection_fork_scan.rb +2 -2
- data/lib/rigor/cli/protection_renderer.rb +2 -1
- data/lib/rigor/cli/show_bleedingedge_command.rb +23 -16
- data/lib/rigor/cli/sig_gen_command.rb +130 -34
- data/lib/rigor/cli/skill_command.rb +32 -3
- data/lib/rigor/cli/skill_deep_probe.rb +1 -1
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/trace_command.rb +14 -9
- data/lib/rigor/cli/triage_command.rb +10 -11
- data/lib/rigor/cli/triage_renderer.rb +2 -1
- data/lib/rigor/cli/type_of_command.rb +18 -12
- data/lib/rigor/cli/type_of_project_scope.rb +24 -0
- data/lib/rigor/cli/type_of_renderer.rb +4 -1
- data/lib/rigor/cli/type_scan_command.rb +14 -9
- data/lib/rigor/cli/type_scan_renderer.rb +2 -1
- data/lib/rigor/cli/unused_command.rb +27 -17
- data/lib/rigor/cli/upgrade_command.rb +5 -0
- data/lib/rigor/cli.rb +209 -54
- data/lib/rigor/config_audit.rb +17 -6
- data/lib/rigor/configuration/severity_profile.rb +5 -5
- data/lib/rigor/configuration.rb +86 -34
- data/lib/rigor/effects/definition_context.rb +0 -2
- data/lib/rigor/effects/envelope.rb +1 -1
- data/lib/rigor/effects/framework_units.rb +1 -1
- data/lib/rigor/effects/mutation_classifier.rb +4 -7
- data/lib/rigor/effects/propagator.rb +1 -1
- data/lib/rigor/effects/registry.rb +2 -2
- data/lib/rigor/effects/snapshot.rb +1 -1
- data/lib/rigor/effects/taint_cause.rb +0 -2
- data/lib/rigor/effects/unit_scan.rb +9 -13
- data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
- data/lib/rigor/environment/class_registry.rb +3 -3
- data/lib/rigor/environment/core_root.rb +77 -0
- data/lib/rigor/environment/default_libraries.rb +1 -1
- data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
- data/lib/rigor/environment/gated_signature_guard.rb +1 -1
- data/lib/rigor/environment/gated_signature_plan.rb +5 -2
- data/lib/rigor/environment/lockfile_resolver.rb +28 -12
- data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
- data/lib/rigor/environment/member_consistency.rb +56 -7
- data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
- data/lib/rigor/environment/project_path_gems.rb +124 -0
- data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
- data/lib/rigor/environment/rbs_loader.rb +592 -70
- data/lib/rigor/environment/reflection.rb +7 -7
- data/lib/rigor/environment/required_features.rb +136 -32
- data/lib/rigor/environment/ruby_library_roots.rb +122 -0
- data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
- data/lib/rigor/environment.rb +44 -11
- data/lib/rigor/flow_contribution/merger.rb +4 -4
- data/lib/rigor/inference/acceptance.rb +93 -32
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/block_run_count.rb +178 -0
- data/lib/rigor/inference/budget_trace.rb +8 -2
- data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +126 -0
- data/lib/rigor/inference/callee_write_census.rb +815 -0
- data/lib/rigor/inference/construction_escape.rb +217 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
- data/lib/rigor/inference/deep_erasure.rb +170 -0
- data/lib/rigor/inference/def_handle.rb +1 -1
- data/lib/rigor/inference/def_node_resolver.rb +1 -1
- data/lib/rigor/inference/def_return_typer.rb +204 -12
- data/lib/rigor/inference/definer_resolution.rb +21 -5
- data/lib/rigor/inference/definers_index.rb +106 -0
- data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
- data/lib/rigor/inference/element_read_widening.rb +49 -3
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +173 -57
- data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
- data/lib/rigor/inference/global_write_census.rb +110 -1
- data/lib/rigor/inference/global_writer_reach.rb +260 -0
- data/lib/rigor/inference/hkt_body.rb +1 -2
- data/lib/rigor/inference/hkt_body_parser.rb +3 -5
- data/lib/rigor/inference/hkt_reducer.rb +2 -2
- data/lib/rigor/inference/hkt_registry.rb +3 -5
- data/lib/rigor/inference/indexed_narrowing.rb +4 -7
- data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
- data/lib/rigor/inference/ivar_writer_census.rb +246 -0
- data/lib/rigor/inference/key_presence_guard.rb +2 -1
- data/lib/rigor/inference/loop_value_record.rb +57 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +65 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
- data/lib/rigor/inference/match_rebinding.rb +11 -1
- data/lib/rigor/inference/memo_audit.rb +55 -0
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +113 -12
- data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -3
- data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
- data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +59 -24
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +109 -29
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +23 -1
- data/lib/rigor/inference/multi_target_binder.rb +1 -1
- data/lib/rigor/inference/mutation_rejoin.rb +240 -4
- data/lib/rigor/inference/mutation_widening.rb +162 -29
- data/lib/rigor/inference/narrowing.rb +113 -38
- data/lib/rigor/inference/object_mixins.rb +13 -1
- data/lib/rigor/inference/optimistic_origin.rb +1 -1
- data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
- data/lib/rigor/inference/pre_eval_constants.rb +1 -1
- data/lib/rigor/inference/project_method_ownership.rb +6 -3
- data/lib/rigor/inference/project_seed_tables.rb +2 -1
- data/lib/rigor/inference/project_yielder.rb +122 -0
- data/lib/rigor/inference/receiver_alias.rb +20 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
- data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
- data/lib/rigor/inference/remover_holder_scan.rb +233 -0
- data/lib/rigor/inference/remover_narrowing.rb +224 -0
- data/lib/rigor/inference/rewrite_mutation.rb +53 -2
- data/lib/rigor/inference/scope_indexer/certainty.rb +9 -1
- data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
- data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +205 -0
- data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
- data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
- data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
- data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
- data/lib/rigor/inference/scope_indexer.rb +614 -146
- data/lib/rigor/inference/self_call_ivar_writes.rb +101 -17
- data/lib/rigor/inference/set_mutation.rb +189 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
- data/lib/rigor/inference/statement_evaluator.rb +3036 -278
- data/lib/rigor/inference/struct_fold_safety.rb +4 -6
- data/lib/rigor/inference/synthetic_method_index.rb +23 -0
- data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +73 -14
- data/lib/rigor/inference/toplevel_def_census.rb +59 -0
- data/lib/rigor/inference/unknown_store_widening.rb +103 -4
- data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
- data/lib/rigor/inference/void_tail_summary.rb +2 -1
- data/lib/rigor/language_server/buffer_table.rb +9 -3
- data/lib/rigor/language_server/completion_provider.rb +26 -20
- data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +28 -9
- data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
- data/lib/rigor/language_server/folding_range_provider.rb +1 -2
- data/lib/rigor/language_server/hover_provider.rb +1 -7
- data/lib/rigor/language_server/hover_renderer.rb +6 -8
- data/lib/rigor/language_server/project_context.rb +98 -4
- data/lib/rigor/language_server/publish_batcher.rb +3 -4
- data/lib/rigor/language_server/selection_range_provider.rb +2 -4
- data/lib/rigor/language_server/server.rb +12 -6
- data/lib/rigor/language_server/signature_help_provider.rb +4 -4
- data/lib/rigor/lens/plugins.rb +1 -1
- data/lib/rigor/mcp/server.rb +82 -3
- data/lib/rigor/mcp.rb +1 -1
- data/lib/rigor/plugin/access_denied_error.rb +1 -2
- data/lib/rigor/plugin/base.rb +26 -11
- data/lib/rigor/plugin/bundled_catalog.rb +1 -12
- data/lib/rigor/plugin/io_boundary.rb +1 -1
- data/lib/rigor/plugin/loader.rb +25 -8
- data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
- data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
- data/lib/rigor/plugin/manifest.rb +38 -16
- data/lib/rigor/plugin/registry.rb +41 -3
- data/lib/rigor/plugin/services.rb +2 -3
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
- data/lib/rigor/plugin/trust_policy.rb +2 -2
- data/lib/rigor/plugin/type_node_resolver.rb +2 -4
- data/lib/rigor/plugin.rb +6 -8
- data/lib/rigor/plugin_gap_advisory.rb +5 -1
- data/lib/rigor/project_environment.rb +16 -2
- data/lib/rigor/protection/analysis_guard.rb +1 -1
- data/lib/rigor/protection/discovery_seed.rb +96 -2
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
- data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
- data/lib/rigor/rbs_extended/reporter.rb +27 -3
- data/lib/rigor/rbs_extended.rb +2 -3
- data/lib/rigor/reflection/constant_ancestors.rb +5 -3
- data/lib/rigor/rooted_glob.rb +25 -2
- data/lib/rigor/run_scoped_slots.rb +38 -0
- data/lib/rigor/runtime/jit.rb +1 -1
- data/lib/rigor/scope/discovery_index.rb +19 -6
- data/lib/rigor/scope/resolution_chain.rb +3 -1
- data/lib/rigor/scope.rb +379 -36
- data/lib/rigor/sig_gen/classification.rb +4 -0
- data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
- data/lib/rigor/sig_gen/generator.rb +668 -84
- data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
- data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
- data/lib/rigor/sig_gen/layout_index.rb +3 -3
- data/lib/rigor/sig_gen/method_candidate.rb +23 -4
- data/lib/rigor/sig_gen/observation_collector.rb +7 -6
- data/lib/rigor/sig_gen/observed_call.rb +1 -1
- data/lib/rigor/sig_gen/path_mapper.rb +6 -0
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +21 -12
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
- data/lib/rigor/sig_gen/write_reporter.rb +63 -14
- data/lib/rigor/sig_gen/write_result.rb +23 -12
- data/lib/rigor/sig_gen/writer.rb +217 -37
- data/lib/rigor/type/acceptance_router.rb +2 -2
- data/lib/rigor/type/accepts_result.rb +1 -1
- data/lib/rigor/type/app.rb +4 -6
- data/lib/rigor/type/combinator.rb +13 -16
- data/lib/rigor/type/constant.rb +48 -7
- data/lib/rigor/type/hash_shape.rb +26 -40
- data/lib/rigor/type/struct_instance.rb +3 -2
- data/lib/rigor/type/union.rb +1 -1
- data/lib/rigor/type_node/identifier.rb +1 -2
- data/lib/rigor/type_node/resolver_chain.rb +1 -1
- data/lib/rigor/type_node/string_literal.rb +1 -2
- data/lib/rigor/type_node/symbol_literal.rb +1 -2
- data/lib/rigor/type_node/union.rb +3 -6
- data/lib/rigor/value_semantics.rb +1 -10
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +89 -70
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +25 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +79 -19
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +6 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
- data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
- data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +5 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +0 -2
- data/sig/rigor/analysis/fact_store.rbs +1 -0
- data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
- data/sig/rigor/cli/diff_command.rbs +1 -1
- data/sig/rigor/cli/explain_command.rbs +1 -1
- data/sig/rigor/effects/file_collection.rbs +5 -6
- data/sig/rigor/effects/registry.rbs +8 -6
- data/sig/rigor/effects/summary.rbs +6 -5
- data/sig/rigor/environment.rbs +5 -3
- data/sig/rigor/inference/optimistic_origin.rbs +11 -0
- data/sig/rigor/inference.rbs +10 -1
- data/sig/rigor/plugin/additional_initializer.rbs +0 -5
- data/sig/rigor/plugin/base.rbs +1 -0
- data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
- data/sig/rigor/plugin/effect_attribution.rbs +25 -10
- data/sig/rigor/plugin/effect_edge.rbs +12 -5
- data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
- data/sig/rigor/plugin/io_boundary.rbs +6 -5
- data/sig/rigor/plugin/manifest.rbs +16 -18
- data/sig/rigor/plugin/protocol_contract.rbs +6 -9
- data/sig/rigor/plugin/registry.rbs +1 -1
- data/sig/rigor/scope.rbs +51 -50
- data/sig/rigor/type.rbs +0 -6
- data/skills/rigor-plugin-author/SKILL.md +4 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
- data/skills/rigor-plugin-review/SKILL.md +3 -3
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
- metadata +55 -3
|
@@ -5,6 +5,7 @@ require_relative "incremental"
|
|
|
5
5
|
require_relative "incremental_run_slot"
|
|
6
6
|
require_relative "incremental_run_slot/write_guard"
|
|
7
7
|
require_relative "plugin_fact_fingerprint"
|
|
8
|
+
require_relative "run_disclosure_ledger"
|
|
8
9
|
require_relative "source_rbs_gate"
|
|
9
10
|
require_relative "../cache/file_digest"
|
|
10
11
|
require_relative "../cache/incremental_snapshot"
|
|
@@ -26,10 +27,8 @@ module Rigor
|
|
|
26
27
|
#
|
|
27
28
|
# The invariant the verify harness (and the spec) assert: `#recheck`'s merged diagnostics are
|
|
28
29
|
# byte-identical to a full `--no-cache` re-analysis of the edited tree, in the same order (#1524; the gate
|
|
29
|
-
# itself compares sorted sets).
|
|
30
|
-
# persistence
|
|
31
|
-
# removes / moves a *file* is outside the analyzed set it maintains and falls to a fresh {#baseline} (the
|
|
32
|
-
# structural tier is a later slice).
|
|
30
|
+
# itself compares sorted sets). The `--verify-incremental` gate runs this engine; `#run_incremental` adds the
|
|
31
|
+
# snapshot persistence that `--incremental` uses.
|
|
33
32
|
# The class-length budget is relaxed: this is one cohesive orchestrator of the incremental state
|
|
34
33
|
# (per-file diagnostics cache, the file-level / symbol-level / negative dependency graphs, and the ADR-85
|
|
35
34
|
# seed bundles), clearer read together than split across micro-classes that would all share the same ivars.
|
|
@@ -100,8 +99,8 @@ module Rigor
|
|
|
100
99
|
# ADR-85 WD2 — per-file discovery seed bundles keyed by logical path. A cold baseline builds them; a
|
|
101
100
|
# warm recheck folds them (re-walking only changed files) and refreshes the set. Ride the snapshot.
|
|
102
101
|
@seed_bundles = {}
|
|
103
|
-
@cache = {} # analyzed path =>
|
|
104
|
-
@sources = {} # analyzed path =>
|
|
102
|
+
@cache = {} # analyzed path => its diagnostics
|
|
103
|
+
@sources = {} # analyzed path => the source paths it read from
|
|
105
104
|
@digests = {} # analyzed path => ADR-87 packed stat-digest entry at last analysis
|
|
106
105
|
# Issue #1583 — discovered-not-analysed path => the same packed entry, for a run over path arguments that
|
|
107
106
|
# #684 widened to the configured `paths:`. Empty on every run over the configured paths.
|
|
@@ -109,18 +108,18 @@ module Rigor
|
|
|
109
108
|
@analyzed = [] # the project files analyzed last round
|
|
110
109
|
@dependents = {} # inverted @sources (file-level)
|
|
111
110
|
# ADR-46 slice 4 — symbol-granularity tracking.
|
|
112
|
-
@symbol_sources = {} # consumer => { source_path =>
|
|
113
|
-
@ancestry_sources = {} # consumer =>
|
|
111
|
+
@symbol_sources = {} # consumer => { source_path => the "ClassName#method" symbols it read }
|
|
112
|
+
@ancestry_sources = {} # consumer => source paths (class-ancestry deps)
|
|
114
113
|
@symbol_fingerprints = {} # path => { "ClassName#method" => sha256_hex }
|
|
115
|
-
@symbol_dependents = {} # [source, symbol] =>
|
|
116
|
-
@ancestry_dependents = {} # source =>
|
|
114
|
+
@symbol_dependents = {} # [source, symbol] => consumers
|
|
115
|
+
@ancestry_dependents = {} # source => consumers (inverted ancestry_sources)
|
|
117
116
|
# ADR-46 slice 3 — negative (missing) dependencies: a consumer that
|
|
118
117
|
# looked up a name and resolved nothing must be re-checked when that
|
|
119
118
|
# name later appears (e.g. a `call.unresolved-toplevel` whose target
|
|
120
119
|
# is defined by a later edit).
|
|
121
|
-
@missing = {} # consumer =>
|
|
122
|
-
@negative_dependents = {} # "kind:name" =>
|
|
123
|
-
@class_decls = {} # path =>
|
|
120
|
+
@missing = {} # consumer => the "kind:name" keys it looked up and missed
|
|
121
|
+
@negative_dependents = {} # "kind:name" => consumers (inverted @missing)
|
|
122
|
+
@class_decls = {} # path => qualified class names declared in the file
|
|
124
123
|
# Issue #644 — the value-constant twin: path => { qualified constant name => publication descriptor }.
|
|
125
124
|
# Diffed on a recheck to find every constant whose PUBLISHED answer could have moved, which satisfies
|
|
126
125
|
# the `constant:` edges a reader records at every reference, resolved or not.
|
|
@@ -139,6 +138,9 @@ module Rigor
|
|
|
139
138
|
# the frozen `Cache::Descriptor::FileEntry` / `GlobEntry` rows, as each read observed its input. A recheck
|
|
140
139
|
# re-analyses every file one of whose rows no longer validates ({#plugin_read_affected}).
|
|
141
140
|
@plugin_reads = {}
|
|
141
|
+
# Issue #2020 — the run-scoped plugin rows each analysed file registered, and the reads that make one stale.
|
|
142
|
+
@plugin_disclosures = RunDisclosureLedger.new
|
|
143
|
+
@stale_disclosures = Set.new
|
|
142
144
|
reset_carried_state
|
|
143
145
|
# ADR-88 WD1 — the plugin fact-surface digest computed for THIS invocation (nil until a
|
|
144
146
|
# `#run_incremental` pass runs / a plugin-free project) and the reporting flags a caller (the CLI
|
|
@@ -269,8 +271,10 @@ module Rigor
|
|
|
269
271
|
@effect_collections = runner.effect_collections_by_path
|
|
270
272
|
@effects_identity = current_effects_identity
|
|
271
273
|
@effect_plugin_digest = effect_plugin_digest_of(runner)
|
|
274
|
+
@synthetic_surface_digest = synthetic_surface_digest_of(runner)
|
|
272
275
|
@cache = per_file(runner.per_file_diagnostics)
|
|
273
276
|
@plugin_reads = plugin_reads_of(runner, @analyzed)
|
|
277
|
+
@plugin_disclosures = RunDisclosureLedger.from(runner.analysis_disclosures.slice(*@analyzed))
|
|
274
278
|
record_baseline_digests
|
|
275
279
|
@run_level_rows = runner.run_level_rows
|
|
276
280
|
diagnostics
|
|
@@ -294,16 +298,11 @@ module Rigor
|
|
|
294
298
|
fresh_params = fresh_param_table(current, changed, added, removed)
|
|
295
299
|
param_files, param_pairs = param_seed_invalidation(fresh_params)
|
|
296
300
|
affected = affected_closure(changed, added, removed, param_files, param_pairs)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
runner = build_runner(analyze_only: analyze_set, record_dependencies: true,
|
|
303
|
-
param_inferred_types: fresh_params, **forced_target_options(forced),
|
|
304
|
-
restored_run_level_rows: @run_level_rows, **served_state(reused))
|
|
305
|
-
merged = run_runner(runner).diagnostics
|
|
306
|
-
return rebaseline_recheck(changed, added, removed) if served_effects_stale?(runner, reused)
|
|
301
|
+
affected, analyze_set, runner, merged, reused =
|
|
302
|
+
run_settled_closure(affected, [current, previous, removed], fresh_params)
|
|
303
|
+
if served_effects_stale?(runner, reused) || synthetic_surface_moved?(runner, reused)
|
|
304
|
+
return rebaseline_recheck(changed, added, removed)
|
|
305
|
+
end
|
|
307
306
|
|
|
308
307
|
absorb(runner, current, analyze_set, removed)
|
|
309
308
|
@param_table = fresh_params
|
|
@@ -311,6 +310,36 @@ module Rigor
|
|
|
311
310
|
stale_readers: @stale_plugin_readers)
|
|
312
311
|
end
|
|
313
312
|
|
|
313
|
+
# {#recheck}'s narrowed run. Issue #2020 — a stale run-scoped key whose chosen refresher registered nothing (a
|
|
314
|
+
# parse error, a plugin that raised, an edit that took the notice out of its text) is decided by its other
|
|
315
|
+
# holders, so they join the closure and it runs again. Returns `[affected, analyze_set, runner, merged, reused]`.
|
|
316
|
+
def run_settled_closure(affected, (current, previous, removed), fresh_params)
|
|
317
|
+
forced, analyze_set = forced_analyze_set(affected, current)
|
|
318
|
+
runner, merged, reused = run_closure(analyze_set, current, previous, forced, fresh_params)
|
|
319
|
+
fallback = @plugin_disclosures.unresolved_holders(runner.analysis_disclosures, analyze_set, @stale_disclosures,
|
|
320
|
+
removed.to_set)
|
|
321
|
+
return [affected, analyze_set, runner, merged, reused] if fallback.empty?
|
|
322
|
+
|
|
323
|
+
affected = (affected | fallback).freeze
|
|
324
|
+
@stale_plugin_readers |= fallback
|
|
325
|
+
forced, analyze_set = forced_analyze_set(affected, current)
|
|
326
|
+
[affected, analyze_set, *run_closure(analyze_set, current, previous, forced, fresh_params)]
|
|
327
|
+
end
|
|
328
|
+
private :run_settled_closure
|
|
329
|
+
|
|
330
|
+
# One narrowed run of {#recheck}'s closure: the runner, its merged rows, and the files it served. The freshly
|
|
331
|
+
# collected table is handed to the runner so the run seeds from the SAME table the diff was decided on (and the
|
|
332
|
+
# collector runs once per recheck, not twice). The reused files' cached rows go in too, so the runner lists them
|
|
333
|
+
# in the full run's file order (#1524).
|
|
334
|
+
def run_closure(analyze_set, current, previous, forced, fresh_params)
|
|
335
|
+
reused = (current & previous) - analyze_set.to_a
|
|
336
|
+
runner = build_runner(analyze_only: analyze_set, record_dependencies: true,
|
|
337
|
+
param_inferred_types: fresh_params, **forced_target_options(forced),
|
|
338
|
+
restored_run_level_rows: @run_level_rows, **served_state(reused))
|
|
339
|
+
[runner, run_runner(runner).diagnostics, reused]
|
|
340
|
+
end
|
|
341
|
+
private :run_closure
|
|
342
|
+
|
|
314
343
|
# Editor mode option B (#146) — a whole-project recheck with the editor's buffer substituted for one
|
|
315
344
|
# file, for the CLI's `--incremental --tmp-file=X --instead-of=Y`. Returns the {Recheck} when the
|
|
316
345
|
# snapshot could be reused, or nil when it could not — the caller then falls back to option A
|
|
@@ -361,7 +390,7 @@ module Rigor
|
|
|
361
390
|
# records none at all), so no dependent set can bound who read it. An edit that moved it re-analyses the
|
|
362
391
|
# whole project — what a `sig/` edit already costs through the snapshot fingerprint.
|
|
363
392
|
def affected_closure(changed, added, removed, param_files = Set.new, param_pairs = Set.new)
|
|
364
|
-
@stale_plugin_readers = plugin_read_affected
|
|
393
|
+
@stale_plugin_readers = plugin_read_affected | disclosure_refreshers(removed)
|
|
365
394
|
return whole_project_closure(changed, added) if source_rbs_gate.moved?(@seed_bundles, changed, added, removed)
|
|
366
395
|
|
|
367
396
|
(gated_closure(changed, added, removed, param_files, param_pairs) | @stale_plugin_readers).freeze
|
|
@@ -471,8 +500,8 @@ module Rigor
|
|
|
471
500
|
# exempts such a write is a fact of the whole program — an alias, or a `write` / `to_str` / `to_int` / hatch
|
|
472
501
|
# defined in any spelling on any receiver, a rewritten ancestor, a mixin — which no name-keyed edge can
|
|
473
502
|
# follow, so a stale report would outlive the edit that exempts it. Such files are rare, so this costs little.
|
|
474
|
-
# A file the census silenced is
|
|
475
|
-
#
|
|
503
|
+
# A file the census silenced is re-checked through the `global_census:` keys its rule recorded
|
|
504
|
+
# ({#global_census_affected}, issue #2158).
|
|
476
505
|
def global_write_reporters
|
|
477
506
|
@cache.each_with_object(Set.new) do |(path, diagnostics), reporters|
|
|
478
507
|
reporters << path if diagnostics.any? { |diagnostic| diagnostic.rule.to_s.start_with?("global.") }
|
|
@@ -602,9 +631,21 @@ module Rigor
|
|
|
602
631
|
runner = build_runner
|
|
603
632
|
files = @paths ? runner.analysis_file_set(@paths) : runner.analysis_file_set
|
|
604
633
|
rebind_buffer(files)
|
|
605
|
-
files
|
|
634
|
+
admit_new_buffer(files)
|
|
606
635
|
end
|
|
607
636
|
|
|
637
|
+
# Issue #2233 — a buffer for a file that is not on disk (a new file) is in no expansion, so the set above never
|
|
638
|
+
# names it: the recheck neither analyses it nor sees its declarations, and the run answers "re-analysed 0 file(s)"
|
|
639
|
+
# with no diagnostics for the buffer. Option A (single-file scope) analyses the logical path whether or not it
|
|
640
|
+
# exists; option B joins it to the set the same way, as an added file, so the closure analyses it like any new
|
|
641
|
+
# file. A buffer already matched by {#rebind_buffer} is in `files` and is returned as it is.
|
|
642
|
+
def admit_new_buffer(files)
|
|
643
|
+
return files if @buffer.nil? || files.include?(@buffer.logical_path)
|
|
644
|
+
|
|
645
|
+
files + [@buffer.logical_path]
|
|
646
|
+
end
|
|
647
|
+
private :admit_new_buffer
|
|
648
|
+
|
|
608
649
|
# Verification engine (the `--verify-incremental` gate): with NO source edit, re-analyze `subset` fresh
|
|
609
650
|
# and serve every other analyzed file from the baseline cache. Because nothing on disk changed, the
|
|
610
651
|
# merged result MUST equal a full analysis — so this exercises the subset-analysis and cache-merge
|
|
@@ -620,6 +661,32 @@ module Rigor
|
|
|
620
661
|
run_runner(runner).diagnostics
|
|
621
662
|
end
|
|
622
663
|
|
|
664
|
+
# Issue #1967 — the edit replay of `--verify-incremental-edits`: a warm recheck of this session's baseline with
|
|
665
|
+
# `buffer` standing in for one file, the way editor mode option B (#146) rechecks an unsaved edit. The baseline is
|
|
666
|
+
# handed over as a Marshal copy of the snapshot a `--incremental` run would save, through a fresh session that
|
|
667
|
+
# never persists, so this session's own state is untouched and each replay starts from the same baseline. Unlike
|
|
668
|
+
# {#reanalyze_subset}, whose tree is the baseline's, the edit moves what the served files' rows were computed
|
|
669
|
+
# from, so a dependent the recheck fails to re-analyse is served stale and the comparison sees it. Returns the
|
|
670
|
+
# {Recheck}, or nil when the recheck declined as option B does (the plugin fact surface moved, an unverified
|
|
671
|
+
# source-RBS gate, a re-baseline), where the answer would be a full analysis and there is nothing to compare.
|
|
672
|
+
def replay_edit(buffer)
|
|
673
|
+
adopt_plugin_fact_fingerprint if @plugin_fact_reusable.nil?
|
|
674
|
+
replay = IncrementalSession.new(
|
|
675
|
+
configuration: @configuration, paths: @paths, environment: @environment, cache_store: @cache_store,
|
|
676
|
+
plugin_requirer: @plugin_requirer, buffer: buffer, explain: @explain
|
|
677
|
+
)
|
|
678
|
+
replay.run_buffer_recheck(snapshot: ReplaySnapshot.new(blob: Marshal.dump(to_payload)), fingerprint: :replay)
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
# The in-memory stand-in for {Cache::IncrementalSnapshot} {#replay_edit} hands its replay session: every load is a
|
|
682
|
+
# fresh copy, as a load from disk is.
|
|
683
|
+
ReplaySnapshot = Data.define(:blob) do
|
|
684
|
+
def load(fingerprint:) # rubocop:disable Lint/UnusedMethodArgument
|
|
685
|
+
Marshal.load(blob) # rubocop:disable Security/MarshalLoad
|
|
686
|
+
end
|
|
687
|
+
end
|
|
688
|
+
private_constant :ReplaySnapshot
|
|
689
|
+
|
|
623
690
|
# Cross-process incremental run (the `--incremental` flag's engine). With a disk `snapshot` whose
|
|
624
691
|
# `fingerprint` matches, restore the prior per-file state and `#recheck` (re-analyze only the changed
|
|
625
692
|
# closure, serve the rest from the restored cache); otherwise run a full `#baseline`. Either way,
|
|
@@ -991,7 +1058,7 @@ module Rigor
|
|
|
991
1058
|
restore_structural_declarations(payload)
|
|
992
1059
|
@return_summaries = payload.return_summaries || {}
|
|
993
1060
|
@param_table = payload.param_table || {} # ADR-67 WD6c lift — the seeds the cache was built under.
|
|
994
|
-
|
|
1061
|
+
restore_plugin_records(payload)
|
|
995
1062
|
# Issues #796 / #794 — absent (nil) in a snapshot written before schema 22 is impossible (the SCHEMA
|
|
996
1063
|
# gate rejects one), so nil here means the writing run genuinely had nothing to say.
|
|
997
1064
|
@run_level_rows = payload.run_level_rows
|
|
@@ -1007,6 +1074,14 @@ module Rigor
|
|
|
1007
1074
|
@negative_dependents = Incremental.invert(@missing)
|
|
1008
1075
|
end
|
|
1009
1076
|
|
|
1077
|
+
# Issue #1553 — the plugin reads each file's last analysis made, and (#2020) the run-scoped plugin rows it
|
|
1078
|
+
# registered; `{}` in a payload built without them. Extracted to keep {#restore} under its ABC budget.
|
|
1079
|
+
def restore_plugin_records(payload)
|
|
1080
|
+
@plugin_reads = payload.plugin_reads
|
|
1081
|
+
@plugin_disclosures = RunDisclosureLedger.load(payload.plugin_disclosures)
|
|
1082
|
+
@synthetic_surface_digest = payload.synthetic_surface_digest # the plugin-built ADR-16 index (PR #2286 M1)
|
|
1083
|
+
end
|
|
1084
|
+
|
|
1010
1085
|
# ADR-46 slice 3 / issue #644 — the structural tier's negative-edge state: the per-consumer missed
|
|
1011
1086
|
# lookups plus the two per-file DECLARATION sets (`class_decls`, `constant_decls`) whose diff says what
|
|
1012
1087
|
# APPEARED in an edit. Absent in an older snapshot → empty, which only loses the appeared-symbol
|
|
@@ -1032,7 +1107,9 @@ module Rigor
|
|
|
1032
1107
|
effects_identity: @effects_identity,
|
|
1033
1108
|
run_level_rows: @run_level_rows,
|
|
1034
1109
|
effect_plugin_digest: @effect_plugin_digest,
|
|
1110
|
+
synthetic_surface_digest: @synthetic_surface_digest,
|
|
1035
1111
|
plugin_reads: @plugin_reads,
|
|
1112
|
+
plugin_disclosures: @plugin_disclosures.to_h,
|
|
1036
1113
|
discovery_digests: @discovery_digests
|
|
1037
1114
|
)
|
|
1038
1115
|
end
|
|
@@ -1152,11 +1229,13 @@ module Rigor
|
|
|
1152
1229
|
@digests[path] = pack_digest(path)
|
|
1153
1230
|
fresh_reads.key?(path) ? @plugin_reads[path] = fresh_reads[path] : @plugin_reads.delete(path)
|
|
1154
1231
|
end
|
|
1232
|
+
absorb_disclosures(runner, analyze_set)
|
|
1155
1233
|
forget_unbound_digests
|
|
1156
1234
|
absorb_dependency_graph(runner)
|
|
1157
1235
|
# Issues #796 / #794 — the recheck's own snapshots already carry the replayed rows folded together
|
|
1158
1236
|
# with anything its closure demanded for itself, so this is the same union the next run replays.
|
|
1159
1237
|
@run_level_rows = runner.run_level_rows
|
|
1238
|
+
@synthetic_surface_digest = synthetic_surface_digest_of(runner)
|
|
1160
1239
|
refresh_return_summaries(runner, analyze_set)
|
|
1161
1240
|
refresh_effect_collections(runner, analyze_set)
|
|
1162
1241
|
end
|
|
@@ -1200,6 +1279,7 @@ module Rigor
|
|
|
1200
1279
|
@digests.delete(path)
|
|
1201
1280
|
@sources.delete(path)
|
|
1202
1281
|
@plugin_reads.delete(path)
|
|
1282
|
+
@plugin_disclosures.forget(path) # Unpinned hygiene: a removed file is never served, so never replayed.
|
|
1203
1283
|
@symbol_sources.delete(path)
|
|
1204
1284
|
@ancestry_sources.delete(path)
|
|
1205
1285
|
@missing.delete(path)
|
|
@@ -1271,7 +1351,8 @@ module Rigor
|
|
|
1271
1351
|
# top-level def, `method:C#m` otherwise), then unions the recorded negative-dependents of those keys.
|
|
1272
1352
|
# Issue #1120 — plus the consumers of any refinement name the edit moved ({#refinement_affected}). Issue #1367 —
|
|
1273
1353
|
# and every file whose last result holds a `global.*` diagnostic ({#global_write_reporters}). Issue #1459 — and
|
|
1274
|
-
# the consumers of any method name a file started or stopped defining ({#defined_name_affected}).
|
|
1354
|
+
# the consumers of any method name a file started or stopped defining ({#defined_name_affected}). Issue #2134 —
|
|
1355
|
+
# and the consumers of any visibility slot a file moved ({#visibility_name_affected}).
|
|
1275
1356
|
def negative_affected(changed, removed, new_fingerprints, new_class_decls, new_constant_decls, summary = nil)
|
|
1276
1357
|
appeared_methods = Incremental.appeared_symbols(changed, @symbol_fingerprints, new_fingerprints)
|
|
1277
1358
|
# Issue #639 — a class DISAPPEARING satisfies the `class:` kind too, now that a resolved bare
|
|
@@ -1288,11 +1369,18 @@ module Rigor
|
|
|
1288
1369
|
keys = appeared_methods.map { |symbol| negative_key_for(symbol) }
|
|
1289
1370
|
keys.concat(class_move_keys(moved_classes))
|
|
1290
1371
|
keys.concat(moved_constants.map { |name| "constant:#{name.split('::').last}" })
|
|
1291
|
-
Incremental.negative_closure(keys, @negative_dependents) |
|
|
1292
|
-
defined_name_affected(changed + removed, summary) | mark_affected(changed + removed, summary) |
|
|
1372
|
+
Incremental.negative_closure(keys, @negative_dependents) | table_diff_affected(changed + removed, summary) |
|
|
1293
1373
|
global_write_reporters
|
|
1294
1374
|
end
|
|
1295
1375
|
|
|
1376
|
+
# The consumers of the name-keyed edges whose producers diff a per-file table between each file's seed bundle
|
|
1377
|
+
# and the scan.
|
|
1378
|
+
def table_diff_affected(paths, summary)
|
|
1379
|
+
refinement_affected(paths, summary) | defined_name_affected(paths, summary) |
|
|
1380
|
+
visibility_name_affected(paths, summary) | mark_affected(paths, summary) |
|
|
1381
|
+
global_census_affected(paths, summary) | ivar_set_affected(paths, summary)
|
|
1382
|
+
end
|
|
1383
|
+
|
|
1296
1384
|
# Issue #1833 — the consumers whose answer read a class mark that moved in this edit. A reader of a class's
|
|
1297
1385
|
# envelope bucket records `mark:<last segment>` (`Scope#parameter_envelopes_of`), whichever way it answered, so
|
|
1298
1386
|
# a file that adds or drops only a mark (`x.singleton_class.prepend(Comparable)`, `o.extend(Helper)` in a
|
|
@@ -1319,7 +1407,8 @@ module Rigor
|
|
|
1319
1407
|
keys = moved.map { |(class_name, _mark)| "mark:#{class_name.split('::').last}" }.uniq
|
|
1320
1408
|
keys.concat(mixin_keys(tainted))
|
|
1321
1409
|
extended = Scope::DiscoveryIndex::ENVELOPE_OBJECT_EXTENDED_MARK
|
|
1322
|
-
|
|
1410
|
+
meta = Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK
|
|
1411
|
+
keys << "mark:*" if moved.any? { |(_class_name, mark)| mark == extended || mark == meta }
|
|
1323
1412
|
Incremental.negative_closure(keys, @negative_dependents)
|
|
1324
1413
|
end
|
|
1325
1414
|
|
|
@@ -1379,8 +1468,55 @@ module Rigor
|
|
|
1379
1468
|
Incremental.negative_closure(names.map { |name| "defines:#{name}" }, @negative_dependents)
|
|
1380
1469
|
end
|
|
1381
1470
|
|
|
1471
|
+
# Issue #2158 — the consumers whose `global.*` write rule read a census entry that a file in `paths` started or
|
|
1472
|
+
# stopped writing. The before-state is each file's seed bundle, the after-state the scan's per-file census. The
|
|
1473
|
+
# write the census silenced reports nothing, so {#global_write_reporters} does not reach it.
|
|
1474
|
+
def global_census_affected(paths, summary)
|
|
1475
|
+
after = (summary && summary[:global_write_census]) || {}
|
|
1476
|
+
before = paths.to_h { |path| [path, @seed_bundles.dig(path, :global_write_census)] }
|
|
1477
|
+
keys = Incremental.changed_global_census_keys(paths, before, after)
|
|
1478
|
+
return Set.new if keys.empty?
|
|
1479
|
+
|
|
1480
|
+
Incremental.negative_closure(keys, @negative_dependents)
|
|
1481
|
+
end
|
|
1482
|
+
|
|
1483
|
+
# Issue #1154 — the consumers whose declared-parameter ivar credit read an ivar writer entry a file in `paths`
|
|
1484
|
+
# started or stopped recording ({GlobalWriteCensus.ivar_key}: `ivar_set:@x`, `ivar_set_class:K`,
|
|
1485
|
+
# `own_write:K#@x`). The before-state is each file's seed bundle, the after-state the scan's per-file census. The
|
|
1486
|
+
# write sits in a method body, so neither the declaration signature nor the symbol fingerprints move with it.
|
|
1487
|
+
def ivar_set_affected(paths, summary)
|
|
1488
|
+
after = (summary && summary[:global_write_census]) || {}
|
|
1489
|
+
keys = Set.new
|
|
1490
|
+
paths.each do |path|
|
|
1491
|
+
census = @seed_bundles.dig(path, :global_write_census) || Inference::GlobalWriteCensus::EMPTY
|
|
1492
|
+
before = Inference::GlobalWriteCensus.ivar_set_entries(census)
|
|
1493
|
+
now = Inference::GlobalWriteCensus.ivar_set_entries(after[path] || Inference::GlobalWriteCensus::EMPTY)
|
|
1494
|
+
(before ^ now).each { |entry| keys << Inference::GlobalWriteCensus.ivar_key(entry) }
|
|
1495
|
+
end
|
|
1496
|
+
return Set.new if keys.empty?
|
|
1497
|
+
|
|
1498
|
+
Incremental.negative_closure(keys.to_a, @negative_dependents)
|
|
1499
|
+
end
|
|
1500
|
+
|
|
1501
|
+
# Issue #2134 — the consumers whose answer read a class's visibility slot for a method, or its contest, for each
|
|
1502
|
+
# slot whose entries a file in `paths` added, dropped or changed (`Scope#discovered_method_visibility`). The
|
|
1503
|
+
# before-state is each file's seed bundle, the after-state the scan's per-file entries. A `private :foo` in a
|
|
1504
|
+
# reopening or a `class_eval` block moves no `def`, so neither the symbol fingerprints nor the defined-name diff
|
|
1505
|
+
# sees it.
|
|
1506
|
+
def visibility_name_affected(paths, summary)
|
|
1507
|
+
after = (summary && summary[:visibility_entries]) || {}
|
|
1508
|
+
before = paths.to_h do |path|
|
|
1509
|
+
bundle = @seed_bundles[path]
|
|
1510
|
+
[path, bundle && Inference::ScopeIndexer.visibility_entries(bundle)]
|
|
1511
|
+
end
|
|
1512
|
+
slots = Incremental.changed_visibility_slots(paths, before, after)
|
|
1513
|
+
return Set.new if slots.empty?
|
|
1514
|
+
|
|
1515
|
+
Incremental.negative_closure(slots.map { |slot| "visibility:#{slot}" }, @negative_dependents)
|
|
1516
|
+
end
|
|
1517
|
+
|
|
1382
1518
|
# The qualified class/module names declared in the pre-parsed `index` (shared with
|
|
1383
|
-
# {#symbol_fingerprints_from_index}, so the changed set is parsed once)
|
|
1519
|
+
# {#symbol_fingerprints_from_index}, so the changed set is parsed once), keyed by path.
|
|
1384
1520
|
def class_declarations_from_index(index)
|
|
1385
1521
|
return {} if index.nil?
|
|
1386
1522
|
|
|
@@ -1541,7 +1677,22 @@ module Rigor
|
|
|
1541
1677
|
# and (#1898) their effect collections, so the run closes its effect graph over the whole project.
|
|
1542
1678
|
def served_state(reused)
|
|
1543
1679
|
{ served_per_file_diagnostics: @cache.slice(*reused),
|
|
1544
|
-
served_effect_collections: @effect_collections.slice(*reused)
|
|
1680
|
+
served_effect_collections: @effect_collections.slice(*reused),
|
|
1681
|
+
served_run_disclosures: @plugin_disclosures.served(reused, @stale_disclosures) }
|
|
1682
|
+
end
|
|
1683
|
+
|
|
1684
|
+
# Issue #2020 — folds the run's run-scoped registrations into the ledger, refreshing the keys this recheck found
|
|
1685
|
+
# stale. Extracted to keep {#absorb} under its ABC budget.
|
|
1686
|
+
def absorb_disclosures(runner, analyze_set)
|
|
1687
|
+
@plugin_disclosures.absorb(runner.analysis_disclosures, analyze_set, @stale_disclosures)
|
|
1688
|
+
@stale_disclosures = Set.new
|
|
1689
|
+
end
|
|
1690
|
+
|
|
1691
|
+
# Issue #2020 — the run-scoped plugin rows whose reads went stale ({RunDisclosureLedger}): one registering file
|
|
1692
|
+
# each joins the closure, and their replays are withheld until {#absorb} refreshes them from that file.
|
|
1693
|
+
def disclosure_refreshers(removed)
|
|
1694
|
+
@stale_disclosures = @plugin_disclosures.stale_identities { |row| plugin_read_fresh?(row) }
|
|
1695
|
+
@plugin_disclosures.refreshers(@stale_disclosures, removed.to_set)
|
|
1545
1696
|
end
|
|
1546
1697
|
|
|
1547
1698
|
# Issue #1898 — the plugin effect-fact digest a run's collections were stamped under. A plugin writes its
|
|
@@ -1560,6 +1711,21 @@ module Rigor
|
|
|
1560
1711
|
effect_plugin_digest_of(runner) != @effect_plugin_digest
|
|
1561
1712
|
end
|
|
1562
1713
|
|
|
1714
|
+
# PR #2286 review M1 — whether the ADR-16 synthetic-method index `runner` built differs from the one the
|
|
1715
|
+
# served files were analysed under. The index comes from class-body DSL calls (`devise :confirmable`,
|
|
1716
|
+
# a Tier C `attribute`), which no declaration signature records and no dependency edge names, so a file
|
|
1717
|
+
# that dispatched through a synthesised member is not in the closure of the edit that removed it. The
|
|
1718
|
+
# comparison waits for the run because the index is built by its pre-pass; a move re-analyses the project.
|
|
1719
|
+
def synthetic_surface_moved?(runner, reused)
|
|
1720
|
+
return false if reused.empty?
|
|
1721
|
+
|
|
1722
|
+
synthetic_surface_digest_of(runner) != @synthetic_surface_digest
|
|
1723
|
+
end
|
|
1724
|
+
|
|
1725
|
+
def synthetic_surface_digest_of(runner)
|
|
1726
|
+
runner.synthetic_method_index&.surface_digest
|
|
1727
|
+
end
|
|
1728
|
+
|
|
1563
1729
|
# The answer {#recheck} gives when {#served_effects_stale?}: a full baseline, reported as a recheck that
|
|
1564
1730
|
# re-analysed every file. {#run_incremental} reads `rebaselined` so it neither runs a second baseline
|
|
1565
1731
|
# nor reports the run as warm.
|
|
@@ -1644,19 +1810,12 @@ module Rigor
|
|
|
1644
1810
|
def rebind_buffer(files)
|
|
1645
1811
|
return if @buffer.nil? || files.include?(@buffer.logical_path)
|
|
1646
1812
|
|
|
1647
|
-
target = real_path(@buffer.logical_path)
|
|
1648
|
-
return if target.nil?
|
|
1813
|
+
target = BufferBinding.real_path(@buffer.logical_path)
|
|
1649
1814
|
|
|
1650
|
-
match = files.find { |path| real_path(path) == target }
|
|
1815
|
+
match = files.find { |path| BufferBinding.real_path(path) == target }
|
|
1651
1816
|
@buffer = BufferBinding.new(logical_path: match, physical_path: @buffer.physical_path) if match
|
|
1652
1817
|
end
|
|
1653
1818
|
|
|
1654
|
-
def real_path(path)
|
|
1655
|
-
File.realpath(path)
|
|
1656
|
-
rescue StandardError
|
|
1657
|
-
nil
|
|
1658
|
-
end
|
|
1659
|
-
|
|
1660
1819
|
# True when `path`'s recorded stat entry proves it unchanged since the last analysis. Any stat / parse
|
|
1661
1820
|
# failure (missing entry, unreadable / vanished file) reads as NOT fresh (→ re-analyse), preserving the
|
|
1662
1821
|
# prior `digest(path) != recorded` "changed" semantics for a file that cannot be validated.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../rooted_glob"
|
|
4
|
+
require_relative "diagnostic"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
module Analysis
|
|
@@ -10,8 +11,13 @@ module Rigor
|
|
|
10
11
|
# is taken verbatim; anything else contributes no files here (the Runner layers its own path-error
|
|
11
12
|
# diagnostics and buffer / in-memory handling on top — none of which add or remove files for an ordinary
|
|
12
13
|
# on-disk check, so the two file sets agree).
|
|
14
|
+
#
|
|
15
|
+
# #1991 — also the one place a path that names neither a directory nor a `.rb` file is worded and
|
|
16
|
+
# severity-graded, so `rigor check` and `rigor sig-gen` report the same path error the same way.
|
|
13
17
|
module PathExpansion
|
|
14
18
|
RUBY_GLOB = "**/*.rb"
|
|
19
|
+
NO_SUCH_PATH = "no such file or directory"
|
|
20
|
+
NOT_A_RUBY_FILE = "not a Ruby file (expected `.rb` or a directory)"
|
|
15
21
|
|
|
16
22
|
module_function
|
|
17
23
|
|
|
@@ -28,10 +34,45 @@ module Rigor
|
|
|
28
34
|
end
|
|
29
35
|
end
|
|
30
36
|
|
|
37
|
+
# The `[path, message]` pairs for each of `paths` that names neither a directory nor a `.rb` file,
|
|
38
|
+
# worded as `rigor check` words them.
|
|
39
|
+
def unusable(paths)
|
|
40
|
+
Array(paths).each_with_object([]) do |path, bad|
|
|
41
|
+
next if File.directory?(path) || (File.file?(path) && path.end_with?(".rb"))
|
|
42
|
+
|
|
43
|
+
bad << [path, unusable_message(path)]
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def unusable_message(path)
|
|
48
|
+
File.exist?(path) ? NOT_A_RUBY_FILE : NO_SUCH_PATH
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The error diagnostics for `bad` (from {unusable}). A bad path among valid ones is a warning that it was
|
|
52
|
+
# skipped; a bad path that leaves nothing to analyse is an error, so a lone typo is not masked.
|
|
53
|
+
def diagnostics(bad, any_files:)
|
|
54
|
+
severity = any_files ? :warning : :error
|
|
55
|
+
suffix = any_files ? " (skipped)" : ""
|
|
56
|
+
bad.map do |path, message|
|
|
57
|
+
Diagnostic.new(path: path, line: 1, column: 1, message: "#{message}#{suffix}", severity: severity)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
31
61
|
def directory_files(dir, exclude_patterns, base: Dir.pwd)
|
|
32
62
|
reject_excluded(RootedGlob.glob(dir, RUBY_GLOB), exclude_patterns, base: base)
|
|
33
63
|
end
|
|
34
64
|
|
|
65
|
+
# The configured project files, expanded as `rigor check` expands its configured paths: a directory globs,
|
|
66
|
+
# any other entry (a file, or a path that does not exist) is kept verbatim and left to the reader to skip.
|
|
67
|
+
# The file list a cross-file discovery seed is folded over ({Protection::DiscoverySeed}).
|
|
68
|
+
def configured_files(configuration)
|
|
69
|
+
Array(configuration.paths).flat_map do |path|
|
|
70
|
+
next [path] unless File.directory?(path)
|
|
71
|
+
|
|
72
|
+
directory_files(path, configuration.exclude_patterns, base: configuration.exclude_base)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
35
76
|
# `exclude_patterns` are matched with `File.fnmatch?` WITHOUT `FNM_PATHNAME`, so `**`/`*` span separators
|
|
36
77
|
# (the patterns behave like substring globs) — the exact semantics the Runner's directory glob applies.
|
|
37
78
|
#
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Analysis
|
|
7
|
+
# Issue #2021 — the files a run analyses outside the configured `paths:` load through `require_relative`.
|
|
8
|
+
#
|
|
9
|
+
# A run over path arguments widens its discovery to the configured paths (#684), which is the whole project for
|
|
10
|
+
# a file inside them. A file outside them has no such project: `rigor check plugins/x/lib/rigor/plugin/x.rb`
|
|
11
|
+
# in a repository whose `paths:` is `lib` discovered `x.rb` and `lib` alone. The plugin's nested `Effects`,
|
|
12
|
+
# declared in the sibling `x/effects.rb`, was then invisible, the lexical lookup fell through to the
|
|
13
|
+
# project's `Rigor::Effects`, and every call on it drew a false `call.undefined-method` that checking the
|
|
14
|
+
# plugin's `lib` did not.
|
|
15
|
+
#
|
|
16
|
+
# The file names its siblings itself: Ruby loads what it `require_relative`s before any of its code runs, so
|
|
17
|
+
# those declarations are in scope at runtime. {.closure} follows that graph from the out-of-paths files,
|
|
18
|
+
# through each target that is itself outside the configured paths. A target under a configured path is
|
|
19
|
+
# already discovered and stops the walk there, so the files inside `paths:` are discovered exactly as a run
|
|
20
|
+
# over the configured paths discovers them. Only a string literal counts (`require_relative "a/b"`, with or
|
|
21
|
+
# without the `.rb`); a computed name, `require`, `load` and `autoload` are not followed, so a gem's own
|
|
22
|
+
# files and a Zeitwerk-loaded tree are not reached this way.
|
|
23
|
+
#
|
|
24
|
+
# The walk sees one entry point's load graph, and a file it does not reach may redefine what a walked file
|
|
25
|
+
# defines. The runner therefore discovers the walked files as uncertain: their declarations resolve constants
|
|
26
|
+
# and their methods exist, but no return type, arity or visibility is read from them.
|
|
27
|
+
module RequireRelativeClosure
|
|
28
|
+
# The walk's answer: the existing targets it reached, and the targets it named that are not files (each an
|
|
29
|
+
# input of the answer too, since one appearing would join the discovery).
|
|
30
|
+
Result = Data.define(:found, :missing)
|
|
31
|
+
|
|
32
|
+
module_function
|
|
33
|
+
|
|
34
|
+
# @param seeds — the analysed files outside the configured paths, as spelled by the run.
|
|
35
|
+
# @param covered — `->(real_path) { Boolean }`, whether a target lies under a configured path, where discovery
|
|
36
|
+
# holds it already. The walk neither adds nor follows such a target. Paths are compared as real paths, so a
|
|
37
|
+
# symlinked spelling of a configured file is covered too.
|
|
38
|
+
# @param read — `->(path) { String }`, the source of a file (an editor buffer's bytes for its file).
|
|
39
|
+
# @return a {Result}; `found` holds absolute paths in walk order, each file once, and never a seed.
|
|
40
|
+
def closure(seeds, covered:, read:)
|
|
41
|
+
seen = seeds.to_set { |path| real_path(path) }
|
|
42
|
+
queue = seeds.dup
|
|
43
|
+
found = []
|
|
44
|
+
missing = []
|
|
45
|
+
until queue.empty?
|
|
46
|
+
targets_of(queue.shift, read).each do |target|
|
|
47
|
+
next missing << target unless File.file?(target)
|
|
48
|
+
|
|
49
|
+
real = real_path(target)
|
|
50
|
+
next if covered.call(real) || !seen.add?(real)
|
|
51
|
+
|
|
52
|
+
found << target
|
|
53
|
+
queue << target
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
Result.new(found: found, missing: missing.uniq)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def real_path(path)
|
|
60
|
+
File.realpath(path)
|
|
61
|
+
rescue SystemCallError
|
|
62
|
+
File.absolute_path(path)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The `.rb` paths `path` names with a literal `require_relative`, absolute, whether or not they exist.
|
|
66
|
+
def targets_of(path, read)
|
|
67
|
+
source = read.call(path)
|
|
68
|
+
return [] if source.nil?
|
|
69
|
+
|
|
70
|
+
base = File.dirname(File.absolute_path(path))
|
|
71
|
+
literal_features(Prism.parse(source).value).map do |feature|
|
|
72
|
+
File.expand_path(feature.end_with?(".rb") ? feature : "#{feature}.rb", base)
|
|
73
|
+
end
|
|
74
|
+
rescue StandardError
|
|
75
|
+
[]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# In source order, so discovery meets the targets in the order Ruby loads them.
|
|
79
|
+
def literal_features(root)
|
|
80
|
+
calls = []
|
|
81
|
+
stack = [root]
|
|
82
|
+
until stack.empty?
|
|
83
|
+
node = stack.pop
|
|
84
|
+
calls << node if literal_feature(node)
|
|
85
|
+
stack.concat(node.compact_child_nodes)
|
|
86
|
+
end
|
|
87
|
+
calls.sort_by { |call| call.location.start_offset }.filter_map { |call| literal_feature(call) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def literal_feature(node)
|
|
91
|
+
return nil unless node.is_a?(Prism::CallNode) && node.name == :require_relative && node.receiver.nil?
|
|
92
|
+
|
|
93
|
+
arguments = node.arguments&.arguments
|
|
94
|
+
return nil unless arguments&.size == 1 && arguments.first.is_a?(Prism::StringNode)
|
|
95
|
+
|
|
96
|
+
arguments.first.unescaped
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -25,7 +25,7 @@ module Rigor
|
|
|
25
25
|
# profile, taken from `Configuration::SeverityProfile::PROFILES`.
|
|
26
26
|
# - `evidence_tier` — `:high` / `:medium` / `:low` (or `nil` for informational helpers), Rigor's own
|
|
27
27
|
# confidence that a firing is a true positive, derived from the rule's firing gates. `:high` rules
|
|
28
|
-
# fire only on a concrete, statically-
|
|
28
|
+
# fire only on a concrete, statically-known type with no metaprogramming escape (Rigor's
|
|
29
29
|
# false-positive discipline has already filtered the uncertain cases); `:medium` rules rest on flow /
|
|
30
30
|
# inference proofs that inherit a documented FP envelope (loop / mutation / RBS-strictness modelling
|
|
31
31
|
# gaps); `:low` rules are resolution- or coverage-gap signals where a firing often reflects missing
|
|
@@ -706,7 +706,11 @@ module Rigor
|
|
|
706
706
|
"Writes are in a singleton body — `def self.foo` or a `class << self` def — those track " \
|
|
707
707
|
"separately.",
|
|
708
708
|
"Writes are in a `def` inside an anonymous-class factory block (`Class.new do … end`, " \
|
|
709
|
-
"`Module.new`, `Struct.new`, `Data.define`): that body defines another class."
|
|
709
|
+
"`Module.new`, `Struct.new`, `Data.define`): that body defines another class.",
|
|
710
|
+
"The class descends from a plugin-declared per-invocation class (ADR-125; rigor-actionpack declares " \
|
|
711
|
+
"`ActionController::Base` / `ActionController::API`, rigor-actionmailer `ActionMailer::Base`), and the " \
|
|
712
|
+
"write and every conflicting write sit in different actions: public methods no callback macro or " \
|
|
713
|
+
"`helper_method` names. One action runs per instance."
|
|
710
714
|
],
|
|
711
715
|
suppression: "`# rigor:disable ivar-write-mismatch` on the offending write.",
|
|
712
716
|
severity_authored: :error,
|
|
@@ -1116,7 +1120,7 @@ module Rigor
|
|
|
1116
1120
|
|
|
1117
1121
|
module_function
|
|
1118
1122
|
|
|
1119
|
-
# Looks up a rule by canonical id, legacy alias, or family wildcard. Returns
|
|
1123
|
+
# Looks up a rule by canonical id, legacy alias, or family wildcard. Returns the matching entries:
|
|
1120
1124
|
#
|
|
1121
1125
|
# - canonical id → 1-element array,
|
|
1122
1126
|
# - legacy alias → 1-element array (resolved to canonical),
|