rigortype 0.3.8 → 0.4.0
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/capability_roles/capability_roles.rbs +36 -0
- data/data/core_overlay/enumerable.rbs +51 -0
- data/data/core_overlay/enumerator.rbs +84 -0
- data/data/core_overlay/hash_rbs3.rbs +41 -0
- data/data/core_overlay/process.rbs +40 -0
- data/data/core_overlay/string_io.rbs +33 -0
- data/data/effects/core.yml +3 -3
- data/data/gem_overlay/activesupport/core_ext.rbs +236 -17
- data/docs/handbook/02-everyday-types.md +17 -14
- data/docs/handbook/03-narrowing.md +132 -14
- data/docs/handbook/04-tuples-and-shapes.md +8 -6
- data/docs/handbook/05-methods-and-blocks.md +1 -1
- data/docs/handbook/06-classes.md +2 -2
- data/docs/handbook/07-rbs-and-extended.md +19 -1
- data/docs/handbook/10-sorbet.md +9 -10
- data/docs/handbook/11-sig-gen.md +525 -13
- data/docs/handbook/README.md +1 -1
- data/docs/handbook/appendix-elixir.md +2 -2
- data/docs/handbook/appendix-go.md +2 -2
- data/docs/handbook/appendix-java-csharp.md +2 -2
- data/docs/handbook/appendix-mypy.md +2 -2
- data/docs/handbook/appendix-phpstan.md +1 -1
- data/docs/handbook/appendix-rust.md +1 -1
- data/docs/handbook/appendix-type-theory.md +4 -4
- data/docs/handbook/appendix-typescript.md +1 -1
- data/docs/llms.txt +2 -0
- data/docs/manual/02-cli-reference.md +95 -6
- data/docs/manual/03-configuration.md +18 -0
- data/docs/manual/04-diagnostics.md +15 -0
- data/docs/manual/07-plugins.md +15 -5
- data/docs/manual/08-skills.md +23 -2
- data/docs/manual/10-mcp-server.md +3 -2
- data/docs/manual/11-ci.md +9 -0
- data/docs/manual/15-type-protection-coverage.md +8 -0
- data/docs/manual/16-rbs-extended-annotations.md +46 -8
- data/docs/manual/18-removing-dead-code.md +10 -8
- data/docs/manual/19-effect-labels.md +10 -0
- data/docs/manual/plugins/README.md +7 -0
- data/docs/manual/plugins/rigor-actioncable.md +8 -1
- data/docs/manual/plugins/rigor-actionmailer.md +7 -0
- data/docs/manual/plugins/rigor-actionpack.md +261 -0
- data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
- data/docs/manual/plugins/rigor-activejob.md +7 -0
- data/docs/manual/plugins/rigor-activerecord.md +138 -4
- data/docs/manual/plugins/rigor-activestorage.md +8 -1
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +43 -7
- data/docs/manual/plugins/rigor-grape.md +106 -0
- data/docs/manual/plugins/rigor-graphql.md +23 -2
- data/docs/manual/plugins/rigor-pundit.md +8 -1
- data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
- data/docs/manual/plugins/rigor-rails-routes.md +7 -0
- data/docs/manual/plugins/rigor-rbs-inline.md +229 -1
- data/docs/manual/plugins/rigor-sidekiq.md +8 -1
- data/docs/manual/plugins/rigor-sorbet.md +21 -5
- data/exe/rigor +19 -4
- data/lib/rigor/analysis/baseline.rb +3 -3
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +1 -5
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +10 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +6 -4
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
- data/lib/rigor/analysis/check_rules/rule_ids.rb +21 -6
- data/lib/rigor/analysis/check_rules/rule_walk.rb +41 -5
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +1 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
- data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +38 -4
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
- data/lib/rigor/analysis/check_rules.rb +718 -79
- data/lib/rigor/analysis/crash_signature.rb +8 -24
- data/lib/rigor/analysis/dependency_recorder.rb +23 -0
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +0 -2
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +0 -2
- data/lib/rigor/analysis/dependency_source_inference/index.rb +5 -5
- data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +1 -2
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
- data/lib/rigor/analysis/effects_cache_probe.rb +3 -4
- data/lib/rigor/analysis/erb_template_detector.rb +1 -2
- data/lib/rigor/analysis/fact_store.rb +15 -1
- data/lib/rigor/analysis/incremental.rb +42 -0
- data/lib/rigor/analysis/incremental_session.rb +117 -26
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
- data/lib/rigor/analysis/project_scan.rb +11 -1
- data/lib/rigor/analysis/reachability/graph.rb +3 -5
- data/lib/rigor/analysis/reachability/plugin_roots.rb +5 -5
- data/lib/rigor/analysis/reachability/project_files.rb +2 -2
- data/lib/rigor/analysis/reachability/scan.rb +5 -5
- data/lib/rigor/analysis/reachability/scan_cache.rb +3 -2
- data/lib/rigor/analysis/reachability/signature_scan.rb +2 -2
- data/lib/rigor/analysis/result.rb +1 -3
- data/lib/rigor/analysis/rule_catalog.rb +131 -1
- data/lib/rigor/analysis/run_cache_key.rb +21 -12
- data/lib/rigor/analysis/run_cache_probe.rb +3 -5
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -8
- data/lib/rigor/analysis/runner/declaration_position.rb +1 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +340 -33
- data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +18 -14
- data/lib/rigor/analysis/runner/pool_coordinator.rb +357 -97
- data/lib/rigor/analysis/runner/project_pre_passes.rb +38 -13
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +407 -51
- data/lib/rigor/analysis/template_unit_collector.rb +303 -0
- data/lib/rigor/analysis/template_unit_paths.rb +91 -0
- data/lib/rigor/analysis/template_unit_positions.rb +293 -0
- data/lib/rigor/analysis/template_units.rb +399 -0
- data/lib/rigor/analysis/worker_session.rb +62 -20
- data/lib/rigor/bleeding_edge.rb +10 -24
- data/lib/rigor/builtins/hkt_builtins.rb +3 -2
- data/lib/rigor/builtins/imported_refinements.rb +159 -21
- data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
- data/lib/rigor/builtins/regex_refinement.rb +21 -13
- data/lib/rigor/builtins/static_return_refinements.rb +7 -6
- data/lib/rigor/cache/annotation_location.rb +2 -4
- data/lib/rigor/cache/descriptor.rb +63 -20
- data/lib/rigor/cache/engine_source.rb +33 -5
- data/lib/rigor/cache/file_digest.rb +9 -1
- data/lib/rigor/cache/incremental_snapshot.rb +72 -6
- data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +0 -3
- data/lib/rigor/cache/rbs_class_type_param_names.rb +0 -3
- data/lib/rigor/cache/rbs_constant_table.rb +0 -3
- data/lib/rigor/cache/rbs_descriptor.rb +112 -14
- data/lib/rigor/cache/rbs_environment.rb +8 -4
- data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
- data/lib/rigor/cache/store.rb +76 -32
- data/lib/rigor/ci_detector.rb +1 -0
- data/lib/rigor/cli/annotate_command.rb +5 -6
- data/lib/rigor/cli/check_command.rb +57 -10
- data/lib/rigor/cli/check_invocation.rb +6 -11
- data/lib/rigor/cli/check_runner_factory.rb +1 -5
- data/lib/rigor/cli/coverage_command.rb +1 -1
- data/lib/rigor/cli/coverage_mutation.rb +12 -3
- data/lib/rigor/cli/coverage_scan.rb +1 -5
- data/lib/rigor/cli/diff_command.rb +1 -1
- data/lib/rigor/cli/doc_links.rb +4 -4
- data/lib/rigor/cli/docs_command.rb +6 -6
- data/lib/rigor/cli/doctor_command.rb +85 -38
- data/lib/rigor/cli/effects_command.rb +2 -2
- data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
- data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +34 -1
- data/lib/rigor/cli/fused_protection_report.rb +7 -1
- data/lib/rigor/cli/lsp_command.rb +1 -1
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
- data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
- data/lib/rigor/cli/mutation_protection_report.rb +7 -1
- data/lib/rigor/cli/plugin_command.rb +4 -4
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/prism_colorizer.rb +1 -2
- data/lib/rigor/cli/protection_fork_scan.rb +6 -6
- data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
- data/lib/rigor/cli/sig_gen_command.rb +212 -31
- data/lib/rigor/cli/skill_command.rb +2 -2
- data/lib/rigor/cli/skill_deep_probe.rb +4 -4
- data/lib/rigor/cli/skill_describe.rb +35 -35
- data/lib/rigor/cli/trace_command.rb +4 -4
- data/lib/rigor/cli/trace_renderer.rb +5 -6
- data/lib/rigor/cli/triage_command.rb +1 -1
- data/lib/rigor/cli/type_of_command.rb +29 -11
- data/lib/rigor/cli/type_of_renderer.rb +20 -9
- data/lib/rigor/cli/type_of_template_probe.rb +189 -0
- data/lib/rigor/cli/type_scan_command.rb +4 -4
- data/lib/rigor/cli/unused_command.rb +11 -3
- data/lib/rigor/cli/upgrade_command.rb +1 -1
- data/lib/rigor/cli.rb +89 -9
- data/lib/rigor/config_audit.rb +30 -5
- data/lib/rigor/configuration/severity_profile.rb +25 -9
- data/lib/rigor/configuration.rb +109 -7
- data/lib/rigor/effects/ancestry_recorder.rb +191 -0
- data/lib/rigor/effects/attribution.rb +12 -5
- data/lib/rigor/effects/callee_rule.rb +368 -0
- data/lib/rigor/effects/catalog.rb +7 -4
- data/lib/rigor/effects/collector.rb +11 -5
- data/lib/rigor/effects/config_envelopes.rb +15 -10
- data/lib/rigor/effects/definition_context.rb +179 -0
- data/lib/rigor/effects/definition_lines.rb +25 -6
- data/lib/rigor/effects/effect_table.rb +11 -4
- data/lib/rigor/effects/entry_points.rb +0 -2
- data/lib/rigor/effects/envelope_check.rb +9 -9
- data/lib/rigor/effects/envelope_index.rb +20 -8
- data/lib/rigor/effects/file_collection.rb +59 -5
- data/lib/rigor/effects/framework_units.rb +5 -7
- data/lib/rigor/effects/identity.rb +20 -6
- data/lib/rigor/effects/inline_anchor.rb +7 -7
- data/lib/rigor/effects/label_intent.rb +3 -4
- data/lib/rigor/effects/liskov_check.rb +8 -8
- data/lib/rigor/effects/local_ownership.rb +38 -12
- data/lib/rigor/effects/method_key.rb +22 -1
- data/lib/rigor/effects/mutation_classifier.rb +23 -12
- data/lib/rigor/effects/plugin_facts.rb +47 -37
- data/lib/rigor/effects/propagator.rb +297 -19
- data/lib/rigor/effects/registry.rb +1 -1
- data/lib/rigor/effects/scanner.rb +122 -73
- data/lib/rigor/effects/signature_sources.rb +4 -6
- data/lib/rigor/effects/snapshot.rb +12 -11
- data/lib/rigor/effects/snapshot_diff.rb +1 -1
- data/lib/rigor/effects/summary.rb +27 -4
- data/lib/rigor/effects/unit_scan.rb +393 -38
- data/lib/rigor/effects/unknown_label_check.rb +3 -8
- data/lib/rigor/effects/unknown_label_report.rb +3 -4
- data/lib/rigor/effects/visibility.rb +101 -0
- data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
- data/lib/rigor/environment/class_registry.rb +3 -1
- data/lib/rigor/environment/failure_slot.rb +2 -2
- data/lib/rigor/environment/installed_gem_set.rb +85 -0
- data/lib/rigor/environment/lockfile_resolver.rb +68 -4
- data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
- data/lib/rigor/environment/member_consistency.rb +298 -0
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
- data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
- data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
- data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
- data/lib/rigor/environment/rbs_loader.rb +674 -79
- data/lib/rigor/environment.rb +174 -24
- data/lib/rigor/flow_contribution/merger.rb +0 -2
- data/lib/rigor/flow_contribution.rb +11 -13
- data/lib/rigor/hashing/xxh3.rb +264 -0
- data/lib/rigor/inference/acceptance.rb +292 -17
- data/lib/rigor/inference/block_auto_splat.rb +216 -0
- data/lib/rigor/inference/block_call_timing.rb +338 -0
- data/lib/rigor/inference/block_parameter_binder.rb +75 -30
- data/lib/rigor/inference/block_repetition.rb +71 -0
- data/lib/rigor/inference/body_fixpoint.rb +7 -6
- data/lib/rigor/inference/budget_trace.rb +3 -6
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
- data/lib/rigor/inference/captured_locals.rb +389 -18
- data/lib/rigor/inference/closure_escape_analyzer.rb +159 -17
- data/lib/rigor/inference/content_join.rb +200 -27
- data/lib/rigor/inference/coverage_scanner.rb +4 -6
- data/lib/rigor/inference/def_return_typer.rb +11 -7
- data/lib/rigor/inference/define_method_block_self.rb +64 -0
- data/lib/rigor/inference/dynamic_origin.rb +1 -1
- data/lib/rigor/inference/element_read_widening.rb +180 -0
- data/lib/rigor/inference/error_info.rb +196 -0
- data/lib/rigor/inference/expression_typer.rb +2124 -468
- data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
- data/lib/rigor/inference/fork_map.rb +5 -7
- data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
- data/lib/rigor/inference/global_write_census.rb +239 -0
- data/lib/rigor/inference/guard_rebinding.rb +447 -0
- data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
- data/lib/rigor/inference/hkt_reducer.rb +2 -3
- data/lib/rigor/inference/hkt_registry.rb +4 -7
- data/lib/rigor/inference/index_write_widening.rb +16 -7
- data/lib/rigor/inference/indexed_narrowing.rb +61 -9
- data/lib/rigor/inference/jump_targets.rb +82 -0
- data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
- data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
- data/lib/rigor/inference/last_line.rb +340 -0
- data/lib/rigor/inference/last_status.rb +144 -0
- data/lib/rigor/inference/macro_block_self_type.rb +168 -21
- data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
- data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
- data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
- data/lib/rigor/inference/match_rebinding.rb +392 -0
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +87 -33
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +165 -111
- data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
- data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
- data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +1046 -69
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +129 -54
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -6
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +225 -25
- data/lib/rigor/inference/method_parameter_binder.rb +12 -9
- data/lib/rigor/inference/multi_target_binder.rb +341 -55
- data/lib/rigor/inference/mutation_rejoin.rb +171 -0
- data/lib/rigor/inference/mutation_widening.rb +156 -85
- data/lib/rigor/inference/narrowing.rb +835 -162
- data/lib/rigor/inference/operand_effects.rb +167 -0
- data/lib/rigor/inference/operand_walk.rb +88 -0
- data/lib/rigor/inference/optimistic_origin.rb +153 -16
- data/lib/rigor/inference/origin_lookup.rb +2 -3
- data/lib/rigor/inference/parameter_inference_collector.rb +7 -7
- data/lib/rigor/inference/pre_eval_constants.rb +6 -5
- data/lib/rigor/inference/precision_scanner.rb +3 -4
- data/lib/rigor/inference/project_method_ownership.rb +136 -0
- data/lib/rigor/inference/project_patched_methods.rb +9 -4
- data/lib/rigor/inference/project_patched_scanner.rb +10 -6
- data/lib/rigor/inference/protection_scanner.rb +1 -2
- data/lib/rigor/inference/range_constant.rb +57 -0
- data/lib/rigor/inference/rbs_type_translator.rb +49 -11
- data/lib/rigor/inference/receiver_alias.rb +93 -4
- data/lib/rigor/inference/receiver_blind_block.rb +219 -0
- data/lib/rigor/inference/refinement_mutation.rb +76 -0
- data/lib/rigor/inference/repeated_or_writes.rb +463 -0
- data/lib/rigor/inference/return_barrier.rb +54 -0
- data/lib/rigor/inference/rewrite_mutation.rb +120 -0
- data/lib/rigor/inference/scope_indexer.rb +5228 -627
- data/lib/rigor/inference/statement_evaluator.rb +3452 -595
- data/lib/rigor/inference/stored_block_call.rb +54 -0
- data/lib/rigor/inference/string_mutation.rb +97 -0
- data/lib/rigor/inference/struct_fold_safety.rb +6 -7
- data/lib/rigor/inference/synthetic_method_index.rb +1 -2
- data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
- data/lib/rigor/inference/unknown_store_widening.rb +200 -0
- data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
- data/lib/rigor/inference/version_guard.rb +28 -21
- data/lib/rigor/inference/void_origin.rb +3 -3
- data/lib/rigor/inference/void_tail_summary.rb +2 -4
- data/lib/rigor/language_server/buffer_table.rb +4 -4
- data/lib/rigor/language_server/completion_provider.rb +1 -1
- data/lib/rigor/language_server/debouncer.rb +0 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +7 -6
- data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
- data/lib/rigor/language_server/folding_range_provider.rb +1 -1
- data/lib/rigor/language_server/hover_provider.rb +1 -1
- data/lib/rigor/language_server/hover_renderer.rb +4 -4
- data/lib/rigor/language_server/incremental_sync.rb +6 -6
- data/lib/rigor/language_server/project_context.rb +7 -8
- data/lib/rigor/language_server/publish_batcher.rb +2 -2
- data/lib/rigor/language_server/selection_range_provider.rb +2 -2
- data/lib/rigor/language_server/server.rb +9 -9
- data/lib/rigor/language_server/signature_help_provider.rb +1 -1
- data/lib/rigor/language_server/uri.rb +1 -1
- data/lib/rigor/mcp/server.rb +2 -1
- data/lib/rigor/plugin/base.rb +258 -26
- data/lib/rigor/plugin/box_probe.rb +91 -0
- data/lib/rigor/plugin/bundled_catalog.rb +167 -0
- data/lib/rigor/plugin/effect_attribution.rb +65 -11
- data/lib/rigor/plugin/effect_entry_points.rb +3 -3
- data/lib/rigor/plugin/fact_store.rb +6 -6
- data/lib/rigor/plugin/io_boundary.rb +93 -8
- data/lib/rigor/plugin/isolation.rb +29 -6
- data/lib/rigor/plugin/loader.rb +33 -13
- data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
- data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
- data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
- data/lib/rigor/plugin/manifest.rb +94 -11
- data/lib/rigor/plugin/registry.rb +76 -12
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +21 -4
- data/lib/rigor/plugin/template_unit.rb +196 -0
- data/lib/rigor/plugin/trust_policy.rb +2 -4
- data/lib/rigor/plugin/type_node_resolver.rb +3 -3
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/plugin_gap_advisory.rb +96 -0
- data/lib/rigor/project_environment.rb +138 -0
- data/lib/rigor/protection/analysis_guard.rb +93 -14
- data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
- data/lib/rigor/protection/dependency_closure.rb +5 -8
- data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
- data/lib/rigor/protection/discovery_seed.rb +14 -12
- data/lib/rigor/protection/kill_signature.rb +1 -4
- data/lib/rigor/protection/measurement_integrity.rb +1 -3
- data/lib/rigor/protection/mutation_cache.rb +7 -11
- data/lib/rigor/protection/mutation_scanner.rb +12 -15
- data/lib/rigor/protection/mutator.rb +2 -1
- data/lib/rigor/protection/test_suite_oracle.rb +5 -5
- data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
- data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
- data/lib/rigor/rbs_extended/reporter.rb +36 -10
- data/lib/rigor/rbs_extended.rb +88 -21
- data/lib/rigor/reflection/constant_ancestors.rb +97 -0
- data/lib/rigor/reflection/constant_path.rb +139 -0
- data/lib/rigor/reflection.rb +93 -110
- data/lib/rigor/runtime/jit.rb +6 -8
- data/lib/rigor/scope/discovery_index.rb +165 -2
- data/lib/rigor/scope.rb +1041 -32
- data/lib/rigor/sig_gen/alias_index.rb +289 -0
- data/lib/rigor/sig_gen/classification.rb +22 -5
- data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
- data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
- data/lib/rigor/sig_gen/generator.rb +673 -53
- data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
- data/lib/rigor/sig_gen/layout_index.rb +3 -4
- data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
- data/lib/rigor/sig_gen/method_candidate.rb +47 -3
- data/lib/rigor/sig_gen/observation_collector.rb +18 -16
- data/lib/rigor/sig_gen/path_mapper.rb +4 -6
- data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
- data/lib/rigor/sig_gen/renderer.rb +128 -14
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +154 -0
- data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
- data/lib/rigor/sig_gen/write_result.rb +19 -3
- data/lib/rigor/sig_gen/writer.rb +189 -41
- data/lib/rigor/sig_gen.rb +3 -0
- data/lib/rigor/signature_path_audit.rb +153 -6
- data/lib/rigor/source/literals.rb +0 -23
- data/lib/rigor/source/node_children.rb +0 -2
- data/lib/rigor/source/node_locator.rb +5 -11
- data/lib/rigor/source/node_walker.rb +0 -6
- data/lib/rigor/source/parameter_envelope.rb +72 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/triage/catalogue.rb +1 -3
- data/lib/rigor/triage.rb +3 -5
- data/lib/rigor/type/accepts_result.rb +24 -6
- data/lib/rigor/type/combinator.rb +144 -5
- data/lib/rigor/type/data_class.rb +2 -2
- data/lib/rigor/type/data_instance.rb +4 -4
- data/lib/rigor/type/difference.rb +1 -0
- data/lib/rigor/type/float_range.rb +128 -0
- data/lib/rigor/type/hash_shape.rb +6 -6
- data/lib/rigor/type/integer_range.rb +13 -8
- data/lib/rigor/type/nominal.rb +8 -2
- data/lib/rigor/type/refined.rb +4 -3
- data/lib/rigor/type/struct_class.rb +3 -3
- data/lib/rigor/type/struct_instance.rb +4 -4
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/generic.rb +1 -1
- data/lib/rigor/type_node/range_literal.rb +25 -0
- data/lib/rigor/type_node/resolver_chain.rb +1 -1
- data/lib/rigor/type_node.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +10 -5
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +14 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +32 -30
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +264 -3
- data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
- data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
- data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
- data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +2 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +10 -5
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +383 -7
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +322 -41
- data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +26 -14
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +290 -19
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +3 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +2 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
- data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
- data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
- data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
- data/plugins/rigor-grape/sig/grape.rbs +263 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +78 -5
- data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -3
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +12 -7
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +36 -37
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -10
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +5 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +3 -2
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +10 -17
- data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
- data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +324 -18
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
- data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +2 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +10 -5
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +3 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +51 -6
- data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
- data/sig/rigor/analysis/baseline.rbs +69 -7
- data/sig/rigor/analysis/fact_store.rbs +1 -1
- data/sig/rigor/analysis/project_scan.rbs +74 -0
- data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
- data/sig/rigor/cache.rbs +2 -3
- data/sig/rigor/effects/config_envelopes.rbs +100 -0
- data/sig/rigor/effects/effect_table.rbs +60 -0
- data/sig/rigor/effects/envelope.rbs +97 -0
- data/sig/rigor/effects/envelope_index.rbs +33 -0
- data/sig/rigor/effects/file_collection.rbs +81 -0
- data/sig/rigor/effects/label.rbs +26 -0
- data/sig/rigor/effects/label_set.rbs +46 -0
- data/sig/rigor/effects/method_key.rbs +24 -0
- data/sig/rigor/effects/origin.rbs +50 -0
- data/sig/rigor/effects/plugin_facts.rbs +141 -0
- data/sig/rigor/effects/registry.rbs +68 -0
- data/sig/rigor/effects/summary.rbs +50 -0
- data/sig/rigor/effects/taint_cause.rbs +12 -0
- data/sig/rigor/environment.rbs +17 -10
- data/sig/rigor/inference/optimistic_origin.rbs +10 -0
- data/sig/rigor/inference.rbs +9 -14
- data/sig/rigor/plugin/additional_initializer.rbs +36 -0
- data/sig/rigor/plugin/base.rbs +30 -7
- data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
- data/sig/rigor/plugin/effect_attribution.rbs +66 -0
- data/sig/rigor/plugin/effect_edge.rbs +33 -0
- data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
- data/sig/rigor/plugin/io_boundary.rbs +4 -0
- data/sig/rigor/plugin/loader.rbs +3 -3
- data/sig/rigor/plugin/manifest.rbs +51 -11
- data/sig/rigor/plugin/protocol_contract.rbs +68 -0
- data/sig/rigor/plugin/registry.rbs +62 -1
- data/sig/rigor/plugin.rbs +1 -1
- data/sig/rigor/rbs_extended.rbs +1 -1
- data/sig/rigor/reflection.rbs +9 -6
- data/sig/rigor/scope.rbs +148 -12
- data/sig/rigor/sig_gen/skip_reason_catalog.rbs +21 -0
- data/sig/rigor/source.rbs +4 -4
- data/sig/rigor/testing.rbs +10 -4
- data/sig/rigor/type.rbs +33 -1
- data/sig/rigor.rbs +39 -20
- data/skills/rigor-ask/SKILL.md +16 -11
- data/skills/rigor-baseline-reduce/SKILL.md +4 -6
- data/skills/rigor-ci-setup/SKILL.md +17 -21
- data/skills/rigor-doctor/SKILL.md +24 -22
- data/skills/rigor-doctor/references/01-checks.md +97 -33
- data/skills/rigor-editor-setup/SKILL.md +6 -4
- data/skills/rigor-mcp-setup/SKILL.md +5 -4
- data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
- data/skills/rigor-next-steps/SKILL.md +4 -2
- data/skills/rigor-plugin-author/SKILL.md +19 -23
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +25 -27
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
- data/skills/rigor-plugin-review/SKILL.md +6 -4
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
- data/skills/rigor-plugin-tune/SKILL.md +4 -2
- data/skills/rigor-project-init/SKILL.md +15 -10
- data/skills/rigor-project-init/references/01-detect.md +13 -9
- data/skills/rigor-project-init/references/02-configure.md +43 -14
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
- data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
- data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
- data/skills/rigor-protection-uplift/SKILL.md +4 -6
- data/skills/rigor-rbs-setup/SKILL.md +4 -2
- data/skills/rigor-type-oracle/SKILL.md +226 -0
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +265 -0
- data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
- data/skills/rigor-type-oracle/references/03-gap-protocol.md +125 -0
- data/skills/rigor-unused-adjudicate/SKILL.md +12 -7
- data/skills/rigor-upgrade/SKILL.md +13 -8
- metadata +133 -3
- data/lib/rigor/cli/probe_environment.rb +0 -85
- data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Effects
|
|
7
|
+
# Callee resolution for plugin attribution rows (#1048; ADR-103 WD10 / WD13; design note § 11.2).
|
|
8
|
+
#
|
|
9
|
+
# A framework method can be an **edge** as well as a label. `render :show` inside `UsersController`
|
|
10
|
+
# runs `app/views/users/show.html.erb`, synchronously and in-process, and since #393 that template is
|
|
11
|
+
# an effect unit keyed `view:users/show.html` sitting in the very same summaries table. Nothing
|
|
12
|
+
# produced the edge, because the only plugin-facing edge surface ({Plugin::EffectEdge}) names a
|
|
13
|
+
# receiver *class* and mints units on a class body, and {Plugin::EffectAttribution} carried labels but
|
|
14
|
+
# no callee.
|
|
15
|
+
#
|
|
16
|
+
# This module is the missing half, and it is deliberately shaped exactly like {Narrowing}: the plugin
|
|
17
|
+
# writes a **rule name** on its row (`callee: "rails_render"`) and the engine owns the strategy. A
|
|
18
|
+
# block would have to run inside the per-file effect scan — the one place ADR-103 WD13 forbids
|
|
19
|
+
# anything that resolves, walks or types — and would not survive the fork-pool / Ractor boundary. A
|
|
20
|
+
# name is a String: declarative, Marshal-clean, and reviewable in the plugin's manifest.
|
|
21
|
+
#
|
|
22
|
+
# ## What a rule may read
|
|
23
|
+
#
|
|
24
|
+
# **The call's own argument literals, the unit's owner class, and the unit's own key.** No dataflow,
|
|
25
|
+
# no typer question, no filesystem. A rule that needed to know whether `app/views/users/show.html.erb`
|
|
26
|
+
# exists would make a plugin row's meaning a function of the view tree, which the scan does not have
|
|
27
|
+
# and must not read; instead an unresolvable answer is `nil` and a resolvable-looking one that no unit
|
|
28
|
+
# answers is **dropped by the propagator**, which restores the row's taint from
|
|
29
|
+
# {FileCollection::Edge#taint_if_unresolved}. So "the template is not in the table" and "the argument
|
|
30
|
+
# was computed" both keep the `template-not-analysed` taint, and only a render that reached a real
|
|
31
|
+
# unit clears it.
|
|
32
|
+
#
|
|
33
|
+
# ## Two kinds of rule
|
|
34
|
+
#
|
|
35
|
+
# - a **site rule** ({SITE_RULES}) answers for one call node — `render :show`, `render partial: "card"`;
|
|
36
|
+
# - a **unit rule** ({UNIT_RULES}) answers for a whole unit and reads no node at all. Rails' *implicit*
|
|
37
|
+
# render is the case that needs one: an action that falls off its end without rendering still renders
|
|
38
|
+
# `<controller>/<action>`, and the fact that produces the edge is the **absence** of a call. Only the
|
|
39
|
+
# unit scan can observe that, which is why the rule is applied there rather than in
|
|
40
|
+
# {FrameworkUnits} — a class body cannot see which of its methods responded.
|
|
41
|
+
#
|
|
42
|
+
# A unit rule contributes an **edge and nothing else**: no labels, no taint. That is what keeps it
|
|
43
|
+
# FP-safe on the private helper a controller also defines — `def load_user` gets an edge to
|
|
44
|
+
# `view:users/load_user.html`, no unit answers it, and the unit is exactly as it was.
|
|
45
|
+
module CalleeRule
|
|
46
|
+
# The callee an applied rule names, as the two halves {FileCollection::Edge} carries. The key the
|
|
47
|
+
# propagator reconstructs is `"#{receiver}.#{selector}"` — `view:users/show` + `html`.
|
|
48
|
+
#
|
|
49
|
+
# `fallbacks` is the ordered list of selectors the propagator retries when `selector` resolves to
|
|
50
|
+
# nothing (#1065), or nil for none. The rule only COPIES it off the plugin's table: whether
|
|
51
|
+
# `view:users/_row.js` exists is a question about the merged table, which the scan cannot ask.
|
|
52
|
+
Callee = Data.define(:receiver, :selector, :fallbacks) do
|
|
53
|
+
def initialize(fallbacks: nil, **) = super
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Must agree with {MethodKey::TEMPLATE_UNIT_PREFIX} and `Plugin::TemplateUnit::KEY_PREFIX`; pinned
|
|
57
|
+
# equal by spec.
|
|
58
|
+
TEMPLATE_PREFIX = "view:"
|
|
59
|
+
|
|
60
|
+
# The format a controller action renders absent a `formats:` override — Rails' own default, and the
|
|
61
|
+
# only one a `render :show` with no other evidence may be read as.
|
|
62
|
+
DEFAULT_FORMAT = "html"
|
|
63
|
+
|
|
64
|
+
# Rules that answer for one call node.
|
|
65
|
+
SITE_RULES = %w[rails_render rails_render_partial].freeze
|
|
66
|
+
|
|
67
|
+
# Rules that answer for a whole unit, from its owner and its own key. A UNIT rule may additionally
|
|
68
|
+
# be applied **per format arm** of a `respond_to` block (#1071): the arm is the unit's own body, and
|
|
69
|
+
# the arm's format is a literal the unit scan read, so it travels as data the same way the unit's key
|
|
70
|
+
# does.
|
|
71
|
+
UNIT_RULES = %w[rails_implicit_render].freeze
|
|
72
|
+
|
|
73
|
+
RULES = (SITE_RULES + UNIT_RULES).freeze
|
|
74
|
+
|
|
75
|
+
# `render json:` / `plain:` / `inline:` and friends render no template. They are listed so the rule
|
|
76
|
+
# can decline rather than read a `partial:` that is not there — declining keeps the row's taint,
|
|
77
|
+
# which is the conservative answer for a shape this rule does not model.
|
|
78
|
+
NON_TEMPLATE_OPTIONS = %w[json xml plain text html body js inline file nothing].freeze
|
|
79
|
+
|
|
80
|
+
# Template handlers, which a logical name never carries: `Plugin::TemplateUnit#logical_name` is
|
|
81
|
+
# `users/show.html`, not `users/show.html.erb`. An author may still write the handler out —
|
|
82
|
+
# `render template: "users/show.html.erb"` is legal Rails — so it is stripped rather than left to
|
|
83
|
+
# build a key no unit could ever answer.
|
|
84
|
+
HANDLERS = %w[erb haml slim jbuilder builder rabl ruby].freeze
|
|
85
|
+
|
|
86
|
+
module_function
|
|
87
|
+
|
|
88
|
+
def known?(name)
|
|
89
|
+
RULES.include?(name.to_s)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def site_rule?(name)
|
|
93
|
+
SITE_RULES.include?(name.to_s)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def unit_rule?(name)
|
|
97
|
+
UNIT_RULES.include?(name.to_s)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Applies a site rule to one call node.
|
|
101
|
+
#
|
|
102
|
+
# @param name — the row's `callee:`
|
|
103
|
+
# @param node — the `Prism::CallNode` the row matched
|
|
104
|
+
# @param owner_class — the unit's owner (`"UsersController"`, `"ActionView::Base"`)
|
|
105
|
+
# @param unit_key — the unit's own key: a selector for a method, `view:users/show.html` for a
|
|
106
|
+
# template unit
|
|
107
|
+
# @param fallbacks — the row's `callee_fallbacks:` table (#1065). Only a rule whose selector the
|
|
108
|
+
# CONTEXT supplied consults it; see {rails_render_partial}.
|
|
109
|
+
# @return the {Callee} the rule named, or nil whenever it cannot settle the target from
|
|
110
|
+
# literals alone
|
|
111
|
+
def site(name, node, owner_class:, unit_key: nil, fallbacks: nil)
|
|
112
|
+
case name.to_s
|
|
113
|
+
when "rails_render" then rails_render(node, owner_class)
|
|
114
|
+
when "rails_render_partial" then rails_render_partial(node, unit_key, fallbacks)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Applies a unit rule. Reads no node; a `format:` from a `respond_to` arm (#1071) narrows the
|
|
119
|
+
# template the rule names from the default to that arm's own. Nil, or absent (`DEFAULT_FORMAT`).
|
|
120
|
+
#
|
|
121
|
+
# @return the {Callee} the rule named, or nil.
|
|
122
|
+
def unit(name, owner_class:, unit_key: nil, format: nil)
|
|
123
|
+
case name.to_s
|
|
124
|
+
when "rails_implicit_render" then rails_implicit_render(owner_class, unit_key, format: format)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# `render` inside a controller. The positional form names an **action template**
|
|
129
|
+
# (`render :show` → `users/show`), which is the one place a controller and a view disagree about
|
|
130
|
+
# what a bare string means.
|
|
131
|
+
#
|
|
132
|
+
# Never consults a format fallback (#1065). The format here is either one the author wrote, or
|
|
133
|
+
# Rails' `html` default standing in for a REQUEST format the rule cannot see — and the lookup order
|
|
134
|
+
# a controller-side render follows is derived from that request (`request.formats`, an `Accept`
|
|
135
|
+
# header), not from anything in the source.
|
|
136
|
+
def rails_render(node, owner_class)
|
|
137
|
+
directory = controller_directory(owner_class)
|
|
138
|
+
return nil if directory.nil?
|
|
139
|
+
|
|
140
|
+
format = format_for(node, DEFAULT_FORMAT)
|
|
141
|
+
return nil if format.nil?
|
|
142
|
+
|
|
143
|
+
name = template_name(node, directory) || partial_name(node, directory, layout: false)
|
|
144
|
+
name.nil? ? nil : template_callee(name, format)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# `render` inside a template. A bare positional argument is a **partial** here, and so is `layout:`
|
|
148
|
+
# — in a view `render layout: "shared/wrapper"` names `shared/_wrapper`, not an `app/views/layouts`
|
|
149
|
+
# file, because `RenderingHelper#render` rewrites `layout:` to `partial:` when a block is given.
|
|
150
|
+
# Since #1047 a layout compiles to a unit like any other template, so such an edge resolves where
|
|
151
|
+
# the named partial exists and keeps its taint where it does not.
|
|
152
|
+
#
|
|
153
|
+
# **The format fallback (#1065).** The format travels from the enclosing unit, and while a `.js.erb`
|
|
154
|
+
# template is rendering Rails' lookup context holds `[:js, :html]` — `LookupContext#formats=` appends
|
|
155
|
+
# `:html` to a lone `:js`, and `AbstractRenderer#prepend_formats` puts the template's own format in
|
|
156
|
+
# front of the request's. So `render partial: "watchers"` from `_set_watcher.js.erb` runs
|
|
157
|
+
# `_watchers.js.erb` where one exists and `_watchers.html.erb` otherwise. The rule copies the row's
|
|
158
|
+
# table for the inherited format onto the callee, and the propagator takes the first key that
|
|
159
|
+
# resolves.
|
|
160
|
+
#
|
|
161
|
+
# The table is consulted only for an INHERITED format. A `formats:` / `format:` keyword or a format
|
|
162
|
+
# spelled into the name is the author's word, and the fallback stands down: that is the direction
|
|
163
|
+
# that keeps a taint rather than guessing at a lookup the call overrode.
|
|
164
|
+
def rails_render_partial(node, unit_key, fallbacks)
|
|
165
|
+
directory, inherited = template_context(unit_key)
|
|
166
|
+
return nil if directory.nil?
|
|
167
|
+
|
|
168
|
+
format = format_for(node, inherited)
|
|
169
|
+
return nil if format.nil?
|
|
170
|
+
|
|
171
|
+
retry_formats = format_keyword?(node) ? nil : fallbacks&.fetch(format, nil)
|
|
172
|
+
explicit = keyword_name(node, "template")
|
|
173
|
+
return template_callee(qualify(explicit, directory), format, retry_formats) if explicit
|
|
174
|
+
|
|
175
|
+
name = partial_name(node, directory) || positional_partial(node, directory)
|
|
176
|
+
name.nil? ? nil : template_callee(name, format, retry_formats)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Rails' implicit render: an action that never rendered still renders `<controller>/<action>`. A
|
|
180
|
+
# `respond_to` arm (#1071) is the same convention per format: `format.js` with no responding block
|
|
181
|
+
# renders `<controller>/<action>.js`, so the rule narrows the format it answers for, and an arm that
|
|
182
|
+
# IS answered outright is not applied at all (the unit scan decides that half and calls with the
|
|
183
|
+
# arm's format only for arms that still fall through).
|
|
184
|
+
def rails_implicit_render(owner_class, unit_key, format: nil)
|
|
185
|
+
directory = controller_directory(owner_class)
|
|
186
|
+
return nil if directory.nil? || unit_key.nil?
|
|
187
|
+
|
|
188
|
+
action = unit_key.to_s
|
|
189
|
+
return nil unless /\A[a-z_][A-Za-z0-9_]*[?!=]?\z/.match?(action)
|
|
190
|
+
return nil if action.end_with?("?", "!", "=")
|
|
191
|
+
|
|
192
|
+
template_callee("#{directory}/#{action}", format || DEFAULT_FORMAT)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# `UsersController` → `users`; `Admin::UsersController` → `admin/users`. A class whose name does not
|
|
196
|
+
# end in `Controller` is not one this rule can read a view directory off, and answers nil.
|
|
197
|
+
def controller_directory(owner_class)
|
|
198
|
+
return nil if owner_class.nil?
|
|
199
|
+
|
|
200
|
+
segments = owner_class.to_s.split("::")
|
|
201
|
+
last = segments.pop
|
|
202
|
+
return nil unless last&.end_with?("Controller") && last != "Controller"
|
|
203
|
+
|
|
204
|
+
segments.push(last.delete_suffix("Controller"))
|
|
205
|
+
segments.map { |segment| underscore(segment) }.join("/")
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# `[directory, format]` for a template unit's own key — `view:users/show.html` → `["users", "html"]`.
|
|
209
|
+
# A partial rendered from a `.json` template is a `.json` partial, which is why the format travels.
|
|
210
|
+
def template_context(unit_key)
|
|
211
|
+
key = unit_key.to_s
|
|
212
|
+
return [nil, nil] unless key.start_with?(TEMPLATE_PREFIX)
|
|
213
|
+
|
|
214
|
+
logical = key.delete_prefix(TEMPLATE_PREFIX)
|
|
215
|
+
name, _, format = logical.rpartition(".")
|
|
216
|
+
return [nil, nil] if name.empty?
|
|
217
|
+
|
|
218
|
+
directory = name.include?("/") ? name[0...name.rindex("/")] : ""
|
|
219
|
+
[directory, format]
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# The explicit template spellings: `template: "users/show"` and `action: :edit`.
|
|
223
|
+
def template_name(node, directory)
|
|
224
|
+
explicit = keyword_name(node, "template")
|
|
225
|
+
return qualify(explicit, directory) if explicit
|
|
226
|
+
|
|
227
|
+
action = keyword_name(node, "action")
|
|
228
|
+
return "#{directory}/#{action}" if action && !action.include?("/")
|
|
229
|
+
return action if action
|
|
230
|
+
|
|
231
|
+
positional_template(node, directory)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# `render :show` / `render "show"` / `render "admin/form"`, but only when no option keyword the rule
|
|
235
|
+
# does not model is present.
|
|
236
|
+
def positional_template(node, directory)
|
|
237
|
+
name = literal_name(positional(node).first)
|
|
238
|
+
return nil if name.nil?
|
|
239
|
+
|
|
240
|
+
qualify(name, directory)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# `render "card"` inside a template — the same literal, read as a partial.
|
|
244
|
+
def positional_partial(node, directory)
|
|
245
|
+
name = literal_name(positional(node).first)
|
|
246
|
+
name.nil? ? nil : partialize(qualify(name, directory))
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# `partial: "card"` / `partial: "users/card"`, and the view-side `layout:`. `collection:` changes
|
|
250
|
+
# how many times the partial runs and not which one, so it is read and ignored on purpose: an
|
|
251
|
+
# effect summary is an upper bound over the body, not a count.
|
|
252
|
+
def partial_name(node, directory, layout: true)
|
|
253
|
+
name = keyword_name(node, "partial") || (layout ? keyword_name(node, "layout") : nil)
|
|
254
|
+
name.nil? ? nil : partialize(qualify(name, directory))
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# The requested format, or nil to decline. Absent is `fallback`; a literal narrows; anything
|
|
258
|
+
# computed is genuinely unknown and must not be guessed at `html`.
|
|
259
|
+
def format_for(node, fallback)
|
|
260
|
+
return nil if non_template?(node)
|
|
261
|
+
|
|
262
|
+
value = keyword_argument(node, "formats") || keyword_argument(node, "format")
|
|
263
|
+
return fallback if value.nil?
|
|
264
|
+
|
|
265
|
+
literal = literal_name(value) || literal_name(array_head(value))
|
|
266
|
+
literal&.split(".")&.last
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def format_keyword?(node)
|
|
270
|
+
!(keyword_argument(node, "formats") || keyword_argument(node, "format")).nil?
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def non_template?(node)
|
|
274
|
+
NON_TEMPLATE_OPTIONS.any? { |option| keyword_argument(node, option) }
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def array_head(node)
|
|
278
|
+
node.elements.first if node.is_a?(Prism::ArrayNode)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# `fallbacks` survives only when the name spelled no format of its own: `render "row.json"` from a
|
|
282
|
+
# `.js` template is the author naming `json`, and a `js` table has nothing to say about it.
|
|
283
|
+
def template_callee(name, format, fallbacks = nil)
|
|
284
|
+
spelled = split_suffixes(name, nil).last
|
|
285
|
+
name, format = split_suffixes(name, format)
|
|
286
|
+
return nil if name.nil? || name.empty? || format.nil? || format.empty?
|
|
287
|
+
return nil if name.include?(" ") || format.include?(" ") || format.include?(".")
|
|
288
|
+
|
|
289
|
+
fallbacks = nil unless spelled.nil? && fallbacks && !fallbacks.empty?
|
|
290
|
+
Callee.new(receiver: "#{TEMPLATE_PREFIX}#{name}", selector: format, fallbacks: fallbacks)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Splits a written handler and format off the name's last segment, so `render "show.json"` names
|
|
294
|
+
# `view:users/show` + `json` and `render template: "users/show.html.erb"` names
|
|
295
|
+
# `view:users/show` + `html` rather than the impossible `view:users/show.html.erb.html`. A name
|
|
296
|
+
# that spells a format wins over the rule's default, because the author said so; a name that
|
|
297
|
+
# spells only a handler keeps the default.
|
|
298
|
+
def split_suffixes(name, format)
|
|
299
|
+
return [name, format] if name.nil?
|
|
300
|
+
|
|
301
|
+
directory, separator, base = name.rpartition("/")
|
|
302
|
+
segments = base.split(".")
|
|
303
|
+
return [name, format] if segments.length <= 1
|
|
304
|
+
|
|
305
|
+
stem = segments.shift
|
|
306
|
+
segments.reject! { |segment| HANDLERS.include?(segment) }
|
|
307
|
+
["#{directory}#{separator}#{stem}", segments.first || format]
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# `"card"` in `users` → `"users/_card"`; `"admin/card"` → `"admin/_card"`. A name already spelled
|
|
311
|
+
# with the underscore keeps it, because that is what Rails accepts too.
|
|
312
|
+
def partialize(name)
|
|
313
|
+
return nil if name.nil?
|
|
314
|
+
|
|
315
|
+
directory, separator, base = name.rpartition("/")
|
|
316
|
+
return base.start_with?("_") ? name : "_#{name}" if separator.empty?
|
|
317
|
+
|
|
318
|
+
base.start_with?("_") ? name : "#{directory}/_#{base}"
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# A name with a `/` is rooted at the views directory; a bare one is relative to the rendering
|
|
322
|
+
# unit's own directory.
|
|
323
|
+
def qualify(name, directory)
|
|
324
|
+
return nil if name.nil? || name.empty?
|
|
325
|
+
return name if name.include?("/") || directory.nil? || directory.empty?
|
|
326
|
+
|
|
327
|
+
"#{directory}/#{name}"
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def keyword_name(node, key)
|
|
331
|
+
literal_name(keyword_argument(node, key))
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def literal_name(value)
|
|
335
|
+
case value
|
|
336
|
+
when Prism::SymbolNode, Prism::StringNode then value.unescaped
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def positional(node)
|
|
341
|
+
node.arguments&.arguments&.grep_v(Prism::KeywordHashNode) || []
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def keyword_argument(node, name)
|
|
345
|
+
hash = node.arguments&.arguments&.find { |argument| argument.is_a?(Prism::KeywordHashNode) }
|
|
346
|
+
pair = hash&.elements&.find do |element|
|
|
347
|
+
element.is_a?(Prism::AssocNode) && element.key.is_a?(Prism::SymbolNode) &&
|
|
348
|
+
element.key.unescaped == name
|
|
349
|
+
end
|
|
350
|
+
pair&.value
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# `ActiveStorage` → `active_storage`. The engine's own reading of a constant segment; deliberately
|
|
354
|
+
# not an inflector call, because a rule may read nothing outside the strings it was given.
|
|
355
|
+
def underscore(segment)
|
|
356
|
+
segment.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
private_class_method :rails_render, :rails_render_partial, :rails_implicit_render,
|
|
360
|
+
:controller_directory, :template_context, :template_name,
|
|
361
|
+
:positional_template, :positional_partial, :partial_name,
|
|
362
|
+
:format_for, :format_keyword?, :non_template?, :array_head, :template_callee,
|
|
363
|
+
:split_suffixes, :partialize,
|
|
364
|
+
:qualify, :keyword_name, :literal_name, :positional, :keyword_argument,
|
|
365
|
+
:underscore
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
end
|
|
@@ -36,14 +36,17 @@ module Rigor
|
|
|
36
36
|
class Error < StandardError
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
DATA_PATH = File.expand_path("../../../data/effects/core.yml", __dir__)
|
|
39
|
+
DATA_PATH = File.expand_path("../../../data/effects/core.yml", __dir__).freeze
|
|
40
40
|
|
|
41
41
|
# The mutator sets a value class may name, by reference. Adding a name here is the only way a
|
|
42
|
-
# class gets one — the data file may not spell a selector list of its own.
|
|
42
|
+
# class gets one — the data file may not spell a selector list of its own. Each is the set
|
|
43
|
+
# {MutationClassifier#mutating?} reads for that class, so a posture's answer and the classifier's
|
|
44
|
+
# cannot disagree: `hash` is the classifier's union of the widening's two Hash tables, because
|
|
45
|
+
# `compare_by_identity` mutates the receiver although the widening keeps it off `HASH_MUTATORS`.
|
|
43
46
|
MUTATOR_SETS = {
|
|
44
47
|
"array" => Inference::MutationWidening::ARRAY_MUTATORS,
|
|
45
|
-
"hash" =>
|
|
46
|
-
"string" =>
|
|
48
|
+
"hash" => MutationClassifier::HASH_MUTATORS,
|
|
49
|
+
"string" => Inference::StringMutation::MUTATORS
|
|
47
50
|
}.freeze
|
|
48
51
|
|
|
49
52
|
NO_MUTATORS = Set[].freeze
|
|
@@ -39,12 +39,16 @@ module Rigor
|
|
|
39
39
|
|
|
40
40
|
# Per-file, per-thread accumulator. Not frozen and never shared: it lives for one `analyze_file`.
|
|
41
41
|
class Accumulator
|
|
42
|
-
attr_reader :path, :calls, :attribution, :envelopes, :plugin_facts
|
|
42
|
+
attr_reader :path, :calls, :attribution, :envelopes, :plugin_facts, :unit_key, :unit_owner
|
|
43
43
|
attr_accessor :root
|
|
44
44
|
|
|
45
45
|
def initialize(path, attribution: Attribution.empty, envelopes: EnvelopeIndex.empty,
|
|
46
|
-
plugin_facts: PluginFacts.empty)
|
|
46
|
+
plugin_facts: PluginFacts.empty, unit_key: nil, unit_owner: nil)
|
|
47
47
|
@path = path
|
|
48
|
+
# #392 — set for a template unit, where the whole file is ONE effect unit keyed
|
|
49
|
+
# `view:<logical_name>` under the plugin's declared `self`.
|
|
50
|
+
@unit_key = unit_key
|
|
51
|
+
@unit_owner = unit_owner
|
|
48
52
|
@attribution = attribution
|
|
49
53
|
@envelopes = envelopes
|
|
50
54
|
@plugin_facts = plugin_facts
|
|
@@ -92,10 +96,11 @@ module Rigor
|
|
|
92
96
|
# {EnvelopeIndex} of #386, carried the same way and for the same reason — the declared lane a call
|
|
93
97
|
# site imports must not depend on which process typed the file.
|
|
94
98
|
def collect_for(path, attribution: Attribution.empty, envelopes: EnvelopeIndex.empty,
|
|
95
|
-
plugin_facts: PluginFacts.empty)
|
|
99
|
+
plugin_facts: PluginFacts.empty, unit_key: nil, unit_owner: nil)
|
|
96
100
|
previous = Thread.current[KEY]
|
|
97
101
|
accumulator = Accumulator.new(path.to_s, attribution: attribution, envelopes: envelopes,
|
|
98
|
-
plugin_facts: plugin_facts
|
|
102
|
+
plugin_facts: plugin_facts, unit_key: unit_key,
|
|
103
|
+
unit_owner: unit_owner)
|
|
99
104
|
Thread.current[KEY] = accumulator
|
|
100
105
|
@mutex.synchronize { @active_count += 1 }
|
|
101
106
|
yield
|
|
@@ -207,7 +212,8 @@ module Rigor
|
|
|
207
212
|
Scanner.scan(
|
|
208
213
|
root: accumulator.root, path: accumulator.path, calls: accumulator.calls,
|
|
209
214
|
attribution: accumulator.attribution, envelopes: accumulator.envelopes,
|
|
210
|
-
plugin_facts: accumulator.plugin_facts
|
|
215
|
+
plugin_facts: accumulator.plugin_facts, unit_key: accumulator.unit_key,
|
|
216
|
+
unit_owner: accumulator.unit_owner
|
|
211
217
|
)
|
|
212
218
|
rescue StandardError
|
|
213
219
|
FileCollection.new(path: accumulator.path, failed: true)
|
|
@@ -69,9 +69,8 @@ module Rigor
|
|
|
69
69
|
|
|
70
70
|
# Resolves `Configuration#effects_envelopes` against a registry.
|
|
71
71
|
#
|
|
72
|
-
# @param entries
|
|
73
|
-
# @param registry
|
|
74
|
-
# @return [Array<Entry>]
|
|
72
|
+
# @param entries — the loaded, shape-validated entries
|
|
73
|
+
# @param registry — the vocabulary, project extensions included
|
|
75
74
|
def build(entries:, registry:)
|
|
76
75
|
entries.each_with_index.map do |entry, index|
|
|
77
76
|
labels = Array(entry["effect"]).map(&:to_s)
|
|
@@ -86,11 +85,10 @@ module Rigor
|
|
|
86
85
|
|
|
87
86
|
# The class-level envelopes the entries put on a project.
|
|
88
87
|
#
|
|
89
|
-
# @param
|
|
90
|
-
# @param
|
|
91
|
-
# @param
|
|
92
|
-
# @
|
|
93
|
-
# @return [Hash{String => Envelope}] one envelope per selected class, keyed by class name
|
|
88
|
+
# @param class_names — every class the run collected units for
|
|
89
|
+
# @param sources — `Runner#effect_sources` — `{method key => [path]}`
|
|
90
|
+
# @param project_root — what `sources` paths are relativised against
|
|
91
|
+
# @return one envelope per selected class, keyed by class name
|
|
94
92
|
def for_classes(entries:, class_names:, sources: {}, project_root: Dir.pwd)
|
|
95
93
|
return NO_ENVELOPES if entries.empty?
|
|
96
94
|
|
|
@@ -115,6 +113,13 @@ module Rigor
|
|
|
115
113
|
)
|
|
116
114
|
end
|
|
117
115
|
|
|
116
|
+
# Whether one entry selects a class — by its fully-qualified name for a `namespace:` entry, by any
|
|
117
|
+
# file that defines it for a `match:` one.
|
|
118
|
+
#
|
|
119
|
+
# Public since #391: `rigor sig-gen`'s emission asks the same question this module's own
|
|
120
|
+
# {.for_classes} asks, and asks it with a defining file it already has. It was private while
|
|
121
|
+
# {.for_classes} was the only caller; a second caller with the same inputs is not a reason to
|
|
122
|
+
# reimplement the predicate.
|
|
118
123
|
def selects?(entry, class_name, paths)
|
|
119
124
|
return namespace_match?(entry.namespace, class_name) if entry.namespace
|
|
120
125
|
|
|
@@ -166,7 +171,7 @@ module Rigor
|
|
|
166
171
|
def files_by_class(sources, project_root)
|
|
167
172
|
root = "#{File.absolute_path(project_root.to_s).chomp('/')}/"
|
|
168
173
|
sources.each_with_object({}) do |(key, paths), out|
|
|
169
|
-
owner = MethodKey.
|
|
174
|
+
owner = MethodKey.envelope_owner(key)
|
|
170
175
|
next if owner.nil?
|
|
171
176
|
|
|
172
177
|
bucket = (out[owner] ||= [])
|
|
@@ -179,7 +184,7 @@ module Rigor
|
|
|
179
184
|
absolute.start_with?(root) ? absolute[root.length..] : path.to_s
|
|
180
185
|
end
|
|
181
186
|
|
|
182
|
-
private_class_method :
|
|
187
|
+
private_class_method :match_segments?, :match_deep?, :files_by_class, :relativize
|
|
183
188
|
end
|
|
184
189
|
end
|
|
185
190
|
end
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Effects
|
|
7
|
+
# Which side of its class a definition written at some point lands on, and what `self` its body runs
|
|
8
|
+
# on, or that the syntax does not say. The scanner keys a unit `Class.m` or `Class#m` from the first, and
|
|
9
|
+
# scans the body with the second (the singleton bit {MutationClassifier} and
|
|
10
|
+
# {LocalOwnership.constructor?} read).
|
|
11
|
+
#
|
|
12
|
+
# Ruby answers the two from different places, which is why one bit carried down the walk got both
|
|
13
|
+
# wrong in turn:
|
|
14
|
+
#
|
|
15
|
+
# - a receiver-less `def` lands on the **default definee**: the class the lexically enclosing body
|
|
16
|
+
# opened, which is the singleton class inside `class << self`. A method body does not move it, so
|
|
17
|
+
# `def inner` inside `def self.outer` defines `Foo#inner`, while inside a `def` in `class << self` it
|
|
18
|
+
# defines `Foo.inner`.
|
|
19
|
+
# - `define_method` and the `attr_*` macros are calls on **`self`**, so they define instance methods of
|
|
20
|
+
# the module `self` is: `Foo#x` inside `def self.setup`, where `self` is `Foo`, and `Foo.x` inside
|
|
21
|
+
# `class << self`, where it is `Foo`'s singleton class.
|
|
22
|
+
#
|
|
23
|
+
# Only the spellings that name the new `self` move either answer: `class << self`, and a block given to
|
|
24
|
+
# `class_eval` / `instance_eval` (or an `_exec` / `module_` alias) on `self` or on `singleton_class`.
|
|
25
|
+
# Where the syntax cannot say, the answer is **unknown** and a definition there is no unit. That covers
|
|
26
|
+
# the block of `Class.new` and its kin, an eval on any other receiver, `class << obj`, and a method
|
|
27
|
+
# defined on one object or on a singleton class's own singleton class. Filed under the enclosing class,
|
|
28
|
+
# such a definition would join that class's real method of the same name, and a `Class.new(self)`
|
|
29
|
+
# override shares its name by design.
|
|
30
|
+
#
|
|
31
|
+
# `self_kind` is `:instance`, `:class` (the class the enclosing namespace opened), `:singleton_class`
|
|
32
|
+
# (that class's singleton class) or `:unknown`, and `definee_singleton` is true, false, or nil when
|
|
33
|
+
# unknown. Every context is allocated once.
|
|
34
|
+
class DefinitionContext < Data.define(:self_kind, :definee_singleton)
|
|
35
|
+
CLASS_EVAL_CALLS = %i[class_eval class_exec module_eval module_exec].to_set.freeze
|
|
36
|
+
INSTANCE_EVAL_CALLS = %i[instance_eval instance_exec].to_set.freeze
|
|
37
|
+
# The builders whose block defines into a class or module the call itself creates.
|
|
38
|
+
ANONYMOUS_BUILDERS = { Class: :new, Module: :new, Struct: :new, Data: :define }.freeze
|
|
39
|
+
# The `self_kind`s that are a class object.
|
|
40
|
+
CLASS_SELVES = %i[class singleton_class].to_set.freeze
|
|
41
|
+
private_constant :CLASS_EVAL_CALLS, :INSTANCE_EVAL_CALLS, :ANONYMOUS_BUILDERS, :CLASS_SELVES
|
|
42
|
+
|
|
43
|
+
CONTEXTS = %i[instance class singleton_class unknown].to_h do |self_kind|
|
|
44
|
+
[self_kind, [false, true, nil].to_h do |definee|
|
|
45
|
+
[definee, new(self_kind: self_kind, definee_singleton: definee)]
|
|
46
|
+
end.freeze]
|
|
47
|
+
end.freeze
|
|
48
|
+
private_constant :CONTEXTS
|
|
49
|
+
|
|
50
|
+
def self.of(self_kind, definee_singleton)
|
|
51
|
+
CONTEXTS.fetch(self_kind).fetch(definee_singleton)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Whether a child of `node` may run under another context than `node` does: a call's block, or a
|
|
55
|
+
# `class <<` body. Only these need {#for_child}, and asking once per node rather than once per child
|
|
56
|
+
# keeps the walks from paying a call for every element of a large literal.
|
|
57
|
+
def self.rebinds?(node)
|
|
58
|
+
node.is_a?(Prism::SingletonClassNode) || (node.is_a?(Prism::CallNode) && node.block.is_a?(Prism::BlockNode))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# A class body.
|
|
62
|
+
CLASS_BODY = of(:class, false)
|
|
63
|
+
# A `class << self` body: `self` and the definee are both the singleton class.
|
|
64
|
+
SINGLETON_CLASS_BODY = of(:singleton_class, true)
|
|
65
|
+
# The body of an instance method of the class.
|
|
66
|
+
INSTANCE_METHOD_BODY = of(:instance, false)
|
|
67
|
+
# The top level, where `self` is `main`, an instance of `Object`, and a `def` defines `Object#m`.
|
|
68
|
+
TOP_LEVEL = INSTANCE_METHOD_BODY
|
|
69
|
+
# Where the syntax says neither what `self` is nor where a `def` lands. Every block inside stays here.
|
|
70
|
+
UNKNOWN = of(:unknown, nil)
|
|
71
|
+
|
|
72
|
+
# Whether `self` is a class object here — the singleton bit a unit's body is scanned with.
|
|
73
|
+
def singleton?
|
|
74
|
+
CLASS_SELVES.include?(self_kind)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Whether `self` is the singleton class here, so a call on it (`define_method`, an `attr_*` macro,
|
|
78
|
+
# `include`, `prepend`, `alias_method`) works on the singleton class rather than the class.
|
|
79
|
+
def self_singleton_class?
|
|
80
|
+
self_kind == :singleton_class
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The context the body of `def` node `node` runs under, or nil where no key names the method. The
|
|
84
|
+
# unit is keyed singleton exactly when that context is {#singleton?}. The body keeps this default
|
|
85
|
+
# definee: a `def` nested in it lands where one written beside it would.
|
|
86
|
+
#
|
|
87
|
+
# `def self.x` defines a singleton method only where `self` is the class. In an instance method it
|
|
88
|
+
# defines a method on one object, and in `class << self` one on the singleton class's own singleton
|
|
89
|
+
# class. `def Const.x` keeps the reading it has always had, a method of the class the unit is keyed
|
|
90
|
+
# under; any other receiver is an object no key names.
|
|
91
|
+
def def_body(node)
|
|
92
|
+
case node.receiver
|
|
93
|
+
when nil
|
|
94
|
+
DefinitionContext.of(definee_singleton ? :class : :instance, definee_singleton) unless definee_singleton.nil?
|
|
95
|
+
when Prism::SelfNode
|
|
96
|
+
self if self_kind == :class
|
|
97
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
98
|
+
DefinitionContext.of(:class, definee_singleton) unless self_kind == :unknown
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# The context the block of a literal-name `define_method` written here runs under, or nil where no key
|
|
103
|
+
# names the method; an `attr_*` macro here defines on the same side. Both are calls on `self`, which
|
|
104
|
+
# only a class and its singleton class answer, and the block keeps the definee it closed over.
|
|
105
|
+
def module_call_body
|
|
106
|
+
case self_kind
|
|
107
|
+
when :class then DefinitionContext.of(:instance, definee_singleton)
|
|
108
|
+
when :singleton_class then DefinitionContext.of(:class, definee_singleton)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The context `child` runs under as a child of `node`, a node {.rebinds?} accepts. A call's block and a
|
|
113
|
+
# `class <<` body may rebind `self`; every other child keeps this context. (A block is a call's only
|
|
114
|
+
# `BlockNode` child.)
|
|
115
|
+
def for_child(node, child)
|
|
116
|
+
case node
|
|
117
|
+
when Prism::CallNode then child.is_a?(Prism::BlockNode) ? block(node) : self
|
|
118
|
+
when Prism::SingletonClassNode then child.equal?(node.body) ? singleton_class_body(node.expression) : self
|
|
119
|
+
else self
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
# `class << self` on the class opens its singleton class; any other `class << …` opens one no key names.
|
|
126
|
+
def singleton_class_body(expression)
|
|
127
|
+
self_kind == :class && expression.is_a?(Prism::SelfNode) ? SINGLETON_CLASS_BODY : UNKNOWN
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def block(call)
|
|
131
|
+
name = call.name
|
|
132
|
+
if CLASS_EVAL_CALLS.include?(name) then class_eval_block(call.receiver)
|
|
133
|
+
elsif INSTANCE_EVAL_CALLS.include?(name) then instance_eval_block(call.receiver)
|
|
134
|
+
elsif anonymous_builder?(call) then UNKNOWN
|
|
135
|
+
else self
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# `class_eval` makes its receiver both `self` and the definee.
|
|
140
|
+
def class_eval_block(receiver)
|
|
141
|
+
if self_receiver?(receiver) && singleton? then DefinitionContext.of(self_kind, self_kind == :singleton_class)
|
|
142
|
+
elsif singleton_class_receiver?(receiver) then SINGLETON_CLASS_BODY
|
|
143
|
+
else UNKNOWN
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# `instance_eval` makes its receiver `self` and the receiver's singleton class the definee. On the
|
|
148
|
+
# class that is the singleton class. On the singleton class it is that class's own singleton class,
|
|
149
|
+
# which no key names, so a `define_method` there is keyed and a `def` is not.
|
|
150
|
+
def instance_eval_block(receiver)
|
|
151
|
+
if self_receiver?(receiver) && self_kind == :class then DefinitionContext.of(:class, true)
|
|
152
|
+
elsif (self_receiver?(receiver) && self_kind == :singleton_class) || singleton_class_receiver?(receiver)
|
|
153
|
+
DefinitionContext.of(:singleton_class, nil)
|
|
154
|
+
else UNKNOWN
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def self_receiver?(receiver)
|
|
159
|
+
receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# `singleton_class` called on the class.
|
|
163
|
+
def singleton_class_receiver?(receiver)
|
|
164
|
+
self_kind == :class && receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class &&
|
|
165
|
+
self_receiver?(receiver.receiver) && receiver.arguments.nil? && receiver.block.nil?
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# `Class.new { … }` and its kin, spelled on the core constant.
|
|
169
|
+
def anonymous_builder?(call)
|
|
170
|
+
receiver = call.receiver
|
|
171
|
+
name = case receiver
|
|
172
|
+
when Prism::ConstantReadNode then receiver.name
|
|
173
|
+
when Prism::ConstantPathNode then receiver.name if receiver.parent.nil?
|
|
174
|
+
end
|
|
175
|
+
!name.nil? && ANONYMOUS_BUILDERS[name] == call.name
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|