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,270 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
|
|
5
|
+
require "rigor/plugin"
|
|
6
|
+
require "rigor/plugin/isolation"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Plugin
|
|
10
|
+
class Actionpack < Rigor::Plugin::Base
|
|
11
|
+
# #393 — ERB → Ruby, with the line map the template-unit seam (#392) reports through.
|
|
12
|
+
#
|
|
13
|
+
# ## Which compiler
|
|
14
|
+
#
|
|
15
|
+
# [ADR-90](../../../../../../docs/adr/90-target-library-resolution-from-project-bundle.md): Erubi is
|
|
16
|
+
# **never bundled** — it is not a Rigor dependency and nothing here adds it to a Gemfile — but it is
|
|
17
|
+
# what Rails itself compiles with, so it is used when it resolves in the ANALYSED project's bundle,
|
|
18
|
+
# through the same `Isolation.require_with_target_bundle` path `Inflector` takes. Stdlib `ERB` is the
|
|
19
|
+
# fallback, and it is always there. Whichever ran is reported as the unit's `transform_id`
|
|
20
|
+
# (`erubi-1.13.1` / `erb-6.0.1.1`), which rides the unit digest — so a project that installs Erubi
|
|
21
|
+
# between two runs re-analyses rather than replaying the stdlib answer.
|
|
22
|
+
#
|
|
23
|
+
# ## The line map is measured, never assumed
|
|
24
|
+
#
|
|
25
|
+
# Both compilers are line-preserving: a tag's text is rewritten in place and the newlines between
|
|
26
|
+
# tags are emitted verbatim, which is why a Rails backtrace can name `show.html.erb:12`. What differs
|
|
27
|
+
# is the **prologue** — stdlib ERB emits a `#coding:` magic comment above the body, Erubi emits its
|
|
28
|
+
# buffer initialiser — and neither documents its height as API.
|
|
29
|
+
#
|
|
30
|
+
# So the height is measured rather than hardcoded: {.line_offset} compiles a probe template of marked
|
|
31
|
+
# lines once per compiler and reads back which compiled line the first marker landed on, checking at
|
|
32
|
+
# the same time that the following markers step by exactly one. A compiler that fails that check is
|
|
33
|
+
# refused (the caller declines the file) rather than trusted, because the failure mode of a wrong
|
|
34
|
+
# offset is every finding in every template reported at the wrong line — silent, and worse than no
|
|
35
|
+
# unit at all.
|
|
36
|
+
module ErbCompiler
|
|
37
|
+
# A compiler whose probe did not step line-for-line. Caught by {Actionpack#template_units_for_file},
|
|
38
|
+
# which declines the file.
|
|
39
|
+
class Unmappable < StandardError; end
|
|
40
|
+
|
|
41
|
+
# Three marked lines, each a STATEMENT tag. A tag's Ruby is emitted verbatim, so the marker is
|
|
42
|
+
# findable in the compiled source and each one is forced onto its own compiled line.
|
|
43
|
+
#
|
|
44
|
+
# Plain text would not do: consecutive text lines are one chunk and are emitted as ONE append
|
|
45
|
+
# carrying the newlines escaped inside the literal — the compiler then pads the missing lines out
|
|
46
|
+
# BELOW it, so a text probe reads as a collapse when the line numbering is in fact intact. That
|
|
47
|
+
# padding is exactly the property being measured, and a tag probe measures it without tripping
|
|
48
|
+
# over how the padding is spelled.
|
|
49
|
+
PROBE = (1..3).map { |n| "<% RIGOR_ERB_PROBE_#{n} %>\n" }.join.freeze
|
|
50
|
+
private_constant :PROBE
|
|
51
|
+
|
|
52
|
+
# An output tag whose Ruby OPENS A BLOCK: `<%= form_with(model: @user) do |f| %>`, the single most
|
|
53
|
+
# common shape in a real Rails view. Neither compiler handles it — both emit
|
|
54
|
+
# `_buf << (form_with(…) do |f|).to_s`, which is a syntax error, so every template using a form
|
|
55
|
+
# helper produced two parse diagnostics and no unit at all (431 of them on redmine, measured).
|
|
56
|
+
#
|
|
57
|
+
# Rails does not hit this because its own ERB handler carries exactly this rule
|
|
58
|
+
# (`ActionView::Template::Handlers::ERB::BLOCK_EXPR`) and emits `@output_buffer.append= expr`
|
|
59
|
+
# unwrapped for a matching tag. The same rule is applied here one step earlier, as a rewrite of
|
|
60
|
+
# the TEMPLATE rather than of a compiler's output: `<%=` becomes `<%` for such a tag, which is a
|
|
61
|
+
# same-width, same-line edit both compilers then handle, and the discarded value is the buffer
|
|
62
|
+
# append the design note says is not an origin anyway.
|
|
63
|
+
BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\z/
|
|
64
|
+
private_constant :BLOCK_EXPR
|
|
65
|
+
|
|
66
|
+
# The Erubi/Rails trim markers, which stdlib ERB only understands under a `trim_mode:` that also
|
|
67
|
+
# changes how it emits newlines. They are blanked instead — `<%-` → `<% `, `-%>` → ` %>` — which
|
|
68
|
+
# is the same-width, same-line rewrite {BLOCK_EXPR} uses and keeps the line map out of the
|
|
69
|
+
# compiler's trim rules entirely. Left in place under `trim_mode: nil` they parse as Ruby's unary
|
|
70
|
+
# minus and cost the template its unit (87 of redmine's 506, measured after the block-expression
|
|
71
|
+
# fix and before this one).
|
|
72
|
+
TRIM_OPEN = /<%-/
|
|
73
|
+
private_constant :TRIM_OPEN
|
|
74
|
+
|
|
75
|
+
TRIM_CLOSE = /-%>/
|
|
76
|
+
private_constant :TRIM_CLOSE
|
|
77
|
+
|
|
78
|
+
# #1047 — a LAYOUT's `yield`. `<%= yield %>`, `<%= yield :sidebar %>` and `<% if content_for?(:x) %>`
|
|
79
|
+
# are what a layout is made of, and the first two are legal ERB and illegal Ruby anywhere outside a
|
|
80
|
+
# method body — so every layout's compiled source failed to parse and the file was declined (four of
|
|
81
|
+
# redmine's 506 templates, two of mastodon's 46; `docs/notes/20260917-erb-template-units.md` § 3).
|
|
82
|
+
#
|
|
83
|
+
# The seam deliberately does NOT wrap a unit's body in a synthesised method
|
|
84
|
+
# (`docs/internal-spec/macro-substrate.md` § Positions: wrapping would shift every line and compose
|
|
85
|
+
# a second line map onto the plugin's), so the fix is a rewrite of the TEMPLATE, in the same pre-pass
|
|
86
|
+
# {BLOCK_EXPR} and {TRIM_OPEN} live in: the `yield` KEYWORD inside an ERB tag becomes a call to
|
|
87
|
+
# {YIELD_METHOD}, an ordinary implicit-self method on the synthesised view context, declared in
|
|
88
|
+
# `sig/action_view.rbs` as returning `String`.
|
|
89
|
+
#
|
|
90
|
+
# Unlike the other two rewrites this one is NOT width-preserving, and it does not have to be: a unit
|
|
91
|
+
# whose `line_map` is non-empty reports at column 1 by construction
|
|
92
|
+
# ({Analysis::TemplateUnits#remap} — "the template's lines and the compiled Ruby's columns do not
|
|
93
|
+
# correspond"), so only the LINE count is load-bearing, and the replacement contains no newline.
|
|
94
|
+
#
|
|
95
|
+
# What the rewrite deliberately does not do is give the call a TYPE beyond `String`. Rails' `yield`
|
|
96
|
+
# returns whatever the inner template's buffer holds, and `yield :sidebar` returns the `content_for`
|
|
97
|
+
# buffer — an empty `SafeBuffer` when nothing was provided, never nil. A lenient `String` is the
|
|
98
|
+
# widest honest reading, and inventing anything narrower would be the `Parameters#[]` trap one
|
|
99
|
+
# layer up.
|
|
100
|
+
YIELD_METHOD = "__rigor_yield"
|
|
101
|
+
|
|
102
|
+
# The `yield` keyword, and only the keyword: not `foo.yield`, not `:yield`, not `@yield`, not
|
|
103
|
+
# `yielding`. A `yield` inside a STRING literal in a tag (`<%= t("yield") %>`) is rewritten too —
|
|
104
|
+
# the pre-pass is a Regexp over the tag, not a Ruby lexer — which changes a literal's bytes in a
|
|
105
|
+
# body that is never executed and that no rule reads the contents of. Recorded rather than guarded,
|
|
106
|
+
# because a lexer here would be a second Ruby parser to keep honest.
|
|
107
|
+
YIELD_KEYWORD = /(?<![A-Za-z0-9_.:@$])yield(?![A-Za-z0-9_?!])/
|
|
108
|
+
private_constant :YIELD_KEYWORD
|
|
109
|
+
|
|
110
|
+
# Any ERB tag, non-greedy and multi-line — the region {YIELD_KEYWORD} is applied inside, so a
|
|
111
|
+
# `yield` in the template's HTML text is left exactly as it was written. A `<%%` opener is ERB's
|
|
112
|
+
# escape for a LITERAL `<%` in the output, so what follows it is text too and is skipped.
|
|
113
|
+
ANY_TAG = /<%.*?%>/m
|
|
114
|
+
private_constant :ANY_TAG
|
|
115
|
+
|
|
116
|
+
# `<%= … %>` and `<%== … %>`, non-greedy and multi-line. No capture group: the block below reads
|
|
117
|
+
# the tag it was handed rather than `Regexp.last_match`, whose group is `String?` however sure
|
|
118
|
+
# the pattern makes it — and a rewrite that depends on global match state is the harder one to
|
|
119
|
+
# read anyway.
|
|
120
|
+
OUTPUT_TAG = /<%={1,2}(?!=).*?%>/m
|
|
121
|
+
private_constant :OUTPUT_TAG
|
|
122
|
+
|
|
123
|
+
# The `<%=` / `<%==` opener of a matched tag, which is all that gets rewritten.
|
|
124
|
+
OUTPUT_OPENER = /\A<%=+/
|
|
125
|
+
private_constant :OUTPUT_OPENER
|
|
126
|
+
|
|
127
|
+
module_function
|
|
128
|
+
|
|
129
|
+
# `[ruby_source, line_map, transform_id]` for one template's bytes. `text` must already be valid
|
|
130
|
+
# UTF-8 — {.scrub} is the caller's, because more than one thing reads the template and every one
|
|
131
|
+
# of them has to read the SAME bytes.
|
|
132
|
+
#
|
|
133
|
+
# `line_map` is `{ compiled line => template line }` for every template line, explicitly — NOT the
|
|
134
|
+
# empty "identity" map, even when the offset is zero. An empty map tells the engine the transform
|
|
135
|
+
# was byte-preserving and lets the compiled Ruby's COLUMNS through, and an ERB column names
|
|
136
|
+
# nothing in the template (`macro-substrate.md` § Positions).
|
|
137
|
+
def compile(text)
|
|
138
|
+
offset = line_offset
|
|
139
|
+
raise Unmappable, "#{transform_id} does not emit template lines one per compiled line" if offset.nil?
|
|
140
|
+
|
|
141
|
+
compiled = compile_source(normalize(text))
|
|
142
|
+
map = (1..template_line_count(text)).to_h { |line| [line + offset, line] }
|
|
143
|
+
[compiled, map, transform_id]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# A template's bytes as valid UTF-8. A `.erb` file is whatever the project committed, and an
|
|
147
|
+
# invalid byte is not a reason to refuse it: `ERB`, `Regexp#match?` and Prism all raise on one,
|
|
148
|
+
# so every reader has to be handed the same scrubbed String or they disagree about the file.
|
|
149
|
+
def scrub(source)
|
|
150
|
+
text = source.to_s.dup.force_encoding(Encoding::UTF_8)
|
|
151
|
+
return text if text.valid_encoding?
|
|
152
|
+
|
|
153
|
+
text.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# The compiled Ruby for one template, through whichever compiler resolved.
|
|
157
|
+
def compile_source(text)
|
|
158
|
+
if erubi?
|
|
159
|
+
::Erubi::Engine.new(text).src
|
|
160
|
+
else
|
|
161
|
+
::ERB.new(text, trim_mode: nil).src
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Rewrites every block-opening output tag into a statement tag; see {BLOCK_EXPR}. Line- and
|
|
166
|
+
# width-preserving by construction: only the tag's `=` characters are replaced, by spaces.
|
|
167
|
+
def normalize(text)
|
|
168
|
+
normalize_block_expressions(normalize_yields(text.gsub(TRIM_OPEN, "<% ").gsub(TRIM_CLOSE, " %>")))
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Rewrites the `yield` keyword inside every ERB tag into a call on the view context; see
|
|
172
|
+
# {YIELD_METHOD}. Runs BEFORE {#normalize_block_expressions} so that pass reads the tag bodies it
|
|
173
|
+
# will actually hand to the compiler.
|
|
174
|
+
def normalize_yields(text)
|
|
175
|
+
return text unless text.match?(YIELD_KEYWORD)
|
|
176
|
+
|
|
177
|
+
text.gsub(ANY_TAG) { |tag| tag.start_with?("<%%") ? tag : tag.gsub(YIELD_KEYWORD, YIELD_METHOD) }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def normalize_block_expressions(text)
|
|
181
|
+
text.gsub(OUTPUT_TAG) do |tag|
|
|
182
|
+
opener = tag[OUTPUT_OPENER]
|
|
183
|
+
next tag if opener.nil?
|
|
184
|
+
|
|
185
|
+
body = tag[opener.length...-2].to_s
|
|
186
|
+
next tag unless body.match?(BLOCK_EXPR)
|
|
187
|
+
|
|
188
|
+
"<%#{' ' * (opener.length - 2)}#{body}%>"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# A template's last line may lack its newline; `String#lines` already counts it, and an EMPTY
|
|
193
|
+
# template has no lines at all and needs no rows.
|
|
194
|
+
def template_line_count(text)
|
|
195
|
+
text.lines.length
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# How many compiled lines sit above the template's first line. Measured once per process per
|
|
199
|
+
# compiler — the answer cannot change under a loaded compiler, and the probe compile is cheap
|
|
200
|
+
# enough that memoising it is about tidiness rather than cost.
|
|
201
|
+
def line_offset
|
|
202
|
+
return @line_offset if defined?(@line_offset) && @line_offset
|
|
203
|
+
|
|
204
|
+
@line_offset = probe_offset
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# `nil` when the loaded compiler does not step line-for-line.
|
|
208
|
+
def probe_offset
|
|
209
|
+
lines = compile_source(PROBE).lines
|
|
210
|
+
first = lines.index { |line| line.include?("RIGOR_ERB_PROBE_1") }
|
|
211
|
+
return nil if first.nil?
|
|
212
|
+
|
|
213
|
+
(2..3).each do |marker|
|
|
214
|
+
return nil unless lines[first + marker - 1]&.include?("RIGOR_ERB_PROBE_#{marker}")
|
|
215
|
+
end
|
|
216
|
+
first
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# `erubi-1.13.1` / `erb-6.0.1.1` — the compiler's identity, which rides the unit digest.
|
|
220
|
+
def transform_id
|
|
221
|
+
@transform_id ||= if erubi?
|
|
222
|
+
"erubi-#{::Erubi::VERSION}"
|
|
223
|
+
else
|
|
224
|
+
"erb-#{::ERB.const_defined?(:VERSION) ? ::ERB::VERSION : 'stdlib'}"
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# True once Erubi has been loaded out of the analysed project's bundle AND its line numbering has
|
|
229
|
+
# been measured. Resolved ONCE per process: a failed resolution is as memoised as a successful
|
|
230
|
+
# one, so a project without Erubi does not pay a `$LOAD_PATH` walk per template.
|
|
231
|
+
#
|
|
232
|
+
# `Isolation.require_with_target_bundle` is [ADR-90](../../../../../../docs/adr/90-target-library-resolution-from-project-bundle.md)'s
|
|
233
|
+
# path — Rigor's own gem env first, then the analysed project's bundle, `$LOAD_PATH` appended and
|
|
234
|
+
# only on a failed require. Erubi is not a Rigor dependency and is never added to one. Any load
|
|
235
|
+
# failure is equally "no Erubi here"; stdlib `ERB` answers the same question, so nothing is
|
|
236
|
+
# reported and nothing degrades but the compiler's name.
|
|
237
|
+
#
|
|
238
|
+
# A resolved Erubi that does NOT step line-for-line is **demoted** rather than trusted. Erubi pads
|
|
239
|
+
# its output to the template's line count deliberately (it is why a Rails backtrace can name
|
|
240
|
+
# `show.html.erb:12`), so the demotion is not expected to fire — but a compiler whose numbering
|
|
241
|
+
# the probe cannot confirm would silently report every finding in every template at the wrong
|
|
242
|
+
# line, and stdlib ERB is right here and always present.
|
|
243
|
+
def erubi?
|
|
244
|
+
return @erubi if defined?(@erubi)
|
|
245
|
+
|
|
246
|
+
@erubi = load_erubi
|
|
247
|
+
return @erubi unless @erubi
|
|
248
|
+
return true unless probe_offset.nil?
|
|
249
|
+
|
|
250
|
+
@erubi = false
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def load_erubi
|
|
254
|
+
Isolation.require_with_target_bundle("erubi", Isolation.target_bundle_root)
|
|
255
|
+
defined?(::Erubi::Engine) ? true : false
|
|
256
|
+
rescue ::StandardError, ::LoadError
|
|
257
|
+
false
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Test seam: forget the probed offset, the resolved compiler and its id, so one process can measure
|
|
261
|
+
# both compilers. Never called by the plugin itself.
|
|
262
|
+
def reset!
|
|
263
|
+
remove_instance_variable(:@line_offset) if defined?(@line_offset)
|
|
264
|
+
remove_instance_variable(:@transform_id) if defined?(@transform_id)
|
|
265
|
+
remove_instance_variable(:@erubi) if defined?(@erubi)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "controller_scan"
|
|
6
|
+
require_relative "erb_compiler"
|
|
7
|
+
require_relative "view_assigns"
|
|
8
|
+
require_relative "view_units"
|
|
9
|
+
|
|
10
|
+
module Rigor
|
|
11
|
+
module Plugin
|
|
12
|
+
class Actionpack < Rigor::Plugin::Base
|
|
13
|
+
# #1047 — the locals a render site passes, indexed by the template they are passed TO.
|
|
14
|
+
#
|
|
15
|
+
# #393 seeded a template's `locals:` from the Rails 7.1 strict-locals comment only, so a partial
|
|
16
|
+
# without one read its own parameters as method calls on the view context. That absence has a
|
|
17
|
+
# measured cost rather than a theoretical one: redmine's `app/views/common/_other.html.erb` opens
|
|
18
|
+
# with the standard optional-local preamble
|
|
19
|
+
#
|
|
20
|
+
# <% path = nil unless defined? path %>
|
|
21
|
+
#
|
|
22
|
+
# and the compiled Ruby really does assign nil there, because nothing told the unit that `path` is
|
|
23
|
+
# bound — so the flow rules folded three live branches and `flow.` had to join `call.` in the
|
|
24
|
+
# plugin's default suppressed set for that reason alone
|
|
25
|
+
# (`docs/notes/20260917-erb-template-units.md` § 4). Tracing the render site is what lets the flow
|
|
26
|
+
# rules back into a view.
|
|
27
|
+
#
|
|
28
|
+
# ## Why over-binding is the safe direction and under-binding is not
|
|
29
|
+
#
|
|
30
|
+
# A name this index seeds that the render site did not really pass costs nothing: the local binds
|
|
31
|
+
# `Dynamic`, `defined?` reads it as bound, and no rule folds anything. A name it MISSES is the
|
|
32
|
+
# false positive above. So the union across render sites is deliberately generous — a name bound at
|
|
33
|
+
# only some of a partial's render sites is seeded anyway, typed `Dynamic`, never left absent — and
|
|
34
|
+
# every shape below that cannot be settled from literals contributes a NAME with no type rather
|
|
35
|
+
# than nothing.
|
|
36
|
+
#
|
|
37
|
+
# The one thing that is not generous is the TYPE. A type is a claim the engine acts on, so a name
|
|
38
|
+
# keeps a concrete type only when every site that passes it passes the same one; anything else is
|
|
39
|
+
# {ViewUnits::UNKNOWN_LOCAL}, which the engine binds `Dynamic[top]` (ADR-5).
|
|
40
|
+
#
|
|
41
|
+
# ## What a render site is read for
|
|
42
|
+
#
|
|
43
|
+
# Both sides of the render, because both bind the same thing:
|
|
44
|
+
#
|
|
45
|
+
# - a **controller** site, through {Analyzer.render_target_for} — `render partial: "card",
|
|
46
|
+
# locals: { user: @user }`;
|
|
47
|
+
# - a **template** site, in the compiled Ruby of every view — the same keyword form, plus the
|
|
48
|
+
# positional-hash form `render "card", user: @user`, which is locals in a view and OPTIONS in a
|
|
49
|
+
# controller (`ActionView::Helpers::RenderingHelper#render(options, locals)` against
|
|
50
|
+
# `ActionController::Base#render(options)`). Reading the controller form as locals would seed a
|
|
51
|
+
# `status` local for every `render :show, status: :ok` in the corpus.
|
|
52
|
+
#
|
|
53
|
+
# `collection:` and `object:` bind a local named after the partial, or after `as:` when given, and a
|
|
54
|
+
# collection additionally binds the `_counter` and `_iteration` companions Rails provides.
|
|
55
|
+
#
|
|
56
|
+
# ## The compiled-source cache
|
|
57
|
+
#
|
|
58
|
+
# Building the index compiles every template in the project, and `#template_units_for_file` is about
|
|
59
|
+
# to compile each of them again. So the builder keeps what it compiled, keyed by path and guarded by
|
|
60
|
+
# the template's own scrubbed bytes, and the hook reuses it — a full run does exactly the compile
|
|
61
|
+
# work it did before this feature, one pass instead of two. An editor buffer's bytes differ from the
|
|
62
|
+
# file's, so its template falls through to its own compile; the index itself is built once per
|
|
63
|
+
# plugin instance and revalidated once per collection pass against {Builder.fingerprint}, so an
|
|
64
|
+
# unsaved buffer's render sites are NOT read — its partials keep the locals the saved file passes
|
|
65
|
+
# until the save.
|
|
66
|
+
class RenderLocals
|
|
67
|
+
# What a name is seeded with when no site settled a type for it, or when two disagreed.
|
|
68
|
+
UNKNOWN = ViewUnits::UNKNOWN_LOCAL
|
|
69
|
+
|
|
70
|
+
# Finders and constructors whose Rails implementation raises rather than returning nil — the same
|
|
71
|
+
# closed set {ViewAssigns::NON_NIL_PRODUCERS} names, and for the same reason: a nil-free nominal
|
|
72
|
+
# standing in for a runtime nil licenses folds that report correct code.
|
|
73
|
+
NON_NIL_PRODUCERS = ViewAssigns::NON_NIL_PRODUCERS
|
|
74
|
+
|
|
75
|
+
# `{ "common/_other" => { "path" => UNKNOWN, … } }`, keyed by logical name with the format
|
|
76
|
+
# stripped — a partial's locals do not depend on which format rendered it.
|
|
77
|
+
attr_reader :by_template
|
|
78
|
+
|
|
79
|
+
# The {Builder.fingerprint} of the sources this index was built from, taken BEFORE they were read —
|
|
80
|
+
# so an edit that lands during the build reads as a change on the next pass rather than being
|
|
81
|
+
# recorded as already seen. nil for {.empty}, which therefore never matches a real fingerprint.
|
|
82
|
+
attr_reader :fingerprint
|
|
83
|
+
|
|
84
|
+
# Every method name a project helper `def`s — see {ViewUnits.self_declared_locals}.
|
|
85
|
+
attr_reader :helper_methods
|
|
86
|
+
|
|
87
|
+
def initialize(by_template, compiled = {}, fingerprint: nil, helper_methods: {})
|
|
88
|
+
@by_template = by_template.freeze
|
|
89
|
+
@compiled = compiled.freeze
|
|
90
|
+
@fingerprint = fingerprint
|
|
91
|
+
@helper_methods = helper_methods.freeze
|
|
92
|
+
freeze
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.empty
|
|
96
|
+
new({})
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# `common/_other.html` → the names every site that renders `common/_other` passed.
|
|
100
|
+
def seeds_for(logical_name)
|
|
101
|
+
@by_template.fetch(logical_name.to_s.sub(%r{\.[^./]+\z}, ""), {})
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The `[compiled, line_map, transform_id, parse_ok]` this index already computed for `path`, or nil
|
|
105
|
+
# when it read different bytes (an editor buffer) or never saw the file. The bytes are compared
|
|
106
|
+
# rather than trusted: a unit compiled from the file on disk must never stand in for one the editor
|
|
107
|
+
# is holding.
|
|
108
|
+
def compiled_for(path, text)
|
|
109
|
+
entry = @compiled[path.to_s]
|
|
110
|
+
return nil if entry.nil? || entry.first != text
|
|
111
|
+
|
|
112
|
+
entry.drop(1)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Builds the index from the controller sources and the templates the plugin claims.
|
|
116
|
+
class Builder
|
|
117
|
+
def initialize(io_boundary:, controller_search_paths:, view_search_paths:, view_assigns:,
|
|
118
|
+
helper_search_paths: [])
|
|
119
|
+
@io_boundary = io_boundary
|
|
120
|
+
@controller_search_paths = controller_search_paths
|
|
121
|
+
@view_search_paths = view_search_paths
|
|
122
|
+
@helper_search_paths = helper_search_paths
|
|
123
|
+
@view_assigns = view_assigns
|
|
124
|
+
# `{ target => [ { name => type_name_or_nil }, … ] }` — one hash per render site, merged only
|
|
125
|
+
# once every site has been seen, because "bound at some sites" is a property of the whole set.
|
|
126
|
+
@sites = Hash.new { |hash, key| hash[key] = [] }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def build
|
|
130
|
+
fingerprint = self.class.fingerprint(
|
|
131
|
+
io_boundary: @io_boundary, controller_search_paths: @controller_search_paths,
|
|
132
|
+
view_search_paths: @view_search_paths, helper_search_paths: @helper_search_paths
|
|
133
|
+
)
|
|
134
|
+
harvest_controllers
|
|
135
|
+
compiled = harvest_templates
|
|
136
|
+
RenderLocals.new(@sites.transform_values { |sites| merge(sites) }, compiled,
|
|
137
|
+
fingerprint: fingerprint, helper_methods: harvest_helper_methods)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# What the index was built from, as one String: every controller, helper and template path with
|
|
141
|
+
# its size and mtime. A glob catches a file appearing or vanishing and a stat catches an edit;
|
|
142
|
+
# neither reads a byte, so revalidating costs one `stat` per file, once per collection pass.
|
|
143
|
+
#
|
|
144
|
+
# There is deliberately no content-digest fallback, unlike the collector's ADR-87 pack: a
|
|
145
|
+
# byte-identical `touch` moves the fingerprint and rebuilds this index while the collector still
|
|
146
|
+
# carries the units. That costs one rebuild and never a wrong answer — the rebuilt index is the
|
|
147
|
+
# index the carried units were seeded from — so a digest per file per pass is not worth buying.
|
|
148
|
+
def self.fingerprint(io_boundary:, controller_search_paths:, view_search_paths:, helper_search_paths: [])
|
|
149
|
+
files = source_files(io_boundary, controller_search_paths, "*.rb") +
|
|
150
|
+
source_files(io_boundary, helper_search_paths, "*.rb") +
|
|
151
|
+
source_files(io_boundary, view_search_paths, "*.erb")
|
|
152
|
+
files.map do |path|
|
|
153
|
+
stat = File.stat(path)
|
|
154
|
+
"#{path}\0#{stat.size}\0#{stat.mtime.to_r}"
|
|
155
|
+
rescue SystemCallError
|
|
156
|
+
"#{path}\0gone"
|
|
157
|
+
end.join("\n")
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Project-relative paths, the spelling `#template_units_for_file` is handed and the spelling the
|
|
161
|
+
# compiled cache is keyed by.
|
|
162
|
+
def self.source_files(io_boundary, roots, pattern)
|
|
163
|
+
root_prefix = "#{Dir.pwd}#{File::SEPARATOR}"
|
|
164
|
+
roots.flat_map do |root|
|
|
165
|
+
absolute = File.expand_path(root)
|
|
166
|
+
next [] unless io_boundary.directory?(absolute)
|
|
167
|
+
|
|
168
|
+
Dir.glob(File.join(absolute, "**", pattern)).map { |path| path.delete_prefix(root_prefix) }
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
private
|
|
173
|
+
|
|
174
|
+
# Two sites that agree on a name and its type keep the type; anything else — a disagreement, a
|
|
175
|
+
# site that settled no type, or a site that did not pass the name at all — is `Dynamic`.
|
|
176
|
+
def merge(sites)
|
|
177
|
+
names = sites.flat_map(&:keys).uniq
|
|
178
|
+
names.to_h do |name|
|
|
179
|
+
types = sites.map { |site| site.key?(name) ? site[name] : :absent }.uniq
|
|
180
|
+
[name, types.length == 1 && types.first.is_a?(String) ? types.first : UNKNOWN]
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
HELPER_DEF = /^[ \t]*def[ \t]+(?:self\.)?([a-z_][A-Za-z0-9_]*[?!]?)/
|
|
185
|
+
private_constant :HELPER_DEF
|
|
186
|
+
|
|
187
|
+
# `{ "current_user" => true }` for every `def` in a project helper. A line scan rather than a
|
|
188
|
+
# parse: the answer only has to be a superset of the names a helper can answer, and a name read
|
|
189
|
+
# out of a comment or a heredoc merely leaves one template's `defined?` test unseeded — which is
|
|
190
|
+
# what #393 shipped for every name.
|
|
191
|
+
def harvest_helper_methods
|
|
192
|
+
source_files(@helper_search_paths, "*.rb").each_with_object({}) do |path, names|
|
|
193
|
+
read(path)&.scan(HELPER_DEF)&.each { |(name)| names[name] = true }
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def harvest_controllers
|
|
198
|
+
source_files(@controller_search_paths, "*.rb").each do |path|
|
|
199
|
+
tree = parse_file(path)
|
|
200
|
+
next if tree.nil?
|
|
201
|
+
|
|
202
|
+
ControllerScan.each_controller(tree, []) do |node, namespace|
|
|
203
|
+
harvest_controller(node, namespace)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def harvest_controller(node, namespace)
|
|
209
|
+
segments = namespace + ControllerScan.constant_segments(node.constant_path)
|
|
210
|
+
prefix = ControllerScan.controller_path(segments)
|
|
211
|
+
return if prefix.nil?
|
|
212
|
+
|
|
213
|
+
ControllerScan.method_bodies(node).each do |action, body|
|
|
214
|
+
ivars = @view_assigns.seeds_for("#{prefix}/#{action}")
|
|
215
|
+
ControllerScan.each_render(body) do |call|
|
|
216
|
+
record(controller_target(call, prefix), call, ivars, view_side: false)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# {Analyzer.render_target_for} already answers this for a controller: `render :edit`,
|
|
222
|
+
# `render "admin/shared/form"`, `render partial: "card"`. Its `[:template, name]` /
|
|
223
|
+
# `[:partial, name]` pair reduces to the name, because the locals of a template and of a partial
|
|
224
|
+
# are seeded the same way.
|
|
225
|
+
def controller_target(call, prefix)
|
|
226
|
+
Analyzer.render_target_for(call, prefix)&.last
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Every claimed template, compiled once. Returns the `{ path => [text, compiled, map, id, ok] }`
|
|
230
|
+
# cache `#template_units_for_file` reuses.
|
|
231
|
+
def harvest_templates
|
|
232
|
+
source_files(@view_search_paths, "*.erb").each_with_object({}) do |path, compiled|
|
|
233
|
+
text = read(path)
|
|
234
|
+
next if text.nil?
|
|
235
|
+
|
|
236
|
+
text = ErbCompiler.scrub(text)
|
|
237
|
+
source, line_map, transform = ErbCompiler.compile(text)
|
|
238
|
+
result = Prism.parse(source)
|
|
239
|
+
compiled[path] = [text, source, line_map, transform, result.errors.empty?]
|
|
240
|
+
next unless result.errors.empty?
|
|
241
|
+
|
|
242
|
+
harvest_template(path, result.value)
|
|
243
|
+
end
|
|
244
|
+
rescue ErbCompiler::Unmappable
|
|
245
|
+
{}
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def harvest_template(path, tree)
|
|
249
|
+
name = ViewUnits.logical_name(path, @view_search_paths).sub(%r{\.[^./]+\z}, "")
|
|
250
|
+
directory = name.include?("/") ? File.dirname(name) : ""
|
|
251
|
+
ivars = @view_assigns.seeds_for(name)
|
|
252
|
+
ControllerScan.each_render(tree) do |call|
|
|
253
|
+
record(template_target(call, directory), call, ivars, view_side: true)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# The view-side reading of a render target, which differs from the controller's in exactly one
|
|
258
|
+
# place: a bare positional name is a PARTIAL here. `template:` is the escape hatch an author uses
|
|
259
|
+
# when they mean otherwise, and `layout:` in a view names a partial too — the same reading
|
|
260
|
+
# {Effects::CalleeRule.rails_render_partial} takes, for the same reason.
|
|
261
|
+
def template_target(call, directory)
|
|
262
|
+
explicit = string_option(call, "template")
|
|
263
|
+
return qualify(explicit, directory) if explicit
|
|
264
|
+
|
|
265
|
+
partial = string_option(call, "partial") || string_option(call, "layout")
|
|
266
|
+
return partialize(qualify(partial, directory)) if partial
|
|
267
|
+
|
|
268
|
+
positional = (call.arguments&.arguments || []).grep_v(Prism::KeywordHashNode).first
|
|
269
|
+
return nil unless positional.is_a?(Prism::StringNode) || positional.is_a?(Prism::SymbolNode)
|
|
270
|
+
|
|
271
|
+
partialize(qualify(positional.unescaped, directory))
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def record(target, call, ivars, view_side:)
|
|
275
|
+
return if target.nil? || target.empty?
|
|
276
|
+
|
|
277
|
+
locals = locals_at(call, target, ivars, view_side: view_side)
|
|
278
|
+
@sites[target] << locals
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# The names one render site binds, and the type of each where the site settled one.
|
|
282
|
+
def locals_at(call, target, ivars, view_side:)
|
|
283
|
+
arguments = call.arguments&.arguments || []
|
|
284
|
+
options = arguments.find { |argument| argument.is_a?(Prism::KeywordHashNode) }
|
|
285
|
+
locals = {}
|
|
286
|
+
# `render "card", user: @user` — the view's second positional argument IS the locals hash, and
|
|
287
|
+
# the controller's is options. Read alongside an explicit `locals:` rather than instead of it,
|
|
288
|
+
# because over-binding is the safe direction.
|
|
289
|
+
collect_pairs(options, locals, ivars) if view_side && !arguments.first.is_a?(Prism::KeywordHashNode)
|
|
290
|
+
collect_pairs(option(call, "locals"), locals, ivars)
|
|
291
|
+
collect_object_locals(call, target, locals, ivars)
|
|
292
|
+
locals
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# `locals: { user: @user }` and its hashrocket spelling. A key that is not a literal symbol or
|
|
296
|
+
# string — a computed key, a `**splat` — names nothing this can seed, and is skipped rather than
|
|
297
|
+
# guessed at.
|
|
298
|
+
def collect_pairs(hash, locals, ivars)
|
|
299
|
+
return unless hash.is_a?(Prism::HashNode) || hash.is_a?(Prism::KeywordHashNode)
|
|
300
|
+
|
|
301
|
+
hash.elements.each do |element|
|
|
302
|
+
next unless element.is_a?(Prism::AssocNode)
|
|
303
|
+
|
|
304
|
+
name = literal_name(element.key)
|
|
305
|
+
next if name.nil? || !name.match?(/\A[a-z_][A-Za-z0-9_]*\z/)
|
|
306
|
+
|
|
307
|
+
locals[name] = value_type(element.value, ivars)
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# `object:` binds one local named after the partial; `collection:` binds the same name plus the
|
|
312
|
+
# `_counter` and `_iteration` companions Rails provides, and never a type — the local is the
|
|
313
|
+
# ELEMENT, and nothing at the site names the element's class.
|
|
314
|
+
def collect_object_locals(call, target, locals, ivars)
|
|
315
|
+
base = literal_name(option(call, "as")) || File.basename(target).delete_prefix("_")
|
|
316
|
+
return unless base.match?(/\A[a-z_][A-Za-z0-9_]*\z/)
|
|
317
|
+
|
|
318
|
+
object = option(call, "object")
|
|
319
|
+
collection = option(call, "collection")
|
|
320
|
+
locals[base] = value_type(object, ivars) if object
|
|
321
|
+
return if collection.nil?
|
|
322
|
+
|
|
323
|
+
locals[base] = nil
|
|
324
|
+
locals["#{base}_counter"] = nil
|
|
325
|
+
locals["#{base}_iteration"] = nil
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# The two shapes a render site settles a type from, and nothing else: a non-nil-able finder or
|
|
329
|
+
# constructor on a constant (`User.find(params[:id])`), and an ivar the rendering unit's own
|
|
330
|
+
# seeds already typed. Anything else is a name with no type.
|
|
331
|
+
def value_type(node, ivars)
|
|
332
|
+
case node
|
|
333
|
+
when Prism::InstanceVariableReadNode then ivars[node.name.to_s]
|
|
334
|
+
when Prism::CallNode then produced_type(node)
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def produced_type(call)
|
|
339
|
+
return nil unless NON_NIL_PRODUCERS.include?(call.name)
|
|
340
|
+
|
|
341
|
+
receiver = call.receiver
|
|
342
|
+
return nil unless receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
|
|
343
|
+
|
|
344
|
+
name = ControllerScan.constant_segments(receiver).join("::")
|
|
345
|
+
name.empty? ? nil : name
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def option(call, key)
|
|
349
|
+
hash = call.arguments&.arguments&.find { |argument| argument.is_a?(Prism::KeywordHashNode) }
|
|
350
|
+
pair = hash&.elements&.find do |element|
|
|
351
|
+
element.is_a?(Prism::AssocNode) && literal_name(element.key) == key
|
|
352
|
+
end
|
|
353
|
+
pair&.value
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def string_option(call, key)
|
|
357
|
+
value = option(call, key)
|
|
358
|
+
value.is_a?(Prism::StringNode) || value.is_a?(Prism::SymbolNode) ? value.unescaped : nil
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def literal_name(node)
|
|
362
|
+
node.unescaped if node.is_a?(Prism::SymbolNode) || node.is_a?(Prism::StringNode)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def qualify(name, directory)
|
|
366
|
+
return nil if name.nil? || name.empty?
|
|
367
|
+
return name if name.include?("/") || directory.empty?
|
|
368
|
+
|
|
369
|
+
"#{directory}/#{name}"
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def partialize(name)
|
|
373
|
+
return nil if name.nil?
|
|
374
|
+
|
|
375
|
+
directory, separator, base = name.rpartition("/")
|
|
376
|
+
return base.start_with?("_") ? name : "_#{name}" if separator.empty?
|
|
377
|
+
|
|
378
|
+
base.start_with?("_") ? name : "#{directory}/_#{base}"
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def source_files(roots, pattern)
|
|
382
|
+
self.class.source_files(@io_boundary, roots, pattern)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def read(path)
|
|
386
|
+
@io_boundary.read_file(File.expand_path(path))
|
|
387
|
+
rescue Plugin::AccessDeniedError, Errno::ENOENT
|
|
388
|
+
nil
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def parse_file(path)
|
|
392
|
+
contents = read(path)
|
|
393
|
+
return nil if contents.nil?
|
|
394
|
+
|
|
395
|
+
result = Prism.parse(contents)
|
|
396
|
+
result.errors.empty? ? result.value : nil
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
end
|